diff --git a/Cargo.lock b/Cargo.lock
index c87308c3e0cf5a9e4e336c6fd6a81f77b40ea35b..6f1b41c6a7f77b86a07fe3c23c3a9086cf6e3422 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,3 +1,11 @@
+[[package]]
+name = "aho-corasick"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "base58"
 version = "0.1.0"
@@ -40,6 +48,19 @@ dependencies = [
  "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "duniter-protocol"
+version = "0.1.0"
+dependencies = [
+ "base58 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "base64 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "duniter-keys 0.3.0",
+ "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "linked-hash-map 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "duniter-wotb"
 version = "0.4.1"
@@ -68,11 +89,29 @@ name = "gcc"
 version = "0.3.54"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "lazy_static"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "libc"
 version = "0.2.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "linked-hash-map"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "memchr"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.35 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "quote"
 version = "0.3.15"
@@ -92,6 +131,23 @@ name = "redox_syscall"
 version = "0.1.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "regex"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex-syntax 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "rust-crypto"
 version = "0.2.36"
@@ -156,6 +212,15 @@ dependencies = [
  "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "thread_local"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "time"
 version = "0.1.39"
@@ -171,6 +236,24 @@ name = "unicode-xid"
 version = "0.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "unreachable"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "utf8-ranges"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "winapi"
 version = "0.3.3"
@@ -191,6 +274,7 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [metadata]
+"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4"
 "checksum base58 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5024ee8015f02155eee35c711107ddd9a9bf3cb689cf2a9089c97e79b6e1ae83"
 "checksum base64 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7c4a342b450b268e1be8036311e2c613d7f8a7ed31214dff1cc3b60852a3168d"
 "checksum bincode 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9a6301db0b49fb63551bc15b5ae348147101cdf323242b93ec7546d5002ff1af"
@@ -199,10 +283,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
 "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
 "checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb"
+"checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d"
 "checksum libc 0.2.35 (registry+https://github.com/rust-lang/crates.io-index)" = "96264e9b293e95d25bfcbbf8a88ffd1aedc85b754eba8b7d78012f638ba220eb"
+"checksum linked-hash-map 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d2aab0478615bb586559b0114d94dd8eca4fdbb73b443adcb0d00b61692b4bf"
+"checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d"
 "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
 "checksum rand 0.3.20 (registry+https://github.com/rust-lang/crates.io-index)" = "512870020642bb8c221bf68baa1b2573da814f6ccfe5c9699b1c303047abe9b1"
 "checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd"
+"checksum regex 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "744554e01ccbd98fff8c457c3b092cd67af62a555a43bfe97ae8a0451f7799fa"
+"checksum regex-syntax 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8e931c58b93d86f080c734bfd2bce7dd0079ae2331235818133c8be7f422e20e"
 "checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a"
 "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
 "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
@@ -211,8 +300,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum serde_derive_internals 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6e03f1c9530c3fb0a0a5c9b826bdd9246a5921ae995d75f512ac917fc4dd55b5"
 "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
 "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
+"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963"
 "checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098"
 "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
+"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
+"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122"
+"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
 "checksum winapi 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b09fb3b6f248ea4cd42c9a65113a847d612e17505d6ebd1f7357ad68a8bf8693"
 "checksum winapi-i686-pc-windows-gnu 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ec6667f60c23eca65c561e63a13d81b44234c2e38a6b6c959025ee907ec614cc"
 "checksum winapi-x86_64-pc-windows-gnu 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "98f12c52b2630cd05d2c3ffd8e008f7f48252c042b4871c72aed9dc733b96668"
diff --git a/Cargo.toml b/Cargo.toml
index f6e34e4f0c5e7962fac73be4770deaf948689044..c2e9aead4099b2c8caad969b0b019ba38df5df0e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,4 +2,5 @@
 members = [
     "wotb",
     "keys",
+    "protocol",
 ]
diff --git a/keys/README.md b/keys/README.md
index f388b3dd079be7373ebe7881f484c6ab5b23dd8e..288814b3ab032d5e36b5201d0eda571fd0f020b5 100644
--- a/keys/README.md
+++ b/keys/README.md
@@ -1,9 +1,9 @@
-# Introduction
+# keys
 
 `keys` is a crate managing cryptographic keys for the Duniter project.
 
 [Duniter]: https://duniter.org/en/
 
-# How to use it ?
+## How to use it
 
 You can add `duniter-keys` as a `cargo` dependency in your Rust project.
diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..8faccd1cde85fd5174e5f82610637db99d9e8cb2
--- /dev/null
+++ b/protocol/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "duniter-protocol"
+version = "0.1.0"
+authors = ["nanocryk <nanocryk@duniter.org>"]
+description = "Implements the Duniter Protocol"
+repository = "https://git.duniter.org/nodes/rust/duniter-rs"
+readme = "README.md"
+keywords = ["duniter", "blockchain", "cryptocurrency", "document"]
+license = "AGPL-3.0"
+
+[lib]
+path = "lib.rs"
+
+[dependencies]
+rust-crypto = "0.2.36"
+linked-hash-map = "0.5.0"
+base58 = "0.1.0"
+base64 = "0.8.0"
+lazy_static = "1.0.0"
+regex = "0.2"
+duniter-keys = { path = "../keys" }
diff --git a/protocol/README.md b/protocol/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d9a912991098f9ad2a437a6c3b666bf8b4fe9eea
--- /dev/null
+++ b/protocol/README.md
@@ -0,0 +1,10 @@
+# protocol
+
+`protocol` is a crate implementing the [Duniter] Protocol in [version 10][version10].
+
+[Duniter]: https://duniter.org/en/
+[version10]: https://git.duniter.org/nodes/typescript/duniter/blob/master/doc/Protocol.md
+
+## How to use it
+
+You can add `duniter-keys` as a `cargo` dependency in your Rust project.
diff --git a/protocol/blockchain/mod.rs b/protocol/blockchain/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ba814282a2c29256f431b2c0104543ca674c2d3e
--- /dev/null
+++ b/protocol/blockchain/mod.rs
@@ -0,0 +1,255 @@
+//  Copyright (C) 2018  The Duniter Project Developers.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+//! Provide wrappers around Duniter documents and events.
+
+use std::fmt::Debug;
+
+use duniter_keys::{PrivateKey, PublicKey};
+
+pub mod v10;
+
+/// List of blockchain protocol versions.
+#[derive(Debug)]
+pub enum BlockchainProtocol {
+    /// Version 10.
+    V10(v10::documents::V10Document),
+    /// Version 11. (not done yet, but defined for tests)
+    V11(),
+}
+
+/// trait providing commun methods for any documents of any protocol version.
+///
+/// # Design choice
+///
+/// Allow only ed25519 for protocol 10 and many differents
+/// schemes for protocol 11 through a proxy type.
+pub trait Document: Debug {
+    /// Type of the `PublicKey` used by the document.
+    type PublicKey: PublicKey;
+    /// Data type of the currency code used by the document.
+    type CurrencyType: ?Sized;
+
+    /// Get document version.
+    fn version(&self) -> u16;
+
+    /// Get document currency.
+    fn currency(&self) -> &Self::CurrencyType;
+
+    /// Iterate over document issuers.
+    fn issuers(&self) -> &Vec<Self::PublicKey>;
+
+    /// Iterate over document signatures.
+    fn signatures(&self) -> &Vec<<Self::PublicKey as PublicKey>::Signature>;
+
+    /// Get document as bytes for signature verification.
+    fn as_bytes(&self) -> &[u8];
+
+    /// Verify signatures of document content (as text format)
+    fn verify_signatures(&self) -> VerificationResult {
+        let issuers_count = self.issuers().len();
+        let signatures_count = self.signatures().len();
+
+        if issuers_count != signatures_count {
+            VerificationResult::IncompletePairs(issuers_count, signatures_count)
+        } else {
+            let issuers = self.issuers();
+            let signatures = self.signatures();
+            let mismatches: Vec<_> = issuers
+                .iter()
+                .zip(signatures.iter())
+                .enumerate()
+                .filter(|&(_, (key, signature))| !key.verify(self.as_bytes(), signature))
+                .map(|(i, _)| i)
+                .collect();
+
+            if mismatches.is_empty() {
+                VerificationResult::Valid()
+            } else {
+                VerificationResult::Invalid(mismatches)
+            }
+        }
+    }
+}
+
+/// List of possible results for signature verification.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum VerificationResult {
+    /// All signatures are valid.
+    Valid(),
+    /// Not same amount of issuers and signatures.
+    /// (issuers count, signatures count)
+    IncompletePairs(usize, usize),
+    /// Signatures don't match.
+    /// List of mismatching pairs indexes.
+    Invalid(Vec<usize>),
+}
+
+/// Trait allowing access to the document through it's proper protocol version.
+///
+/// This trait is generic over `P` providing all supported protocol version variants.
+///
+/// A lifetime is specified to allow enum variants to hold references to the document.
+pub trait IntoSpecializedDocument<P> {
+    /// Get a protocol-specific document wrapped in an enum variant.
+    fn into_specialized(self) -> P;
+}
+
+/// Trait helper for building new documents.
+pub trait DocumentBuilder {
+    /// Type of the builded document.
+    type Document: Document;
+
+    /// Type of the private keys signing the documents.
+    type PrivateKey: PrivateKey<
+        Signature = <<Self::Document as Document>::PublicKey as PublicKey>::Signature,
+    >;
+
+    /// Build a document with provided signatures.
+    fn build_with_signature(
+        self,
+        signatures: Vec<<<Self::Document as Document>::PublicKey as PublicKey>::Signature>,
+    ) -> Self::Document;
+
+    /// Build a document and sign it with the private key.
+    fn build_and_sign(self, private_keys: Vec<Self::PrivateKey>) -> Self::Document;
+}
+
+/// Trait for a document parser from a `S` source
+/// format to a `D` document. Will return the
+/// parsed document or an `E` error.
+pub trait DocumentParser<S, D, E> {
+    /// Parse a source and return a document or an error.
+    fn parse(source: S) -> Result<D, E>;
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use duniter_keys::{Signature, ed25519};
+
+    // simple text document for signature testing
+    #[derive(Debug, Clone)]
+    struct PlainTextDocument {
+        pub text: &'static str,
+        pub issuers: Vec<ed25519::PublicKey>,
+        pub signatures: Vec<ed25519::Signature>,
+    }
+
+    impl Document for PlainTextDocument {
+        type PublicKey = ed25519::PublicKey;
+        type CurrencyType = str;
+
+        fn version(&self) -> u16 {
+            unimplemented!()
+        }
+
+        fn currency(&self) -> &str {
+            unimplemented!()
+        }
+
+        fn issuers(&self) -> &Vec<ed25519::PublicKey> {
+            &self.issuers
+        }
+
+        fn signatures(&self) -> &Vec<ed25519::Signature> {
+            &self.signatures
+        }
+
+        fn as_bytes(&self) -> &[u8] {
+            self.text.as_bytes()
+        }
+    }
+
+    #[test]
+    fn verify_signatures() {
+        let text = "Version: 10
+Type: Identity
+Currency: duniter_unit_test_currency
+Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV
+UniqueID: tic
+Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
+";
+
+        // good pair
+        let issuer1 = ed25519::PublicKey::from_base58(
+            "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV",
+        ).unwrap();
+
+        let sig1 = ed25519::Signature::from_base64(
+            "1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGMM\
+             mQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg==",
+        ).unwrap();
+
+        // incorrect pair
+        let issuer2 = ed25519::PublicKey::from_base58(
+            "DNann1Lh55eZMEDXeYt32bzHbA3NJR46DeQYCS2qQdLV",
+        ).unwrap();
+
+        let sig2 = ed25519::Signature::from_base64(
+            "1eubHHbuNfilHHH0G2bI30iZzebQ2cQ1PC7uPAw08FGMM\
+             mQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg==",
+        ).unwrap();
+
+        {
+            let doc = PlainTextDocument {
+                text,
+                issuers: vec![issuer1],
+                signatures: vec![sig1],
+            };
+
+            assert_eq!(doc.verify_signatures(), VerificationResult::Valid());
+        }
+
+        {
+            let doc = PlainTextDocument {
+                text,
+                issuers: vec![issuer1],
+                signatures: vec![sig2],
+            };
+
+            assert_eq!(
+                doc.verify_signatures(),
+                VerificationResult::Invalid(vec![0])
+            );
+        }
+
+        {
+            let doc = PlainTextDocument {
+                text,
+                issuers: vec![issuer1, issuer2],
+                signatures: vec![sig1],
+            };
+
+            assert_eq!(
+                doc.verify_signatures(),
+                VerificationResult::IncompletePairs(2, 1)
+            );
+        }
+
+        {
+            let doc = PlainTextDocument {
+                text,
+                issuers: vec![issuer1],
+                signatures: vec![sig1, sig2],
+            };
+
+            assert_eq!(
+                doc.verify_signatures(),
+                VerificationResult::IncompletePairs(1, 2)
+            );
+        }
+    }
+}
diff --git a/protocol/blockchain/v10/documents/identity.rs b/protocol/blockchain/v10/documents/identity.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8b733759c4fd902a90f75b21b238faa000c0b236
--- /dev/null
+++ b/protocol/blockchain/v10/documents/identity.rs
@@ -0,0 +1,273 @@
+//  Copyright (C) 2018  The Duniter Project Developers.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+//! Wrappers around Identity documents.
+
+use duniter_keys::{PublicKey, ed25519};
+use regex::Regex;
+
+use Blockstamp;
+use blockchain::{BlockchainProtocol, Document, DocumentBuilder, IntoSpecializedDocument};
+use blockchain::v10::documents::{StandardTextDocumentParser, TextDocument, TextDocumentBuilder,
+                                 V10Document, V10DocumentParsingError};
+
+lazy_static! {
+    static ref IDENTITY_REGEX: Regex = Regex::new(
+        "^Issuer: (?P<issuer>[1-9A-Za-z][^OIl]{43,44})\nUniqueID: (?P<uid>[[:alnum:]_-]+)\nTimestamp: (?P<blockstamp>[0-9]+-[0-9A-F]{64})\n$"
+    ).unwrap();
+}
+
+/// Wrap an Identity document.
+///
+/// Must be created by parsing a text document or using a builder.
+#[derive(Debug, Clone)]
+pub struct IdentityDocument {
+    /// Document as text.
+    ///
+    /// Is used to check signatures, and other values
+    /// must be extracted from it.
+    text: String,
+
+    /// Currency.
+    currency: String,
+    /// Unique ID
+    unique_id: String,
+    /// Blockstamp
+    blockstamp: Blockstamp,
+    /// Document issuer (there should be only one).
+    issuers: Vec<ed25519::PublicKey>,
+    /// Document signature (there should be only one).
+    signatures: Vec<ed25519::Signature>,
+}
+
+impl Document for IdentityDocument {
+    type PublicKey = ed25519::PublicKey;
+    type CurrencyType = str;
+
+    fn version(&self) -> u16 {
+        10
+    }
+
+    fn currency(&self) -> &str {
+        &self.currency
+    }
+
+    fn issuers(&self) -> &Vec<ed25519::PublicKey> {
+        &self.issuers
+    }
+
+    fn signatures(&self) -> &Vec<ed25519::Signature> {
+        &self.signatures
+    }
+
+    fn as_bytes(&self) -> &[u8] {
+        self.as_text().as_bytes()
+    }
+}
+
+impl TextDocument for IdentityDocument {
+    fn as_text(&self) -> &str {
+        &self.text
+    }
+}
+
+impl IntoSpecializedDocument<BlockchainProtocol> for IdentityDocument {
+    fn into_specialized(self) -> BlockchainProtocol {
+        BlockchainProtocol::V10(V10Document::Identity(self))
+    }
+}
+
+/// Identity document builder.
+#[derive(Debug, Copy, Clone)]
+pub struct IdentityDocumentBuilder<'a> {
+    /// Document currency.
+    pub currency: &'a str,
+    /// Identity unique id.
+    pub unique_id: &'a str,
+    /// Reference blockstamp.
+    pub blockstamp: &'a Blockstamp,
+    /// Document/identity issuer.
+    pub issuer: &'a ed25519::PublicKey,
+}
+
+impl<'a> IdentityDocumentBuilder<'a> {
+    fn build_with_text_and_sigs(
+        self,
+        text: String,
+        signatures: Vec<ed25519::Signature>,
+    ) -> IdentityDocument {
+        IdentityDocument {
+            text: text,
+            currency: self.currency.to_string(),
+            unique_id: self.unique_id.to_string(),
+            blockstamp: *self.blockstamp,
+            issuers: vec![*self.issuer],
+            signatures,
+        }
+    }
+}
+
+impl<'a> DocumentBuilder for IdentityDocumentBuilder<'a> {
+    type Document = IdentityDocument;
+    type PrivateKey = ed25519::PrivateKey;
+
+    fn build_with_signature(self, signatures: Vec<ed25519::Signature>) -> IdentityDocument {
+        self.build_with_text_and_sigs(self.generate_text(), signatures)
+    }
+
+    fn build_and_sign(self, private_keys: Vec<ed25519::PrivateKey>) -> IdentityDocument {
+        let (text, signatures) = self.build_signed_text(private_keys);
+        self.build_with_text_and_sigs(text, signatures)
+    }
+}
+
+impl<'a> TextDocumentBuilder for IdentityDocumentBuilder<'a> {
+    fn generate_text(&self) -> String {
+        format!(
+            "Version: 10
+Type: Identity
+Currency: {currency}
+Issuer: {issuer}
+UniqueID: {unique_id}
+Timestamp: {blockstamp}
+",
+            currency = self.currency,
+            issuer = self.issuer,
+            unique_id = self.unique_id,
+            blockstamp = self.blockstamp
+        )
+    }
+}
+
+/// Identity document parser
+#[derive(Debug, Clone, Copy)]
+pub struct IdentityDocumentParser;
+
+impl StandardTextDocumentParser for IdentityDocumentParser {
+    fn parse_standard(
+        doc: &str,
+        body: &str,
+        currency: &str,
+        signatures: Vec<ed25519::Signature>,
+    ) -> Result<V10Document, V10DocumentParsingError> {
+        if let Some(caps) = IDENTITY_REGEX.captures(body) {
+            let issuer = &caps["issuer"];
+            let uid = &caps["uid"];
+            let blockstamp = &caps["blockstamp"];
+
+            // Regex match so should not fail.
+            // TODO : Test it anyway
+            let issuer = ed25519::PublicKey::from_base58(issuer).unwrap();
+            let blockstamp = Blockstamp::from_string(blockstamp).unwrap();
+
+            Ok(V10Document::Identity(IdentityDocument {
+                text: doc.to_owned(),
+                currency: currency.to_owned(),
+                unique_id: uid.to_owned(),
+                blockstamp: blockstamp,
+                issuers: vec![issuer],
+                signatures,
+            }))
+        } else {
+            Err(V10DocumentParsingError::InvalidInnerFormat(
+                "Identity".to_string(),
+            ))
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use duniter_keys::{PrivateKey, PublicKey, Signature};
+    use blockchain::VerificationResult;
+
+    #[test]
+    fn generate_real_document() {
+        let pubkey = ed25519::PublicKey::from_base58(
+            "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV",
+        ).unwrap();
+
+        let prikey = ed25519::PrivateKey::from_base58(
+            "468Q1XtTq7h84NorZdWBZFJrGkB18CbmbHr9tkp9snt5G\
+             iERP7ySs3wM8myLccbAAGejgMRC9rqnXuW3iAfZACm7",
+        ).unwrap();
+
+        let sig = ed25519::Signature::from_base64(
+            "1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGM\
+             MmQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg==",
+        ).unwrap();
+
+        let block = Blockstamp::from_string(
+            "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
+        ).unwrap();
+
+        {
+            let doc = IdentityDocumentBuilder {
+                currency: "duniter_unit_test_currency",
+                unique_id: "tic",
+                blockstamp: &block,
+                issuer: &pubkey,
+            }.build_with_signature(vec![sig]);
+
+            assert_eq!(doc.verify_signatures(), VerificationResult::Valid());
+        }
+
+        {
+            let doc = IdentityDocumentBuilder {
+                currency: "duniter_unit_test_currency",
+                unique_id: "tic",
+                blockstamp: &block,
+                issuer: &pubkey,
+            }.build_and_sign(vec![prikey]);
+
+            assert_eq!(doc.verify_signatures(), VerificationResult::Valid());
+        }
+    }
+
+    #[test]
+    fn identity_standard_regex() {
+        assert!(IDENTITY_REGEX.is_match(
+            "Issuer: DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo
+UniqueID: toc
+Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
+"
+        ));
+    }
+
+    #[test]
+    fn parse_identity_document() {
+        let doc = "Version: 10
+Type: Identity
+Currency: duniter_unit_test_currency
+Issuer: DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo
+UniqueID: toc
+Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
+lcekuS0eP2dpFL99imJcwvDAwx49diiDMkG8Lj7FLkC/6IJ0tgNjUzCIZgMGi7bL5tODRiWi9B49UMXb8b3MAw==";
+
+        let body = "Issuer: DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo
+UniqueID: toc
+Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
+";
+
+        let currency = "duniter_unit_test_currency";
+
+        let signatures = vec![Signature::from_base64(
+"lcekuS0eP2dpFL99imJcwvDAwx49diiDMkG8Lj7FLkC/6IJ0tgNjUzCIZgMGi7bL5tODRiWi9B49UMXb8b3MAw=="
+        ).unwrap(),];
+
+        let _ = IdentityDocumentParser::parse_standard(doc, body, currency, signatures).unwrap();
+    }
+}
diff --git a/protocol/blockchain/v10/documents/mod.rs b/protocol/blockchain/v10/documents/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..681d2be6604efb3f050e8873bf7f1a2f9dfeb60a
--- /dev/null
+++ b/protocol/blockchain/v10/documents/mod.rs
@@ -0,0 +1,262 @@
+//  Copyright (C) 2018  The Duniter Project Developers.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+//! Provide wrappers around Duniter blockchain documents for protocol version 10.
+
+use duniter_keys::{Signature, ed25519};
+use regex::Regex;
+use blockchain::{Document, DocumentBuilder, DocumentParser};
+use blockchain::v10::documents::identity::IdentityDocumentParser;
+
+pub mod identity;
+
+pub use blockchain::v10::documents::identity::{IdentityDocument, IdentityDocumentBuilder};
+
+// Use of lazy_static so the regex is only compiled at first use.
+lazy_static! {
+    static ref DOCUMENT_REGEX: Regex = Regex::new(
+        "^(?P<doc>Version: 10\n\
+        Type: (?P<type>[[:alpha:]]+)\n\
+        Currency: (?P<currency>[[:alnum:] _-]+)\n\
+        (?P<body>(?:.*\n)+?))\
+        (?P<sigs>([[:alnum:]+/=]+\n)+)$"
+    ).unwrap();
+
+    static ref SIGNATURES_REGEX: Regex = Regex::new(
+        "[[:alnum:]+/=]+\n"
+    ).unwrap();
+}
+
+/// List of wrapped document types.
+///
+/// > TODO Add wrapped types in enum variants.
+#[derive(Debug, Clone)]
+pub enum V10Document {
+    /// Block document.
+    Block(),
+
+    /// Transaction document.
+    Transaction(),
+
+    /// Identity document.
+    Identity(IdentityDocument),
+
+    /// Membership document.
+    Membership(),
+
+    /// Certification document.
+    Certification(),
+
+    /// Revocation document.
+    Revocation(),
+}
+
+/// Trait for a V10 document.
+pub trait TextDocument
+    : Document<PublicKey = ed25519::PublicKey, CurrencyType = str> {
+    /// Return document as text.
+    fn as_text(&self) -> &str;
+
+    /// Return document as text with leading signatures.
+    fn as_text_with_signatures(&self) -> String {
+        let mut text = self.as_text().to_string();
+
+        for sig in self.signatures() {
+            text = format!("{}{}\n", text, sig.to_base64());
+        }
+
+        text
+    }
+}
+
+/// Trait for a V10 document builder.
+pub trait TextDocumentBuilder: DocumentBuilder {
+    /// Generate document text.
+    ///
+    /// - Don't contains leading signatures
+    /// - Contains line breaks on all line.
+    fn generate_text(&self) -> String;
+
+    /// Generate final document with signatures, and also return them in an array.
+    ///
+    /// Returns :
+    ///
+    /// - Text without signatures
+    /// - Signatures
+    fn build_signed_text(
+        &self,
+        private_keys: Vec<ed25519::PrivateKey>,
+    ) -> (String, Vec<ed25519::Signature>) {
+        use duniter_keys::PrivateKey;
+
+        let text = self.generate_text();
+
+        let signatures: Vec<_> = {
+            let text_bytes = text.as_bytes();
+            private_keys
+                .iter()
+                .map(|key| key.sign(text_bytes))
+                .collect()
+        };
+
+        (text, signatures)
+    }
+}
+
+/// List of possible errors while parsing.
+#[derive(Debug, Clone)]
+pub enum V10DocumentParsingError {
+    /// The given source don't have a valid document format.
+    InvalidWrapperFormat(),
+    /// The given source don't have a valid specific document format (document type).
+    InvalidInnerFormat(String),
+    /// Type fields contains an unknown document type.
+    UnknownDocumentType(String),
+}
+
+trait StandardTextDocumentParser {
+    fn parse_standard(
+        doc: &str,
+        body: &str,
+        currency: &str,
+        signatures: Vec<ed25519::Signature>,
+    ) -> Result<V10Document, V10DocumentParsingError>;
+}
+
+trait CompactTextDocumentParser<D: TextDocument> {
+    fn parse_compact(
+        doc: &str,
+        body: &str,
+        currency: &str,
+        signatures: Vec<ed25519::Signature>,
+    ) -> Result<D, V10DocumentParsingError>;
+}
+
+/// A V10 document parser.
+#[derive(Debug, Clone, Copy)]
+pub struct V10DocumentParser;
+
+impl<'a> DocumentParser<&'a str, V10Document, V10DocumentParsingError> for V10DocumentParser {
+    fn parse(source: &'a str) -> Result<V10Document, V10DocumentParsingError> {
+        if let Some(caps) = DOCUMENT_REGEX.captures(source) {
+            let doctype = &caps["type"];
+            let doc = &caps["doc"];
+            let currency = &caps["currency"];
+            let body = &caps["body"];
+            let sigs = SIGNATURES_REGEX
+                .captures_iter(&caps["sigs"])
+                .map(|capture| ed25519::Signature::from_base64(&capture[0]).unwrap())
+                .collect::<Vec<_>>();
+
+            // TODO : Improve error handling of Signature::from_base64 failure
+
+            match doctype {
+                "Identity" => IdentityDocumentParser::parse_standard(doc, body, currency, sigs),
+                _ => Err(V10DocumentParsingError::UnknownDocumentType(
+                    doctype.to_string(),
+                )),
+            }
+        } else {
+            Err(V10DocumentParsingError::InvalidWrapperFormat())
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn document_regex() {
+        assert!(DOCUMENT_REGEX.is_match(
+            "Version: 10
+Type: Transaction
+Currency: beta_brousouf
+Blockstamp: 204-00003E2B8A35370BA5A7064598F628A62D4E9EC1936BE8651CE9A85F2E06981B
+Locktime: 0
+Issuers:
+HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY
+CYYjHsNyg3HMRMpTHqCJAN9McjH5BwFLmDKGV3PmCuKp
+9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB
+Inputs:
+40:2:T:6991C993631BED4733972ED7538E41CCC33660F554E3C51963E2A0AC4D6453D3:2
+70:2:T:3A09A20E9014110FD224889F13357BAB4EC78A72F95CA03394D8CCA2936A7435:8
+20:2:D:HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY:46
+70:2:T:A0D9B4CDC113ECE1145C5525873821398890AE842F4B318BD076095A23E70956:3
+20:2:T:67F2045B5318777CC52CD38B424F3E40DDA823FA0364625F124BABE0030E7B5B:5
+15:2:D:9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB:46
+Unlocks:
+0:SIG(0)
+1:XHX(7665798292)
+2:SIG(0)
+3:SIG(0) SIG(2)
+4:SIG(0) SIG(1) SIG(2)
+5:SIG(2)
+Outputs:
+120:2:SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g)
+146:2:SIG(DSz4rgncXCytsUMW2JU2yhLquZECD2XpEkpP9gG5HyAx)
+49:2:(SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i)\
+ || XHX(3EB4702F2AC2FD3FA4FDC46A4FC05AE8CDEE1A85))
+Comment: -----@@@----- (why not this comment?)
+42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r
+2D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX
+2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk
+"
+        ));
+
+        assert!(DOCUMENT_REGEX.is_match(
+            "Version: 10
+Type: Certification
+Currency: beta_brousouf
+Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV
+IdtyIssuer: HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd
+IdtyUniqueID: lolcat
+IdtyTimestamp: 32-DB30D958EE5CB75186972286ED3F4686B8A1C2CD
+IdtySignature: J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUb\
+GpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci
+CertTimestamp: 36-1076F10A7397715D2BEE82579861999EA1F274AC
+SoKwoa8PFfCDJWZ6dNCv7XstezHcc2BbKiJgVDXv82R5zYR83nis9dShLgWJ5w48noVUHimdngzYQneNYSMV3rk
+"
+        ));
+    }
+
+    #[test]
+    fn signatures_regex() {
+        assert_eq!(
+            SIGNATURES_REGEX
+                .captures_iter(
+                    "
+42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r
+2D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX
+2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk
+"
+                )
+                .count(),
+            3
+        );
+
+        assert_eq!(
+            SIGNATURES_REGEX
+                .captures_iter(
+                    "
+42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r
+2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk
+"
+                )
+                .count(),
+            2
+        );
+    }
+}
diff --git a/protocol/blockchain/v10/mod.rs b/protocol/blockchain/v10/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9b3d159d6d576877e01a5822ed3e720793bb3775
--- /dev/null
+++ b/protocol/blockchain/v10/mod.rs
@@ -0,0 +1,18 @@
+//  Copyright (C) 2018  The Duniter Project Developers.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+//! Provide wrappers around Duniter V10 documents and events.
+
+pub mod documents;
diff --git a/protocol/lib.rs b/protocol/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..84908914a5384bc2eed4cb7a35b5510681199504
--- /dev/null
+++ b/protocol/lib.rs
@@ -0,0 +1,198 @@
+//  Copyright (C) 2018  The Duniter Project Developers.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+//! Implements the Duniter Protocol.
+
+#![deny(missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts,
+        trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces,
+        unused_qualifications)]
+
+extern crate base58;
+extern crate base64;
+extern crate crypto;
+extern crate duniter_keys;
+#[macro_use]
+extern crate lazy_static;
+extern crate linked_hash_map;
+extern crate regex;
+
+use std::fmt::{Debug, Display, Error, Formatter};
+
+use duniter_keys::BaseConvertionError;
+
+pub mod blockchain;
+
+/// A block Id.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub struct BlockId(pub u32);
+
+impl Display for BlockId {
+    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+        write!(f, "{}", self.0)
+    }
+}
+
+/// A hash wrapper.
+///
+/// A hash is often provided as string composed of 64 hexadecimal character (0 to 9 then A to F).
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub struct Hash(pub [u8; 32]);
+
+impl Display for Hash {
+    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+        write!(f, "{}", self.to_hex())
+    }
+}
+
+impl Debug for Hash {
+    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+        write!(f, "Hash({})", self)
+    }
+}
+
+impl Hash {
+    /// Convert a `Hash` to an hex string.
+    pub fn to_hex(&self) -> String {
+        let strings: Vec<String> = self.0.iter().map(|b| format!("{:02X}", b)).collect();
+
+        strings.join("")
+    }
+
+    /// Convert a hex string in a `Hash`.
+    ///
+    /// The hex string must only contains hex characters
+    /// and produce a 32 bytes value.
+    pub fn from_hex(text: &str) -> Result<Hash, BaseConvertionError> {
+        if text.len() != 64 {
+            Err(BaseConvertionError::InvalidKeyLendth(text.len(), 64))
+        } else {
+            let mut hash = Hash([0u8; 32]);
+
+            let chars: Vec<char> = text.chars().collect();
+
+            for i in 0..64 {
+                if i % 2 != 0 {
+                    continue;
+                }
+
+                let byte1 = chars[i].to_digit(16);
+                let byte2 = chars[i + 1].to_digit(16);
+
+                if byte1.is_none() {
+                    return Err(BaseConvertionError::InvalidCharacter(chars[i], i));
+                } else if byte2.is_none() {
+                    return Err(BaseConvertionError::InvalidCharacter(chars[i + 1], i + 1));
+                }
+
+                let byte1 = byte1.unwrap() as u8;
+                let byte2 = byte2.unwrap() as u8;
+
+                let byte = (byte1 << 4) | byte2;
+                hash.0[i / 2] = byte;
+            }
+
+            Ok(hash)
+        }
+    }
+}
+
+/// Wrapper of a block hash.
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub struct BlockHash(Hash);
+
+impl Display for BlockHash {
+    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+        write!(f, "{}", self.0.to_hex())
+    }
+}
+
+impl Debug for BlockHash {
+    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+        write!(f, "BlockHash({})", self)
+    }
+}
+
+/// Type of errors for [`BlockUId`] parsing.
+///
+/// [`BlockUId`]: struct.BlockUId.html
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum BlockUIdParseError {
+    /// Given string have invalid format
+    InvalidFormat(),
+    /// [`BlockId`](struct.BlockHash.html) part is not a valid number.
+    InvalidBlockId(),
+    /// [`BlockHash`](struct.BlockHash.html) part is not a valid hex number.
+    InvalidBlockHash(),
+}
+
+/// A blockstamp (Unique ID).
+///
+/// It's composed of the [`BlockId`] and
+/// the [`BlockHash`] of the block.
+///
+/// Thanks to blockchain immutability and frequent block production, it can
+/// be used to date information.
+///
+/// [`BlockId`]: struct.BlockId.html
+/// [`BlockHash`]: struct.BlockHash.html
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub struct Blockstamp {
+    /// Block Id.
+    pub id: BlockId,
+    /// Block hash.
+    pub hash: BlockHash,
+}
+
+impl Display for Blockstamp {
+    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+        write!(f, "{}-{}", self.id, self.hash)
+    }
+}
+
+impl Debug for Blockstamp {
+    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+        write!(f, "BlockUId({})", self)
+    }
+}
+
+impl Blockstamp {
+    /// Create a `BlockUId` from a text.
+    pub fn from_string(src: &str) -> Result<Blockstamp, BlockUIdParseError> {
+        let mut split = src.split('-');
+
+        if split.clone().count() != 2 {
+            Err(BlockUIdParseError::InvalidFormat())
+        } else {
+            let id = split.next().unwrap().parse::<u32>();
+            let hash = Hash::from_hex(split.next().unwrap());
+
+            if id.is_err() {
+                Err(BlockUIdParseError::InvalidBlockId())
+            } else if hash.is_err() {
+                Err(BlockUIdParseError::InvalidBlockHash())
+            } else {
+                Ok(Blockstamp {
+                    id: BlockId(id.unwrap()),
+                    hash: BlockHash(hash.unwrap()),
+                })
+            }
+        }
+    }
+
+    /// Convert a `BlockUId` to its text format.
+    pub fn to_string(&self) -> String {
+        format!("{}", self)
+    }
+}
diff --git a/wotb/README.md b/wotb/README.md
index fc6bf882c7373eb98ed7a1a27c06a6b3c5215b49..32266674b05e3aa88c51a4c7a199646e2c7428db 100644
--- a/wotb/README.md
+++ b/wotb/README.md
@@ -1,11 +1,10 @@
-# Introduction
+# wotb
 
 `wotb` is a crate making "Web of Trust" computations for
 the [Duniter] project.
 
 [Duniter]: https://duniter.org/en/
 
-# How to use it ?
+## How to use it
 
-You can add `duniter-wotb` as a `cargo` dependency in your Rust project.
-To use it in JavaScript, see `duniter-wotb-js` package. (not done yet)
\ No newline at end of file
+You can add `duniter-wotb` as a `cargo` dependency in your Rust project.
\ No newline at end of file