diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9fc197f54e5498b4982b16af19dda5420c45c524..63f8f9ba4809fade3cbfff1bfad07f6a65cae29f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -24,7 +24,7 @@ workflow:
       - server.ts
 
 .env: &env
-  image: registry.duniter.org/docker/duniter-ci:v0.0.4
+  image: registry.duniter.org/docker/duniter-ci:v0.2.0
   tags:
     - redshift
   before_script:
diff --git a/Cargo.lock b/Cargo.lock
index 11e4923e074358799b14a01a8f966ff63a12cb7f..335797456d9a61fea4f92e07c37f298968ed9c84 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,5 +1,15 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+[[package]]
+name = "Inflector"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
+dependencies = [
+ "lazy_static",
+ "regex",
+]
+
 [[package]]
 name = "addr2line"
 version = "0.13.0"
@@ -17,9 +27,9 @@ checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
 
 [[package]]
 name = "aho-corasick"
-version = "0.7.10"
+version = "0.7.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
+checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86"
 dependencies = [
  "memchr",
 ]
@@ -39,6 +49,12 @@ version = "1.0.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b"
 
+[[package]]
+name = "arc-swap"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034"
+
 [[package]]
 name = "arrayref"
 version = "0.3.6"
@@ -51,6 +67,123 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
 
+[[package]]
+name = "async-attributes"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efd3d156917d94862e779f356c5acae312b08fd3121e792c857d7928c8088423"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21279cfaa4f47df10b1816007e738ca3747ef2ee53ffc51cdbf57a8bb266fee3"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d373d78ded7d0b3fa8039375718cde0aace493f2e34fb60f51cbf567562ca801"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "once_cell",
+ "vec-arena",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fefeb39da249f4c33af940b779a56723ce45809ef5c54dad84bb538d4ffb6d9e"
+dependencies = [
+ "async-executor",
+ "async-io",
+ "futures-lite",
+ "num_cpus",
+ "once_cell",
+]
+
+[[package]]
+name = "async-io"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a027f4b662e59d7070791e33baafd36affe67388965701b50039db5ebb240d2"
+dependencies = [
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "libc",
+ "log",
+ "nb-connect",
+ "once_cell",
+ "parking",
+ "polling",
+ "vec-arena",
+ "waker-fn",
+ "winapi",
+]
+
+[[package]]
+name = "async-mutex"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-std"
+version = "1.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9fa76751505e8df1c7a77762f60486f60c71bbd9b8557f4da6ad47d083732ed"
+dependencies = [
+ "async-attributes",
+ "async-global-executor",
+ "async-io",
+ "async-mutex",
+ "blocking",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "num_cpus",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-task"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ab27c1aa62945039e44edaeee1dc23c74cc0c303dd5fe0fb462a184f1c3a518"
+
+[[package]]
+name = "atomic-waker"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a"
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -64,15 +197,15 @@ dependencies = [
 
 [[package]]
 name = "autocfg"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
 
 [[package]]
 name = "backtrace"
-version = "0.3.50"
+version = "0.3.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293"
+checksum = "ec1931848a574faa8f7c71a12ea00453ff5effbb5f51afe7f77d7a48cace6ac1"
 dependencies = [
  "addr2line",
  "cfg-if",
@@ -102,9 +235,9 @@ checksum = "474a626a67200bd107d44179bb3d4fc61891172d11696609264589be6a0e6a43"
 
 [[package]]
 name = "bincode"
-version = "1.2.1"
+version = "1.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf"
+checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d"
 dependencies = [
  "byteorder",
  "serde",
@@ -148,17 +281,43 @@ dependencies = [
  "byte-tools",
 ]
 
+[[package]]
+name = "blocking"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2640778f8053e72c11f621b0a5175a0560a269282aa98ed85107773ab8e2a556"
+dependencies = [
+ "async-channel",
+ "atomic-waker",
+ "fastrand",
+ "futures-lite",
+ "once_cell",
+ "waker-fn",
+]
+
 [[package]]
 name = "bs58"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb"
 
+[[package]]
+name = "bstr"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931"
+dependencies = [
+ "lazy_static",
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
 [[package]]
 name = "bumpalo"
-version = "3.2.1"
+version = "3.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12ae9db68ad7fac5fe51304d20f016c911539251075a214f8e663babefa35187"
+checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
 
 [[package]]
 name = "byte-tools"
@@ -172,11 +331,26 @@ version = "1.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
 
+[[package]]
+name = "cache-padded"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba"
+
+[[package]]
+name = "cast"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0"
+dependencies = [
+ "rustc_version",
+]
+
 [[package]]
 name = "cc"
-version = "1.0.52"
+version = "1.0.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d87b23d6a92cd03af510a5ade527033f6aa6fa92161e2d5863a907d4c5e31d"
+checksum = "ef611cc68ff783f18535d77ddd080185275713d852c4f5cbb6122c462a7a825c"
 
 [[package]]
 name = "cfg-if"
@@ -221,6 +395,53 @@ dependencies = [
  "vec_map",
 ]
 
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "cloudabi"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e56268c17a6248366d66d4a47a3381369d068cce8409bb1716ed77ea32163bb"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "comfy-table"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f97a418f1dee79b100875499e272ea81882c1b931a9c8432e165b595b461752"
+dependencies = [
+ "crossterm",
+ "strum",
+ "strum_macros",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3"
+dependencies = [
+ "cache-padded",
+]
+
 [[package]]
 name = "constant_time_eq"
 version = "0.1.5"
@@ -236,6 +457,52 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "criterion"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70daa7ceec6cf143990669a04c7df13391d55fb27bd4079d252fca774ba244d8"
+dependencies = [
+ "atty",
+ "cast",
+ "clap",
+ "criterion-plot",
+ "csv",
+ "itertools",
+ "lazy_static",
+ "num-traits",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_cbor",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d"
+dependencies = [
+ "cast",
+ "itertools",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
+dependencies = [
+ "crossbeam-utils",
+ "maybe-uninit",
+]
+
 [[package]]
 name = "crossbeam-deque"
 version = "0.7.3"
@@ -263,24 +530,39 @@ dependencies = [
 ]
 
 [[package]]
-name = "crossbeam-queue"
-version = "0.2.1"
+name = "crossbeam-utils"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
 dependencies = [
+ "autocfg",
  "cfg-if",
- "crossbeam-utils",
+ "lazy_static",
 ]
 
 [[package]]
-name = "crossbeam-utils"
-version = "0.7.2"
+name = "crossterm"
+version = "0.17.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
+checksum = "6f4919d60f26ae233e14233cc39746c8c8bb8cd7b05840ace83604917b51b6c7"
 dependencies = [
- "autocfg",
- "cfg-if",
+ "bitflags",
+ "crossterm_winapi",
  "lazy_static",
+ "libc",
+ "mio",
+ "parking_lot 0.10.2",
+ "signal-hook",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "057b7146d02fb50175fd7dbe5158f6097f33d02831f43b4ee8ae4ddf67b68f5c"
+dependencies = [
+ "winapi",
 ]
 
 [[package]]
@@ -295,6 +577,28 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "697c714f50560202b1f4e2e09cd50a421881c83e9025db75d15f276616f04f40"
 
+[[package]]
+name = "csv"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279"
+dependencies = [
+ "bstr",
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "ctrlc"
 version = "3.1.6"
@@ -316,6 +620,12 @@ dependencies = [
  "snafu",
 ]
 
+[[package]]
+name = "difference"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
+
 [[package]]
 name = "digest"
 version = "0.8.1"
@@ -352,16 +662,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
 
 [[package]]
-name = "dubp-common"
-version = "0.2.0"
+name = "downcast"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b41d722bf752e5f87d07685aba6df25f69881f9aafa7060fae459e2948d8080"
-dependencies = [
- "dup-crypto 0.18.0",
- "serde",
- "serde_json",
- "thiserror",
-]
+checksum = "4bb454f0228b18c7f4c3b0ebbee346ed9c52e7443b0999cd543ff3571205701d"
 
 [[package]]
 name = "dubp-common"
@@ -369,7 +673,7 @@ version = "0.25.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "00588975a1ce2511ba0072563dc69e79534c28846ad68701699ae112d01975ff"
 dependencies = [
- "dup-crypto 0.25.2",
+ "dup-crypto",
  "serde",
  "serde_json",
  "thiserror",
@@ -409,7 +713,7 @@ version = "0.25.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "45d87c99e3e231625ba35c0f0ad4f8c6186f91ae5f0504fa752b47e46762dbc0"
 dependencies = [
- "dubp-common 0.25.2",
+ "dubp-common",
  "serde",
  "smallvec",
  "thiserror",
@@ -420,12 +724,47 @@ name = "dubp-wot"
 version = "0.11.0"
 dependencies = [
  "bincode",
- "dubp-common 0.2.0",
+ "dubp-common",
  "log",
  "rayon",
  "serde",
 ]
 
+[[package]]
+name = "duniter-dbex"
+version = "0.1.0"
+dependencies = [
+ "arrayvec",
+ "comfy-table",
+ "dirs",
+ "dubp-common",
+ "duniter-dbs",
+ "rayon",
+ "serde",
+ "serde_json",
+ "structopt",
+ "unwrap",
+]
+
+[[package]]
+name = "duniter-dbs"
+version = "0.1.0"
+dependencies = [
+ "arrayvec",
+ "chrono",
+ "dubp-common",
+ "kv_typed",
+ "log",
+ "mockall",
+ "once_cell",
+ "serde",
+ "serde_json",
+ "smallvec",
+ "tempdir",
+ "thiserror",
+ "unwrap",
+]
+
 [[package]]
 name = "duniter-launcher"
 version = "1.8.1"
@@ -447,7 +786,7 @@ version = "1.8.1"
 dependencies = [
  "bincode",
  "bs58",
- "dubp-common 0.25.2",
+ "dubp-common",
  "dubp-documents",
  "dubp-documents-parser",
  "dubp-wallet",
@@ -461,22 +800,6 @@ dependencies = [
  "unwrap",
 ]
 
-[[package]]
-name = "dup-crypto"
-version = "0.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7315898eda74bf7c9e825f9afae1c9f16debb401fdb7d658fb851fb00a6260c"
-dependencies = [
- "base64 0.11.0",
- "bs58",
- "byteorder",
- "ring",
- "serde",
- "thiserror",
- "unwrap",
- "zeroize",
-]
-
 [[package]]
 name = "dup-crypto"
 version = "0.25.2"
@@ -496,9 +819,9 @@ dependencies = [
 
 [[package]]
 name = "either"
-version = "1.5.3"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
 
 [[package]]
 name = "envmnt"
@@ -520,17 +843,32 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "event-listener"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59"
+
 [[package]]
 name = "fake-simd"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
 
+[[package]]
+name = "fastrand"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3"
+dependencies = [
+ "instant",
+]
+
 [[package]]
 name = "flate2"
-version = "1.0.17"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "766d0e77a2c1502169d4a93ff3b8c15a71fd946cd0126309752104e5f3c46d94"
+checksum = "da80be589a72651dcda34d8b35bcdc9b7254ad06325611074d9cc0fbb19f60ee"
 dependencies = [
  "cfg-if",
  "crc32fast",
@@ -551,12 +889,88 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "float-cmp"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "fragile"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69a039c3498dc930fe810151a34ba0c1c70b02b8625035592e74432f678591f2"
+
+[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
 [[package]]
 name = "fsio"
 version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c1fd087255f739f4f1aeea69f11b72f8080e9c2e7645cd06955dad4a178a49e3"
 
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399"
+
+[[package]]
+name = "futures-io"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789"
+
+[[package]]
+name = "futures-lite"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b2ec5fce115ef4d4cc77f46025232fba6a2f84381ff42337794147050aae971"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.12.3"
@@ -599,6 +1013,25 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 
+[[package]]
+name = "gloo-timers"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "half"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177"
+
 [[package]]
 name = "hashbrown"
 version = "0.9.1"
@@ -616,9 +1049,9 @@ dependencies = [
 
 [[package]]
 name = "hermit-abi"
-version = "0.1.12"
+version = "0.1.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4"
+checksum = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151"
 dependencies = [
  "libc",
 ]
@@ -633,6 +1066,24 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "instant"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63312a18f7ea8760cdd0a7c5aac1a619752a246b833545e3e36d1f81f7cd9e66"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itertools"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "0.4.6"
@@ -641,9 +1092,9 @@ checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
 
 [[package]]
 name = "js-sys"
-version = "0.3.37"
+version = "0.3.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a27d435371a2fa5b6d2b028a74bbdb1234f308da363226a2854ca3ff8ba7055"
+checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -660,17 +1111,97 @@ dependencies = [
  "unwrap",
 ]
 
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "kv_typed"
+version = "0.1.0"
+dependencies = [
+ "async-channel",
+ "async-std",
+ "criterion",
+ "crossbeam-channel",
+ "kv_typed_code_gen",
+ "leveldb_minimal",
+ "maybe-async",
+ "mockall",
+ "parking_lot 0.11.0",
+ "rayon",
+ "regex",
+ "serde_json",
+ "sled",
+ "smallvec",
+ "thiserror",
+ "unwrap",
+]
+
+[[package]]
+name = "kv_typed_code_gen"
+version = "0.1.0"
+dependencies = [
+ "Inflector",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
+[[package]]
+name = "leveldb-sys"
+version = "2.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76c44b9b785ca705d58190ebd432a4e7edb900eadf236ff966d7d1307e482e87"
+dependencies = [
+ "cmake",
+ "libc",
+ "num_cpus",
+]
+
+[[package]]
+name = "leveldb_minimal"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b4cb22d7d3cce486fc6e2ef7cd25d38e118f525f79c4a946ac48d89c5d16b1"
+dependencies = [
+ "leveldb-sys",
+ "libc",
+]
+
 [[package]]
 name = "libc"
-version = "0.2.69"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa7087f49d294270db4e1928fc110c976cd4b9e5a16348e0a1df09afa99e6c98"
+
+[[package]]
+name = "lock_api"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005"
+checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c"
+dependencies = [
+ "scopeguard",
+]
 
 [[package]]
 name = "log"
@@ -693,6 +1224,17 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
 
+[[package]]
+name = "maybe-async"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51772c1b1c6c129eeeaf8ffe7581906aa14c8cca116b40d41ce1285f5a4fdfa0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "maybe-uninit"
 version = "2.0.0"
@@ -707,20 +1249,81 @@ checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
 
 [[package]]
 name = "memoffset"
-version = "0.5.4"
+version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8"
+checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa"
 dependencies = [
  "autocfg",
 ]
 
 [[package]]
 name = "miniz_oxide"
-version = "0.4.0"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f"
+checksum = "c60c0dfe32c10b43a144bad8fc83538c52f58302c92300ea7ec7bf7b38d5a7b9"
 dependencies = [
  "adler",
+ "autocfg",
+]
+
+[[package]]
+name = "mio"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e53a6ea5f38c0a48ca42159868c6d8e1bd56c0451238856cc08d58563643bdc3"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07b88fb9795d4d36d62a012dfbf49a8f5cf12751f36d31a9dbe66d528e58979e"
+dependencies = [
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "mockall"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ecfc6340c5b98a9a270b56e5f43353d87ebb18d9458a9301344bc79317c563"
+dependencies = [
+ "cfg-if",
+ "downcast",
+ "fragile",
+ "lazy_static",
+ "mockall_derive",
+ "predicates",
+ "predicates-tree",
+]
+
+[[package]]
+name = "mockall_derive"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b873f753808fe0c3827ce76edb3ace27804966dfde3043adfac1c24d0a2559df"
+dependencies = [
+ "cfg-if",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "nb-connect"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8123a81538e457d44b933a02faf885d3fe8408806b23fa700e8f01c6c3a98998"
+dependencies = [
+ "libc",
+ "winapi",
 ]
 
 [[package]]
@@ -790,11 +1393,26 @@ version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363"
 dependencies = [
- "bitflags",
- "cc",
- "cfg-if",
- "libc",
- "void",
+ "bitflags",
+ "cc",
+ "cfg-if",
+ "libc",
+ "void",
+]
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "ntapi"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a31937dea023539c72ddae0e3571deadc1414b300483fa7aaec176168cfa9d2"
+dependencies = [
+ "winapi",
 ]
 
 [[package]]
@@ -883,12 +1501,74 @@ version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad"
 
+[[package]]
+name = "oorandom"
+version = "11.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a170cebd8021a008ea92e4db85a72f80b35df514ec664b296fdcbb654eac0b2c"
+
 [[package]]
 name = "opaque-debug"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
 
+[[package]]
+name = "parking"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
+
+[[package]]
+name = "parking_lot"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e"
+dependencies = [
+ "lock_api 0.3.4",
+ "parking_lot_core 0.7.2",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733"
+dependencies = [
+ "instant",
+ "lock_api 0.4.1",
+ "parking_lot_core 0.8.0",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3"
+dependencies = [
+ "cfg-if",
+ "cloudabi 0.0.3",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b"
+dependencies = [
+ "cfg-if",
+ "cloudabi 0.1.0",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
 [[package]]
 name = "pest"
 version = "2.1.3"
@@ -932,6 +1612,72 @@ dependencies = [
  "sha-1",
 ]
 
+[[package]]
+name = "pin-project-lite"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e555d9e657502182ac97b539fb3dae8b79cda19e3e4f8ffb5e8de4f18df93c95"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "plotters"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d1685fbe7beba33de0330629da9d955ac75bd54f33d7b79f9a895590124f6bb"
+dependencies = [
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "polling"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7215a098a80ab8ebd6349db593dc5faf741781bad0c4b7c5701fea6af548d52c"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "log",
+ "wepoll-sys",
+ "winapi",
+]
+
+[[package]]
+name = "predicates"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bfead12e90dccead362d62bb2c90a5f6fc4584963645bc7f71a735e0b0735a"
+dependencies = [
+ "difference",
+ "float-cmp",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124"
+dependencies = [
+ "predicates-core",
+ "treeline",
+]
+
 [[package]]
 name = "proc-macro-error"
 version = "1.0.4"
@@ -958,28 +1704,57 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.19"
+version = "1.0.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12"
+checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
 dependencies = [
  "unicode-xid",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.3"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
+checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
 dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+dependencies = [
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "rdrand",
+ "winapi",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
 [[package]]
 name = "rayon"
-version = "1.3.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098"
+checksum = "dcf6960dc9a5b4ee8d3e4c5787b4a112a8818e0290a42ff664ad60692fdf2032"
 dependencies = [
+ "autocfg",
  "crossbeam-deque",
  "either",
  "rayon-core",
@@ -987,17 +1762,26 @@ dependencies = [
 
 [[package]]
 name = "rayon-core"
-version = "1.7.0"
+version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9"
+checksum = "e8c4fec834fb6e6d2dd5eece3c7b432a52f0ba887cf40e595190c4107edc08bf"
 dependencies = [
+ "crossbeam-channel",
  "crossbeam-deque",
- "crossbeam-queue",
  "crossbeam-utils",
  "lazy_static",
  "num_cpus",
 ]
 
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.1.57"
@@ -1017,9 +1801,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.3.7"
+version = "1.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692"
+checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1027,11 +1811,29 @@ dependencies = [
  "thread_local",
 ]
 
+[[package]]
+name = "regex-automata"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4"
+dependencies = [
+ "byteorder",
+]
+
 [[package]]
 name = "regex-syntax"
-version = "0.6.17"
+version = "0.6.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae"
+checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
 
 [[package]]
 name = "ring"
@@ -1066,6 +1868,15 @@ version = "0.1.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
 
+[[package]]
+name = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
 [[package]]
 name = "rusty-hook"
 version = "0.11.2"
@@ -1084,6 +1895,15 @@ version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
 
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
 [[package]]
 name = "scopeguard"
 version = "1.1.0"
@@ -1107,18 +1927,28 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 
 [[package]]
 name = "serde"
-version = "1.0.115"
+version = "1.0.116"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5"
+checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5"
 dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "serde_cbor"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622"
+dependencies = [
+ "half",
+ "serde",
+]
+
 [[package]]
 name = "serde_derive"
-version = "1.0.115"
+version = "1.0.116"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48"
+checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1127,9 +1957,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.57"
+version = "1.0.58"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c"
+checksum = "a230ea9107ca2220eea9d46de97eddcb04cd00e92d13dda78e478dd33fa82bd4"
 dependencies = [
  "itoa",
  "ryu",
@@ -1148,6 +1978,49 @@ dependencies = [
  "opaque-debug",
 ]
 
+[[package]]
+name = "signal-hook"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e12110bc539e657a646068aaf5eb5b63af9d0c1f7b29c97113fad80e15f035"
+dependencies = [
+ "arc-swap",
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
+
+[[package]]
+name = "sled"
+version = "0.34.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f72c064e63fbca3138ad07f3588c58093f1684f3a99f60dcfa6d46b87e60fde7"
+dependencies = [
+ "crc32fast",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "fs2",
+ "fxhash",
+ "libc",
+ "log",
+ "parking_lot 0.11.0",
+]
+
 [[package]]
 name = "smallvec"
 version = "1.4.2"
@@ -1178,6 +2051,18 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "socket2"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1fa70dc5c8104ec096f4fe7ede7a221d35ae13dcd19ba1ad9a81d2cab9a1c44"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "winapi",
+]
+
 [[package]]
 name = "spin"
 version = "0.5.2"
@@ -1214,11 +2099,29 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "strum"
+version = "0.19.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b89a286a7e3b5720b9a477b23253bc50debac207c8d21505f8e70b36792f11b5"
+
+[[package]]
+name = "strum_macros"
+version = "0.19.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e61bb0be289045cb80bfce000512e32d09f8337e54c186725da381377ad1f8d5"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "syn"
-version = "1.0.39"
+version = "1.0.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9"
+checksum = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1227,9 +2130,9 @@ dependencies = [
 
 [[package]]
 name = "synstructure"
-version = "0.12.3"
+version = "0.12.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
+checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1237,6 +2140,16 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "tempdir"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
+dependencies = [
+ "rand",
+ "remove_dir_all",
+]
+
 [[package]]
 name = "textwrap"
 version = "0.11.0"
@@ -1286,6 +2199,16 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "tinytemplate"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d3dc76004a03cec1c5932bca4cdc2e39aaa798e3f82363dd94f9adf6098c12f"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "toml"
 version = "0.5.6"
@@ -1295,6 +2218,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "treeline"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
+
 [[package]]
 name = "typenum"
 version = "1.12.0"
@@ -1321,9 +2250,9 @@ checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
 
 [[package]]
 name = "unicode-xid"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
 
 [[package]]
 name = "untrusted"
@@ -1337,6 +2266,12 @@ version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7e33648dd74328e622c7be51f3b40a303c63f93e6fa5f08778b6203a4c25c20f"
 
+[[package]]
+name = "vec-arena"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eafc1b9b2dfc6f5529177b62cf806484db55b32dc7c9658a118e11bbeb33061d"
+
 [[package]]
 name = "vec_map"
 version = "0.8.2"
@@ -1355,6 +2290,23 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
 
+[[package]]
+name = "waker-fn"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+
+[[package]]
+name = "walkdir"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
 [[package]]
 name = "wasi"
 version = "0.9.0+wasi-snapshot-preview1"
@@ -1369,9 +2321,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.60"
+version = "0.2.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cc57ce05287f8376e998cbddfb4c8cb43b84a7ec55cf4551d7c00eef317a47f"
+checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42"
 dependencies = [
  "cfg-if",
  "wasm-bindgen-macro",
@@ -1379,9 +2331,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.60"
+version = "0.2.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d967d37bf6c16cca2973ca3af071d0a2523392e4a594548155d89a678f4237cd"
+checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68"
 dependencies = [
  "bumpalo",
  "lazy_static",
@@ -1392,11 +2344,23 @@ dependencies = [
  "wasm-bindgen-shared",
 ]
 
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7866cab0aa01de1edf8b5d7936938a7e397ee50ce24119aef3e1eaa3b6171da"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.60"
+version = "0.2.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8bd151b63e1ea881bb742cd20e1d6127cef28399558f3b5d415289bc41eee3a4"
+checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -1404,9 +2368,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.60"
+version = "0.2.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d68a5b36eef1be7868f668632863292e37739656a80fc4b9acec7b0bd35a4931"
+checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1417,25 +2381,34 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.60"
+version = "0.2.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daf76fe7d25ac79748a37538b7daeed1c7a6867c92d3245c12c6222e4a20d639"
+checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307"
 
 [[package]]
 name = "web-sys"
-version = "0.3.37"
+version = "0.3.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d6f51648d8c56c366144378a33290049eafdd784071077f6fe37dae64c1c4cb"
+checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "wepoll-sys"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142bc2cba3fe88be1a8fcb55c727fa4cd5b0cf2d7438722792e22f26f04bc1e0"
+dependencies = [
+ "cc",
+]
+
 [[package]]
 name = "winapi"
-version = "0.3.8"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
 dependencies = [
  "winapi-i686-pc-windows-gnu",
  "winapi-x86_64-pc-windows-gnu",
@@ -1447,6 +2420,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
@@ -1463,18 +2445,18 @@ dependencies = [
 
 [[package]]
 name = "zeroize"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8"
+checksum = "05f33972566adbd2d3588b0491eb94b98b43695c4ef897903470ede4f3f5a28a"
 dependencies = [
  "zeroize_derive",
 ]
 
 [[package]]
 name = "zeroize_derive"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2"
+checksum = "c3f369ddb18862aba61aa49bf31e74d29f0f162dec753063200e1dc084345d16"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/Cargo.toml b/Cargo.toml
index e146e0790cb5cf9344057486f718130c3496e027..90a051b9c4c06f1dd6f307707e18fe04c1974128 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,8 +29,12 @@ rusty-hook = "0.11.2"
 [workspace]
 members = [
     "neon/native",
+    "rust-bins/duniter-dbex",
     "rust-bins/xtask",
-    "rust-libs/dubp-wot"
+    "rust-libs/dubp-wot",
+    "rust-libs/duniter-dbs",
+    "rust-libs/tools/kv_typed",
+    "rust-libs/tools/kv_typed_code_gen"
 ]
 
 [patch.crates-io]
@@ -38,3 +42,4 @@ members = [
 #dubp-documents  = { path = "../dubp-rs-libs/documents" }
 #dubp-documents-parser = { path = "../dubp-rs-libs/documents-parser" }
 #dubp-wallet  = { path = "../dubp-rs-libs/wallet" }
+#leveldb_minimal = { path = "../../../../rust/leveldb_minimal" }
diff --git a/rust-bins/duniter-dbex/Cargo.toml b/rust-bins/duniter-dbex/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..2cb639e6e9a09eb1132b03bd608625cc96d8be98
--- /dev/null
+++ b/rust-bins/duniter-dbex/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "duniter-dbex"
+version = "0.1.0"
+authors = ["elois <elois@duniter.org>"]
+description = "Duniter blockchain DB"
+repository = "https://git.duniter.org/nodes/typescript/duniter/rust-bins/duniter-dbs-explorer"
+readme = "README.md"
+keywords = ["duniter", "database"]
+license = "AGPL-3.0"
+edition = "2018"
+
+[[bin]]
+bench = false
+path = "src/main.rs"
+name = "dex"
+
+[build-dependencies]
+structopt = "0.3.16"
+
+[dependencies]
+arrayvec = "0.5.1"
+comfy-table = "1.0.0"
+dirs = "3.0.1"
+dubp-common = { version = "0.25.2", features = ["crypto_scrypt"] }
+duniter-dbs = { path = "../../rust-libs/duniter-dbs", default-features = false, features = ["explorer", "leveldb_backend", "sync"] }
+rayon = "1.3.1"
+serde_json = "1.0.53"
+structopt = "0.3.16"
+
+[dev-dependencies]
+serde = { version = "1.0.105", features = ["derive"] }
+unwrap = "1.2.1"
diff --git a/rust-bins/duniter-dbex/README.md b/rust-bins/duniter-dbex/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f49c48fd89510f6efa24c7b495396cac545d7d21
--- /dev/null
+++ b/rust-bins/duniter-dbex/README.md
@@ -0,0 +1,25 @@
+# Duniter databases explorer (dex)
+
+## Compile
+
+    git clone https://git.duniter.org/nodes/typescript/duniter.git
+    cd duniter
+    cargo build --release -p duniter-dbex
+
+The binary executable is then here:  `target/release/dex`
+
+## Use
+
+See `dex --help`
+
+## Autocompletion
+
+Bash autocompletion script is available here : `target/release/dex.bash`
+
+**Several others Shell are supported : Zsh, Fish, Powershell and Elvish!**
+
+To generate the autocompletion script for your shell, recompile with env var `COMPLETION_SHELL`.
+
+For exemple for fish : `COMPLETION_SHELL=fish cargo build --release -p duniter-dbex`
+
+The autocompletion script can be found in : `target/release/`
diff --git a/rust-bins/duniter-dbex/build.rs b/rust-bins/duniter-dbex/build.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dd655380a11772f3baafcd6d14fcde679dcf07f3
--- /dev/null
+++ b/rust-bins/duniter-dbex/build.rs
@@ -0,0 +1,47 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+extern crate structopt;
+
+include!("src/cli.rs");
+
+use std::env;
+use structopt::clap::Shell;
+
+fn main() {
+    // Define out dir
+    let current_dir = match env::current_dir() {
+        Err(_e) => return,
+        Ok(current_dir) => current_dir,
+    };
+    let out_dir = current_dir.as_path().join(format!(
+        "../../target/{}",
+        env::var("PROFILE").unwrap_or_else(|_| "debug".to_owned())
+    ));
+
+    // Define shell
+    let shell = if let Some(shell_str) = option_env!("COMPLETION_SHELL") {
+        Shell::from_str(shell_str).expect("Unknown shell")
+    } else {
+        Shell::Bash
+    };
+
+    let mut app = Opt::clap();
+    app.gen_completions(
+        "dex", // We need to specify the bin name manually
+        shell, // Then say which shell to build completions for
+        out_dir,
+    ); // Then say where write the completions to
+}
diff --git a/rust-bins/duniter-dbex/src/cli.rs b/rust-bins/duniter-dbex/src/cli.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e5da817361b9acf60b25c2fe70165c2feee7d277
--- /dev/null
+++ b/rust-bins/duniter-dbex/src/cli.rs
@@ -0,0 +1,125 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use std::{num::NonZeroUsize, path::PathBuf, str::FromStr};
+use structopt::StructOpt;
+
+#[derive(Debug, StructOpt)]
+#[structopt(name = "duniter-dbex", about = "Duniter databases explorer.")]
+pub struct Opt {
+    /// Duniter profile name
+    #[structopt(short, long)]
+    pub profile: Option<String>,
+
+    /// Duniter home directory
+    #[structopt(short, long, parse(from_os_str))]
+    pub home: Option<PathBuf>,
+
+    /// database
+    #[structopt(default_value = "bc_v1", possible_values = &["bc_v1", "bc_v2", "mp_v1"])]
+    pub database: Database,
+
+    #[structopt(subcommand)]
+    pub cmd: SubCommand,
+}
+
+#[derive(Debug)]
+pub enum Database {
+    BcV1,
+}
+
+impl FromStr for Database {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "bc_v1" => Ok(Self::BcV1),
+            "bc_v2" | "mp_v1" => unimplemented!(),
+            _ => unreachable!(),
+        }
+    }
+}
+
+#[derive(Debug, StructOpt)]
+pub enum SubCommand {
+    /// Count collection entries
+    Count { collection: String },
+    /// Get one value
+    Get { collection: String, key: String },
+    /// Search values by criteria
+    Find {
+        collection: String,
+        #[structopt(short, long)]
+        /// Key min
+        start: Option<String>,
+        #[structopt(short, long)]
+        /// Key max
+        end: Option<String>,
+        /// Filter keys by a regular expression
+        #[structopt(short = "k", long)]
+        key_regex: Option<String>,
+        /// Show keys only
+        #[structopt(long)]
+        keys_only: bool,
+        /// Filter values by a regular expression
+        #[structopt(short = "v", long)]
+        value_regex: Option<String>,
+        /// Maximum number of entries to be found (Slower because force sequential search)
+        #[structopt(short, long)]
+        limit: Option<usize>,
+        /// Browse the collection upside down
+        #[structopt(short, long)]
+        reverse: bool,
+        /// Step by
+        #[structopt(long, default_value = "1")]
+        step: NonZeroUsize,
+        /// Output format
+        #[structopt(short, long, default_value = "table-json", possible_values = &["csv", "json", "table-json", "table-properties"])]
+        output: OutputFormat,
+        /// Pretty json (Only for output format json or table-json)
+        #[structopt(long)]
+        pretty: bool,
+        /// Show only the specified properties
+        #[structopt(short, long)]
+        properties: Vec<String>,
+        /// Export found data to a file
+        #[structopt(short, long, parse(from_os_str))]
+        file: Option<PathBuf>,
+    },
+    /// Show database schema
+    Schema,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum OutputFormat {
+    Table,
+    TableJson,
+    Json,
+    Csv,
+}
+
+impl FromStr for OutputFormat {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "csv" => Ok(Self::Csv),
+            "json" => Ok(Self::Json),
+            "table-properties" => Ok(Self::Table),
+            "table-json" => Ok(Self::TableJson),
+            _ => unreachable!(),
+        }
+    }
+}
diff --git a/rust-bins/duniter-dbex/src/main.rs b/rust-bins/duniter-dbex/src/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5d6b646645ed63f67953bf2f663e503804e0a44b
--- /dev/null
+++ b/rust-bins/duniter-dbex/src/main.rs
@@ -0,0 +1,305 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+#![deny(
+    clippy::unwrap_used,
+    missing_copy_implementations,
+    trivial_casts,
+    trivial_numeric_casts,
+    unstable_features,
+    unused_import_braces
+)]
+
+mod cli;
+mod print_found_data;
+mod stringify_json_value;
+
+use self::cli::{Database, Opt, OutputFormat, SubCommand};
+use self::stringify_json_value::stringify_json_value;
+use comfy_table::Table;
+use duniter_dbs::kv_typed::prelude::*;
+use duniter_dbs::prelude::*;
+use duniter_dbs::regex::Regex;
+use duniter_dbs::serde_json::{Map, Value};
+use duniter_dbs::smallvec::{smallvec, SmallVec};
+use duniter_dbs::BcV1Db;
+use duniter_dbs::BcV1DbWritable;
+use rayon::prelude::*;
+use std::{
+    collections::{HashMap, HashSet},
+    fs::File,
+    io::{stdin, Write},
+    iter::FromIterator,
+    time::Instant,
+};
+use structopt::StructOpt;
+
+const DATA_DIR: &str = "data";
+const TOO_MANY_ENTRIES_ALERT: usize = 5_000;
+
+fn main() -> Result<(), String> {
+    let opt = Opt::from_args();
+
+    let home = if let Some(home) = opt.home {
+        home
+    } else {
+        dirs::config_dir()
+            .ok_or_else(|| {
+                "Fail to auto find duniter's home directory, please specify it explicitly."
+                    .to_owned()
+            })?
+            .as_path()
+            .join("duniter")
+    };
+
+    let profile_name = if let Some(profile_name) = opt.profile {
+        profile_name
+    } else {
+        "duniter_default".to_owned()
+    };
+
+    let profile_path = home.as_path().join(&profile_name);
+    let data_path = profile_path.as_path().join(DATA_DIR);
+
+    if !data_path.exists() {
+        return Err(format!(
+            "Path '{}' don't exist !",
+            data_path.to_str().expect("non-UTF-8 strings not supported")
+        ));
+    }
+
+    let open_db_start_time = Instant::now();
+    match opt.database {
+        Database::BcV1 => apply_subcommand(
+            BcV1Db::<LevelDb>::open(LevelDbConf {
+                db_path: data_path.as_path().join("leveldb"),
+                ..Default::default()
+            })
+            .map_err(|e| format!("{}", e))?,
+            opt.cmd,
+            open_db_start_time,
+        ),
+    }
+}
+
+fn apply_subcommand<DB: DbExplorable>(
+    db: DB,
+    cmd: SubCommand,
+    open_db_start_time: Instant,
+) -> Result<(), String> {
+    let duration = open_db_start_time.elapsed();
+    println!(
+        "Database opened in {}.{:06} seconds.",
+        duration.as_secs(),
+        duration.subsec_micros()
+    );
+    let start_time = Instant::now();
+
+    match cmd {
+        SubCommand::Count { collection } => {
+            if let ExplorerActionResponse::Count(count) = db
+                .explore(&collection, ExplorerAction::Count, stringify_json_value)
+                .map_err(|e| format!("{}", e))?
+                .map_err(|e| e.0)?
+            {
+                let duration = start_time.elapsed();
+                println!(
+                    "Count operation performed in {}.{:06} seconds.",
+                    duration.as_secs(),
+                    duration.subsec_micros()
+                );
+                println!("\nThis collection contains {} entries.", count);
+            }
+        }
+        SubCommand::Get { collection, key } => {
+            if let ExplorerActionResponse::Get(value_opt) = db
+                .explore(
+                    &collection,
+                    ExplorerAction::Get { key: &key },
+                    stringify_json_value,
+                )
+                .map_err(|e| format!("{}", e))?
+                .map_err(|e| e.0)?
+            {
+                if let Some(value) = value_opt {
+                    println!("\n{}", value)
+                } else {
+                    println!("\nThis collection not contains this key.")
+                }
+            }
+        }
+        SubCommand::Find {
+            collection,
+            start,
+            end,
+            key_regex,
+            keys_only,
+            value_regex,
+            limit,
+            reverse,
+            properties,
+            output: output_format,
+            pretty: pretty_json,
+            file: output_file,
+            step,
+        } => {
+            let value_regex_opt = opt_string_to_res_opt_regex(value_regex)?;
+            let captures_headers = if let Some(ref value_regex) = value_regex_opt {
+                value_regex
+                    .capture_names()
+                    .skip(1)
+                    .enumerate()
+                    .map(|(i, name_opt)| {
+                        if let Some(name) = name_opt {
+                            name.to_owned()
+                        } else {
+                            format!("CAP{}", i + 1)
+                        }
+                    })
+                    .collect()
+            } else {
+                vec![]
+            };
+            if let ExplorerActionResponse::Find(entries) = db
+                .explore(
+                    &collection,
+                    ExplorerAction::Find {
+                        key_min: start,
+                        key_max: end,
+                        key_regex: opt_string_to_res_opt_regex(key_regex)?,
+                        value_regex: value_regex_opt,
+                        limit,
+                        reverse,
+                        step,
+                    },
+                    stringify_json_value,
+                )
+                .map_err(|e| format!("{}", e))?
+                .map_err(|e| e.0)?
+            {
+                let duration = start_time.elapsed();
+                println!(
+                    "Search performed in {}.{:06} seconds.\n\n{} entries found.",
+                    duration.as_secs(),
+                    duration.subsec_micros(),
+                    entries.len()
+                );
+
+                if !too_many_entries(entries.len(), output_file.is_none())
+                    .map_err(|e| format!("{}", e))?
+                {
+                    return Ok(());
+                }
+
+                let start_print = Instant::now();
+                if let Some(output_file) = output_file {
+                    let mut file =
+                        File::create(output_file.as_path()).map_err(|e| format!("{}", e))?;
+
+                    //let mut file_buffer = BufWriter::new(file);
+                    print_found_data::print_found_data(
+                        &mut file,
+                        output_format,
+                        pretty_json,
+                        false,
+                        print_found_data::DataToShow {
+                            entries,
+                            keys_only,
+                            only_properties: properties,
+                        },
+                        captures_headers,
+                    )
+                    .map_err(|e| format!("{}", e))?;
+                    //file_buffer.flush().map_err(|e| format!("{}", e))?;
+
+                    let export_duration = start_print.elapsed();
+                    println!(
+                        "Search results were written to file: '{}' in {}.{:06} seconds.",
+                        output_file
+                            .to_str()
+                            .expect("output-file contains invalid utf8 characters"),
+                        export_duration.as_secs(),
+                        export_duration.subsec_micros(),
+                    );
+                } else {
+                    print_found_data::print_found_data(
+                        &mut std::io::stdout(),
+                        output_format,
+                        pretty_json,
+                        true,
+                        print_found_data::DataToShow {
+                            entries,
+                            keys_only,
+                            only_properties: properties,
+                        },
+                        captures_headers,
+                    )
+                    .map_err(|e| format!("{}", e))?;
+                    let print_duration = start_print.elapsed();
+                    println!(
+                        "Search results were displayed in {}.{:06} seconds.",
+                        print_duration.as_secs(),
+                        print_duration.subsec_micros(),
+                    );
+                };
+            }
+        }
+        SubCommand::Schema => {
+            show_db_schema(db.list_collections());
+        }
+    };
+
+    Ok(())
+}
+
+fn too_many_entries(entries_len: usize, output_in_term: bool) -> std::io::Result<bool> {
+    if entries_len > TOO_MANY_ENTRIES_ALERT {
+        println!(
+            "{} all {} entries ? (Be careful, may crash your system!) [y/N]",
+            if output_in_term { "Display" } else { "Export" },
+            entries_len
+        );
+        let mut buffer = String::new();
+        stdin().read_line(&mut buffer)?;
+        Ok(buffer == "y\n")
+    } else {
+        Ok(true)
+    }
+}
+
+fn show_db_schema(collections_names: Vec<(&'static str, &'static str, &'static str)>) {
+    let mut table = Table::new();
+    table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
+    table.set_header(&["Collection name", "Key type", "Value type"]);
+    for (collection_name, key_full_type_name, value_full_type_name) in collections_names {
+        let key_type_name_opt = key_full_type_name.split(':').last();
+        let value_type_name_opt = value_full_type_name.split(':').last();
+        table.add_row(&[
+            collection_name,
+            key_type_name_opt.unwrap_or("?"),
+            value_type_name_opt.unwrap_or("?"),
+        ]);
+    }
+    println!("{}", table);
+}
+
+#[inline]
+fn opt_string_to_res_opt_regex(str_regex_opt: Option<String>) -> Result<Option<Regex>, String> {
+    if let Some(str_regex) = str_regex_opt {
+        Ok(Some(Regex::new(&str_regex).map_err(|e| format!("{}", e))?))
+    } else {
+        Ok(None)
+    }
+}
diff --git a/rust-bins/duniter-dbex/src/print_found_data.rs b/rust-bins/duniter-dbex/src/print_found_data.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0c600ef0b2ab22b7831a79a1ec0f83d4558ebaa0
--- /dev/null
+++ b/rust-bins/duniter-dbex/src/print_found_data.rs
@@ -0,0 +1,383 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+const KEY_COLUMN_NAME: &str = "Key";
+const VALUE_COLUMN_NAME: &str = "Value";
+
+pub struct DataToShow {
+    pub entries: Vec<EntryFound>,
+    pub keys_only: bool,
+    pub only_properties: Vec<String>,
+}
+
+pub fn print_found_data<W: Write>(
+    output: &mut W,
+    output_format: OutputFormat,
+    pretty_json: bool,
+    dynamic_content_arrangement: bool,
+    data_to_show: DataToShow,
+    captures_names: Vec<String>,
+) -> std::io::Result<()> {
+    let DataToShow {
+        entries,
+        keys_only,
+        only_properties,
+    } = data_to_show;
+    if entries.is_empty() {
+        return Ok(());
+    }
+
+    let only_properties_set = if !only_properties.is_empty() {
+        HashSet::from_iter(only_properties.into_iter())
+    } else {
+        HashSet::with_capacity(0)
+    };
+
+    match output_format {
+        OutputFormat::Json => {
+            let json_array = if keys_only {
+                entries
+                    .into_par_iter()
+                    .map(|entry| key_to_json(entry.key))
+                    .collect()
+            } else {
+                entries
+                    .into_par_iter()
+                    .map(|entry| entry_to_json(&only_properties_set, &captures_names, entry))
+                    .collect()
+            };
+            if pretty_json {
+                writeln!(output, "{:#}", Value::Array(json_array))
+            } else {
+                writeln!(output, "{}", Value::Array(json_array))
+            }
+        }
+        OutputFormat::Table => {
+            // If value is not an object or an array of objects, force raw output format
+            let mut entries_iter = entries.iter();
+            let first_object_opt = if keys_only {
+                None
+            } else {
+                loop {
+                    if let Some(EntryFound { value, .. }) = entries_iter.next() {
+                        if let Value::Array(ref json_array) = value {
+                            if json_array.is_empty() {
+                                continue;
+                            } else {
+                                break json_array[0].as_object();
+                            }
+                        } else {
+                            break value.as_object();
+                        }
+                    } else {
+                        // All values are empty array, force raw output format
+                        break None;
+                    }
+                }
+            };
+
+            let properties_names = if let Some(first_object) = first_object_opt {
+                if only_properties_set.is_empty() {
+                    first_object.keys().cloned().collect::<HashSet<String>>()
+                } else {
+                    first_object
+                        .keys()
+                        .filter(|property_name| {
+                            only_properties_set.contains(property_name.as_str())
+                        })
+                        .cloned()
+                        .collect::<HashSet<String>>()
+                }
+            } else {
+                return print_found_data(
+                    output,
+                    OutputFormat::TableJson,
+                    pretty_json,
+                    dynamic_content_arrangement,
+                    print_found_data::DataToShow {
+                        entries,
+                        keys_only,
+                        only_properties: vec![],
+                    },
+                    captures_names,
+                );
+            };
+
+            // Create table
+            let mut table = Table::new();
+            if dynamic_content_arrangement {
+                table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
+            }
+
+            // Map data by property
+            let entries_map: Vec<HashMap<String, String>> = entries
+                .into_par_iter()
+                .map(|entry| entry_to_rows(&only_properties_set, &captures_names, entry))
+                .flatten()
+                .collect();
+
+            // Define table headers
+            let mut headers = Vec::with_capacity(1 + properties_names.len() + captures_names.len());
+            headers.push(KEY_COLUMN_NAME.to_owned());
+            if !keys_only {
+                for property_name in properties_names {
+                    headers.push(property_name);
+                }
+                headers.sort_by(|a, b| {
+                    if a == KEY_COLUMN_NAME {
+                        std::cmp::Ordering::Less
+                    } else if b == KEY_COLUMN_NAME {
+                        std::cmp::Ordering::Greater
+                    } else {
+                        a.cmp(b)
+                    }
+                });
+                headers.extend(captures_names);
+            }
+            table.set_header(&headers);
+
+            // Fill table
+            for properties_values in entries_map {
+                let mut row = SmallVec::<[&str; 8]>::new();
+                for column_name in &headers {
+                    if let Some(property_value) = properties_values.get(column_name) {
+                        row.push(property_value.as_str());
+                    } else {
+                        row.push("")
+                    }
+                }
+                table.add_row(row);
+            }
+
+            // Print table
+            writeln!(output, "{}", table)
+        }
+        OutputFormat::TableJson => {
+            let mut table = Table::new();
+            if dynamic_content_arrangement {
+                table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
+            }
+            let mut headers = Vec::with_capacity(2 + captures_names.len());
+            headers.push(KEY_COLUMN_NAME);
+            if !keys_only {
+                headers.push(VALUE_COLUMN_NAME);
+            }
+            headers.extend(captures_names.iter().map(String::as_str));
+            table.set_header(headers);
+            for EntryFound {
+                key,
+                value,
+                captures: captures_opt,
+            } in entries
+            {
+                let mut row = Vec::with_capacity(2 + captures_names.len());
+                row.push(key);
+                if keys_only {
+                    table.add_row(row);
+                } else {
+                    if pretty_json {
+                        row.push(format!("{:#}", value));
+                    } else {
+                        row.push(value.to_string());
+                    }
+                    let rows = caps_to_rows(row, &captures_names[..], captures_opt);
+                    for row in rows {
+                        table.add_row(row);
+                    }
+                }
+            }
+            writeln!(output, "{}", table)
+        }
+        _ => todo!(),
+    }
+}
+
+#[inline(always)]
+fn key_to_json(key: String) -> Value {
+    Value::String(key)
+}
+
+fn entry_to_json(
+    only_properties_set: &HashSet<String>,
+    captures_names: &[String],
+    entry: EntryFound,
+) -> Value {
+    let EntryFound {
+        key,
+        mut value,
+        captures: captures_opt,
+    } = entry;
+    if !only_properties_set.is_empty() {
+        match value {
+            Value::Object(ref mut json_map) => {
+                let mut properties_to_rm = SmallVec::<[String; 64]>::new();
+                for property_name in json_map.keys() {
+                    if !only_properties_set.contains(property_name) {
+                        properties_to_rm.push(property_name.clone());
+                    }
+                }
+                for property_name in properties_to_rm {
+                    json_map.remove(&property_name);
+                }
+            }
+            Value::Array(ref mut json_array) => {
+                for sub_value in json_array {
+                    if let Value::Object(ref mut json_map) = sub_value {
+                        let mut properties_to_rm = SmallVec::<[String; 64]>::new();
+                        for property_name in json_map.keys() {
+                            if !only_properties_set.contains(property_name) {
+                                properties_to_rm.push(property_name.clone());
+                            }
+                        }
+                        for property_name in properties_to_rm {
+                            json_map.remove(&property_name);
+                        }
+                    }
+                }
+            }
+            _ => (),
+        }
+    }
+    let mut json_map = Map::with_capacity(2);
+    json_map.insert("key".to_owned(), Value::String(key));
+    json_map.insert("value".to_owned(), value);
+    if !captures_names.is_empty() {
+        let mut captures_objects = Vec::new();
+        if let Some(ValueCaptures(captures)) = captures_opt {
+            for capture in captures {
+                let mut capture_object = Map::with_capacity(captures_names.len());
+                for (i, capture_group_value_opt) in capture.into_iter().enumerate() {
+                    if let Some(capture_group_value) = capture_group_value_opt {
+                        capture_object.insert(
+                            captures_names[i].to_owned(),
+                            Value::String(capture_group_value),
+                        );
+                    }
+                }
+                captures_objects.push(Value::Object(capture_object));
+            }
+        }
+        json_map.insert("captures".to_owned(), Value::Array(captures_objects));
+    }
+    Value::Object(json_map)
+}
+
+fn entry_to_rows(
+    only_properties_set: &HashSet<String>,
+    captures_names: &[String],
+    entry: EntryFound,
+) -> Vec<HashMap<String, String>> {
+    let EntryFound {
+        key,
+        value,
+        captures: captures_opt,
+    } = entry;
+    match value {
+        Value::Object(value_json_map) => {
+            let row_map = map_entry_by_properties(&only_properties_set, key, value_json_map);
+            caps_to_rows_maps(row_map, captures_names, captures_opt)
+        }
+        Value::Array(json_array) => json_array
+            .into_iter()
+            .map(|sub_value| {
+                if let Value::Object(sub_value_json_map) = sub_value {
+                    map_entry_by_properties(&only_properties_set, key.clone(), sub_value_json_map)
+                } else {
+                    unreachable!()
+                }
+            })
+            .collect(),
+        _ => unreachable!(),
+    }
+}
+
+fn map_entry_by_properties(
+    only_properties_set: &HashSet<String>,
+    k: String,
+    value_json_map: Map<String, Value>,
+) -> HashMap<String, String> {
+    let mut row_map = HashMap::with_capacity(1 + value_json_map.len());
+    row_map.insert(KEY_COLUMN_NAME.to_owned(), k);
+    for (property_name, property_value) in value_json_map {
+        if only_properties_set.is_empty() || only_properties_set.contains(&property_name) {
+            if let Value::String(property_value_string) = property_value {
+                row_map.insert(property_name, property_value_string);
+            } else {
+                row_map.insert(property_name, property_value.to_string());
+            }
+        }
+    }
+    row_map
+}
+
+fn caps_to_rows(
+    mut first_row_begin: Vec<String>,
+    captures_names: &[String],
+    captures_opt: Option<ValueCaptures>,
+) -> SmallVec<[Vec<String>; 2]> {
+    if !captures_names.is_empty() {
+        if let Some(ValueCaptures(captures)) = captures_opt {
+            let first_row_begin_len = first_row_begin.len();
+            let mut rows = SmallVec::with_capacity(captures.len());
+            let mut current_row = first_row_begin;
+            for capture in captures {
+                for capture_group_value_opt in capture.into_iter() {
+                    if let Some(capture_group_value) = capture_group_value_opt {
+                        current_row.push(capture_group_value);
+                    } else {
+                        current_row.push(String::new());
+                    }
+                }
+                rows.push(current_row);
+                current_row = (0..first_row_begin_len).map(|_| String::new()).collect();
+            }
+            rows
+        } else {
+            first_row_begin.extend((0..captures_names.len()).map(|_| String::new()));
+            smallvec![first_row_begin]
+        }
+    } else {
+        smallvec![first_row_begin]
+    }
+}
+
+fn caps_to_rows_maps(
+    first_row_map_begin: HashMap<String, String>,
+    captures_names: &[String],
+    captures_opt: Option<ValueCaptures>,
+) -> Vec<HashMap<String, String>> {
+    if !captures_names.is_empty() {
+        if let Some(ValueCaptures(captures)) = captures_opt {
+            let mut rows = Vec::with_capacity(captures.len());
+            let mut current_row_map = first_row_map_begin;
+            for capture in captures {
+                for (i, capture_group_value_opt) in capture.into_iter().enumerate() {
+                    if let Some(capture_group_value) = capture_group_value_opt {
+                        current_row_map.insert(captures_names[i].to_owned(), capture_group_value);
+                    }
+                }
+                rows.push(current_row_map);
+                current_row_map = HashMap::with_capacity(captures_names.len());
+            }
+            rows
+        } else {
+            vec![first_row_map_begin]
+        }
+    } else {
+        vec![first_row_map_begin]
+    }
+}
diff --git a/rust-bins/duniter-dbex/src/stringify_json_value.rs b/rust-bins/duniter-dbex/src/stringify_json_value.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7070c166c5baa8cd8b7b0fda2be5f66f321ad853
--- /dev/null
+++ b/rust-bins/duniter-dbex/src/stringify_json_value.rs
@@ -0,0 +1,219 @@
+use arrayvec::ArrayVec;
+use dubp_common::crypto::bases::b58::ToBase58 as _;
+use dubp_common::crypto::hashs::Hash;
+use dubp_common::crypto::keys::ed25519::{PublicKey, Signature};
+use dubp_common::crypto::keys::Signature as _;
+use std::convert::TryFrom;
+
+pub fn stringify_json_value(mut json_value: serde_json::Value) -> serde_json::Value {
+    match json_value {
+        serde_json::Value::Object(ref mut json_obj) => stringify_json_object(json_obj),
+        serde_json::Value::Array(ref mut json_array) => {
+            for json_array_cell in json_array {
+                if let serde_json::Value::Object(json_obj) = json_array_cell {
+                    stringify_json_object(json_obj)
+                }
+            }
+        }
+        _ => (),
+    }
+
+    json_value
+}
+
+fn stringify_json_object(json_object: &mut serde_json::Map<String, serde_json::Value>) {
+    let mut stringified_values: Vec<(String, serde_json::Value)> = Vec::new();
+    for (k, v) in json_object.iter_mut() {
+        match k.as_str() {
+            "pub" | "pubkey" | "issuer" => {
+                if let serde_json::Value::Object(json_pubkey) = v {
+                    let json_pubkey_data = json_pubkey.get("datas").expect("corrupted db");
+                    if let serde_json::Value::Array(json_array) = json_pubkey_data {
+                        let pubkey_string =
+                            PublicKey::try_from(&json_array_to_32_bytes(json_array)[..])
+                                .expect("corrupted db")
+                                .to_base58();
+                        stringified_values
+                            .push((k.to_owned(), serde_json::Value::String(pubkey_string)));
+                    } else {
+                        panic!("corrupted db");
+                    }
+                }
+            }
+            "hash" | "inner_hash" | "previous_hash" => {
+                if let serde_json::Value::Array(json_array) = v {
+                    let hash_string = Hash(json_array_to_32_bytes(json_array)).to_hex();
+                    stringified_values.push((k.to_owned(), serde_json::Value::String(hash_string)));
+                }
+            }
+            "sig" | "signature" => {
+                if let serde_json::Value::Array(json_array) = v {
+                    let sig_string = Signature(json_array_to_64_bytes(json_array)).to_base64();
+                    stringified_values.push((k.to_owned(), serde_json::Value::String(sig_string)));
+                }
+            }
+            _ => {
+                if let serde_json::Value::Object(ref mut json_sub_object) = v {
+                    stringify_json_object(json_sub_object)
+                }
+            }
+        }
+    }
+    for (k, v) in stringified_values {
+        json_object.insert(k, v);
+    }
+}
+
+#[inline]
+fn json_array_to_32_bytes(json_array: &[serde_json::Value]) -> [u8; 32] {
+    let bytes = json_array
+        .iter()
+        .map(|jv| {
+            if let serde_json::Value::Number(jn) = jv {
+                jn.as_u64().unwrap_or_default() as u8
+            } else {
+                panic!("corrupted db")
+            }
+        })
+        .collect::<ArrayVec<[u8; 32]>>();
+    bytes.into_inner().expect("corrupted db")
+}
+
+#[inline]
+fn json_array_to_64_bytes(json_array: &[serde_json::Value]) -> [u8; 64] {
+    let bytes = json_array
+        .iter()
+        .map(|jv| {
+            if let serde_json::Value::Number(jn) = jv {
+                jn.as_u64().unwrap_or_default() as u8
+            } else {
+                panic!("corrupted db")
+            }
+        })
+        .collect::<ArrayVec<[u8; 64]>>();
+    bytes.into_inner().expect("corrupted db")
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use dubp_common::crypto::keys::PublicKey as _;
+    use serde_json::Number;
+    use serde_json::Value;
+    use unwrap::unwrap;
+
+    #[derive(serde::Serialize)]
+    struct JsonObjectTest {
+        pubkey: PublicKey,
+        hash: Hash,
+        other: usize,
+        inner: JsonSubObjectTest,
+    }
+
+    #[derive(serde::Serialize)]
+    struct JsonSubObjectTest {
+        issuer: PublicKey,
+    }
+
+    #[test]
+    fn test_stringify_json_object() {
+        let mut json_value = unwrap!(serde_json::to_value(JsonObjectTest {
+            pubkey: unwrap!(PublicKey::from_base58(
+                "A2C6cVJnkkT2n4ivMPiLH2njQHeHSZcVf1cSTwZYScQ6"
+            )),
+            hash: unwrap!(Hash::from_hex(
+                "51DF2FCAB8809596253CD98594D0DBCEECAAF3A88A43C6EDD285B6B24FB9D50D"
+            )),
+            other: 3,
+            inner: JsonSubObjectTest {
+                issuer: unwrap!(PublicKey::from_base58(
+                    "4agK3ycEQNahuRGoFJDXA2aQGt4iV2YSMPKcoMeR6ZfA"
+                ))
+            }
+        }));
+
+        if let serde_json::Value::Object(ref mut json_obj) = json_value {
+            stringify_json_object(json_obj);
+
+            assert_eq!(
+                json_obj.get("pubkey"),
+                Some(Value::String(
+                    "A2C6cVJnkkT2n4ivMPiLH2njQHeHSZcVf1cSTwZYScQ6".to_owned()
+                ))
+                .as_ref()
+            );
+            assert_eq!(
+                json_obj.get("hash"),
+                Some(Value::String(
+                    "51DF2FCAB8809596253CD98594D0DBCEECAAF3A88A43C6EDD285B6B24FB9D50D".to_owned()
+                ))
+                .as_ref()
+            );
+            assert_eq!(
+                json_obj.get("other"),
+                Some(Value::Number(Number::from(3))).as_ref()
+            );
+
+            let json_sub_obj = unwrap!(json_obj.get("inner"));
+            if let serde_json::Value::Object(json_sub_obj) = json_sub_obj {
+                assert_eq!(
+                    json_sub_obj.get("issuer"),
+                    Some(Value::String(
+                        "4agK3ycEQNahuRGoFJDXA2aQGt4iV2YSMPKcoMeR6ZfA".to_owned()
+                    ))
+                    .as_ref()
+                );
+            } else {
+                panic!("json_sub_obj must be an abject");
+            }
+        } else {
+            panic!("json_value must be an abject");
+        }
+    }
+
+    #[test]
+    fn test_json_array_to_32_bytes() {
+        let json_array = vec![
+            Value::Number(Number::from(0)),
+            Value::Number(Number::from(1)),
+            Value::Number(Number::from(2)),
+            Value::Number(Number::from(0)),
+            Value::Number(Number::from(1)),
+            Value::Number(Number::from(2)),
+            Value::Number(Number::from(0)),
+            Value::Number(Number::from(1)),
+            Value::Number(Number::from(2)),
+            Value::Number(Number::from(0)),
+            Value::Number(Number::from(1)),
+            Value::Number(Number::from(2)),
+            Value::Number(Number::from(0)),
+            Value::Number(Number::from(1)),
+            Value::Number(Number::from(2)),
+            Value::Number(Number::from(0)),
+            Value::Number(Number::from(1)),
+            Value::Number(Number::from(2)),
+            Value::Number(Number::from(0)),
+            Value::Number(Number::from(1)),
+            Value::Number(Number::from(2)),
+            Value::Number(Number::from(0)),
+            Value::Number(Number::from(1)),
+            Value::Number(Number::from(2)),
+            Value::Number(Number::from(0)),
+            Value::Number(Number::from(1)),
+            Value::Number(Number::from(2)),
+            Value::Number(Number::from(0)),
+            Value::Number(Number::from(1)),
+            Value::Number(Number::from(2)),
+            Value::Number(Number::from(0)),
+            Value::Number(Number::from(1)),
+        ];
+
+        assert_eq!(
+            [
+                0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0,
+                1, 2, 0, 1
+            ],
+            json_array_to_32_bytes(json_array.as_ref()),
+        )
+    }
+}
diff --git a/rust-libs/dubp-wot/Cargo.toml b/rust-libs/dubp-wot/Cargo.toml
index 6f619bea6e445be9673b064499cdceb64fc4d5ba..5120cd47a0728dc3f85130c7f88779bd8e56eb64 100644
--- a/rust-libs/dubp-wot/Cargo.toml
+++ b/rust-libs/dubp-wot/Cargo.toml
@@ -19,6 +19,6 @@ serde = { version = "1.0.105", features = ["derive"] }
 
 [dev-dependencies]
 bincode = "1.2.0"
-dubp-common = { version = "0.2.0", features = ["rand", "scrypt"] }
+dubp-common = { version = "0.25.2", features = ["crypto_scrypt"] }
 
 [features]
diff --git a/rust-libs/dubp-wot/src/data/mod.rs b/rust-libs/dubp-wot/src/data/mod.rs
index 3aa86a7deeafd2cdec72607c9949508b8fac6c27..eddc11e19b5083148615cd53848fd212cd894f6d 100644
--- a/rust-libs/dubp-wot/src/data/mod.rs
+++ b/rust-libs/dubp-wot/src/data/mod.rs
@@ -1,4 +1,4 @@
-//  Copyright (C) 2017-2019  The AXIOM TEAM Association.
+//  Copyright (C) 2020 Éloïs SANCHEZ.
 //
 // This program is free software: you can redistribute it and/or modify
 // it under the terms of the GNU Affero General Public License as
diff --git a/rust-libs/dubp-wot/src/data/rusty.rs b/rust-libs/dubp-wot/src/data/rusty.rs
index 553bdfb023190814118ab65478e4d6f52c30819d..398027350a5fc2d25ea1da83ba0c58e252d2e7fc 100644
--- a/rust-libs/dubp-wot/src/data/rusty.rs
+++ b/rust-libs/dubp-wot/src/data/rusty.rs
@@ -1,4 +1,4 @@
-//  Copyright (C) 2017-2019  The AXIOM TEAM Association.
+//  Copyright (C) 2020 Éloïs SANCHEZ.
 //
 // This program is free software: you can redistribute it and/or modify
 // it under the terms of the GNU Affero General Public License as
diff --git a/rust-libs/dubp-wot/src/operations/centrality.rs b/rust-libs/dubp-wot/src/operations/centrality.rs
index 029d8927a4d00bf0bee1ca8c610f1d8bc231b9e5..933e43621c24863dbfb6b410d336dff7a32d9c59 100644
--- a/rust-libs/dubp-wot/src/operations/centrality.rs
+++ b/rust-libs/dubp-wot/src/operations/centrality.rs
@@ -1,4 +1,4 @@
-//  Copyright (C) 2017-2019  The AXIOM TEAM Association.
+//  Copyright (C) 2020 Éloïs SANCHEZ.
 //
 // This program is free software: you can redistribute it and/or modify
 // it under the terms of the GNU Affero General Public License as
diff --git a/rust-libs/dubp-wot/src/operations/density.rs b/rust-libs/dubp-wot/src/operations/density.rs
index 029d176c7095a11143a2d6969aec9d5a8ae03420..76ae9a0f2ca00e540c9ad55c64d2b1dafaf2e8cf 100644
--- a/rust-libs/dubp-wot/src/operations/density.rs
+++ b/rust-libs/dubp-wot/src/operations/density.rs
@@ -1,4 +1,4 @@
-//  Copyright (C) 2017-2019  The AXIOM TEAM Association.
+//  Copyright (C) 2020 Éloïs SANCHEZ.
 //
 // This program is free software: you can redistribute it and/or modify
 // it under the terms of the GNU Affero General Public License as
diff --git a/rust-libs/dubp-wot/src/operations/distance.rs b/rust-libs/dubp-wot/src/operations/distance.rs
index 3badf16cbf9f87ab4c039ec7b2ceb76123dff279..f67dffbe18bd8c5b13fd04dc83bf26987f51b9c9 100644
--- a/rust-libs/dubp-wot/src/operations/distance.rs
+++ b/rust-libs/dubp-wot/src/operations/distance.rs
@@ -1,4 +1,4 @@
-//  Copyright (C) 2017-2019  The AXIOM TEAM Association.
+//  Copyright (C) 2020 Éloïs SANCHEZ.
 //
 // This program is free software: you can redistribute it and/or modify
 // it under the terms of the GNU Affero General Public License as
diff --git a/rust-libs/dubp-wot/src/operations/mod.rs b/rust-libs/dubp-wot/src/operations/mod.rs
index 376ad1c281812d82094c2a66810bedaf351051ee..e3fb53ca1cbe492ef0f6b2aba76f6a3d2f8ad133 100644
--- a/rust-libs/dubp-wot/src/operations/mod.rs
+++ b/rust-libs/dubp-wot/src/operations/mod.rs
@@ -1,4 +1,4 @@
-//  Copyright (C) 2017-2019  The AXIOM TEAM Association.
+//  Copyright (C) 2020 Éloïs SANCHEZ.
 //
 // This program is free software: you can redistribute it and/or modify
 // it under the terms of the GNU Affero General Public License as
diff --git a/rust-libs/dubp-wot/src/operations/path.rs b/rust-libs/dubp-wot/src/operations/path.rs
index 5ca9b70283c86827ea6da7b1699ec2fa8fb8db85..5aef5149c1ebe34d2bf9cbebe63a752522c08858 100644
--- a/rust-libs/dubp-wot/src/operations/path.rs
+++ b/rust-libs/dubp-wot/src/operations/path.rs
@@ -1,4 +1,4 @@
-//  Copyright (C) 2017-2019  The AXIOM TEAM Association.
+//  Copyright (C) 2020 Éloïs SANCHEZ.
 //
 // This program is free software: you can redistribute it and/or modify
 // it under the terms of the GNU Affero General Public License as
diff --git a/rust-libs/duniter-dbs/Cargo.toml b/rust-libs/duniter-dbs/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..5d2d1c4e36bf487c03544f130886ae984ec2b24a
--- /dev/null
+++ b/rust-libs/duniter-dbs/Cargo.toml
@@ -0,0 +1,40 @@
+[package]
+name = "duniter-dbs"
+version = "0.1.0"
+authors = ["elois <elois@duniter.org>"]
+description = "Duniter blockchain DB"
+repository = "https://git.duniter.org/nodes/typescript/duniter"
+keywords = ["dubp", "duniter", "blockchain", "database"]
+license = "AGPL-3.0"
+edition = "2018"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+arrayvec = "0.5.1"
+chrono = { version = "0.4.15", optional = true }
+dubp-common = { version = "0.25.2", features = ["crypto_scrypt"] }
+kv_typed = { path = "../tools/kv_typed", default-features = false }
+log = "0.4.8"
+mockall = { version = "0.8.0", optional = true }
+serde = { version = "1.0.105", features = ["derive"] }
+serde_json = "1.0.53"
+smallvec = { version = "1.4.0", features = ["serde"] }
+thiserror = "1.0.20"
+
+[dev-dependencies]
+once_cell = "1.4.0"
+tempdir = "0.3.7"
+unwrap = "1.2.1"
+
+[features]
+default = ["leveldb_backend", "memory_backend", "sled_backend", "subscription", "sync"]
+
+explorer = ["chrono", "kv_typed/explorer"]
+leveldb_backend = ["kv_typed/leveldb_backend"]
+memory_backend = ["kv_typed/memory_backend"]
+mock = ["kv_typed/mock", "mockall"]
+sled_backend = ["kv_typed/sled_backend"]
+subscription = ["kv_typed/subscription"]
+sync = ["kv_typed/sync"]
diff --git a/rust-libs/duniter-dbs/src/bc_v1.rs b/rust-libs/duniter-dbs/src/bc_v1.rs
new file mode 100644
index 0000000000000000000000000000000000000000..870bea9add36fcb3a3d7b3b3b0510968e0a33c19
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/bc_v1.rs
@@ -0,0 +1,158 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+db_schema!(
+    BcV1,
+    [
+        ["level_blockchain", main_blocks, BlockNumberKeyV1, BlockDbV1,],
+        [
+            "level_blockchain/idty",
+            mb_idty,
+            PubKeyKeyV1,
+            BlockNumberArrayV1,
+        ],
+        [
+            "level_blockchain/certs",
+            mb_certs,
+            PubKeyKeyV1,
+            BlockNumberArrayV1
+        ],
+        [
+            "level_blockchain/joiners",
+            mb_joiners,
+            PubKeyKeyV1,
+            BlockNumberArrayV1
+        ],
+        [
+            "level_blockchain/actives",
+            mb_actives,
+            PubKeyKeyV1,
+            BlockNumberArrayV1
+        ],
+        [
+            "level_blockchain/leavers",
+            mb_leavers,
+            PubKeyKeyV1,
+            BlockNumberArrayV1
+        ],
+        [
+            "level_blockchain/excluded",
+            mb_excluded,
+            PubKeyKeyV1,
+            BlockNumberArrayV1
+        ],
+        [
+            "level_blockchain/revoked",
+            mb_revoked,
+            PubKeyAndSigV1,
+            BlockNumberArrayV1
+        ],
+        [
+            "level_blockchain/dividends",
+            mb_dividends,
+            AllKeyV1,
+            BlockNumberArrayV1
+        ],
+        [
+            "level_blockchain/transactions",
+            mb_transactions,
+            AllKeyV1,
+            BlockNumberArrayV1
+        ],
+        [
+            "level_blockchain/forks",
+            fork_blocks,
+            BlockstampKeyV1,
+            BlockDbV1
+        ],
+        ["level_bindex", bindex, BlockNumberKeyV1, BlockHeadDbV1],
+        ["level_iindex", iindex, PubKeyKeyV1, IIndexDbV1],
+        [
+            "level_iindex/hash",
+            iindex_hash,
+            HashKeyV1,
+            PublicKeySingletonDbV1
+        ],
+        ["level_iindex/kick", iindex_kick, PubKeyKeyV1, KickDbV1],
+        [
+            "level_iindex/writtenOn",
+            iindex_written_on,
+            BlockNumberKeyV1,
+            PublicKeyArrayDbV1
+        ],
+        ["level_iindex/uid", uids, UidKeyV1, PublicKeySingletonDbV1],
+        ["level_mindex", mindex, PubKeyKeyV1, MIndexDbV1],
+        [
+            "level_mindex/expiresOn",
+            mindex_expires_on,
+            TimestampKeyV1,
+            PublicKeyArrayDbV1
+        ],
+        [
+            "level_mindex/revokesOn",
+            mindex_revokes_on,
+            TimestampKeyV1,
+            PublicKeyArrayDbV1
+        ],
+        [
+            "level_mindex/writtenOn",
+            mindex_written_on,
+            BlockNumberKeyV1,
+            PublicKeyArrayDbV1
+        ],
+        ["level_cindex", cindex, PubKeyKeyV1, CIndexDbV1],
+        [
+            "level_cindex/expiresOn",
+            cindex_expires_on,
+            BlockNumberKeyV1,
+            PublicKeyArrayDbV1
+        ],
+        [
+            "level_cindex/writtenOn",
+            cindex_written_on,
+            BlockNumberKeyV1,
+            PublicKeyArrayDbV1
+        ],
+        ["level_wallet", Wallet, WalletConditionsV1, WalletDbV1],
+        ["level_dividend", uds, PubKeyKeyV1, UdEntryDbV1],
+        [
+            "level_dividend/level_dividend_trim_index",
+            uds_trim,
+            BlockNumberKeyV1,
+            PublicKeyArrayDbV1
+        ],
+        ["level_sindex", sindex, SourceKeyV1, SIndexDBV1],
+        [
+            "level_sindex/written_on",
+            sindex_written_on,
+            BlockNumberKeyV1,
+            SourceKeyArrayDbV1
+        ],
+        [
+            "level_sindex/consumed_on",
+            sindex_consumed_on,
+            BlockNumberKeyV1,
+            SourceKeyArrayDbV1
+        ],
+        [
+            "level_sindex/conditions",
+            sindex_conditions,
+            WalletConditionsV1,
+            SourceKeyArrayDbV1
+        ],
+    ]
+);
diff --git a/rust-libs/duniter-dbs/src/errors.rs b/rust-libs/duniter-dbs/src/errors.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d945b9235813f2beb06708ae9b103e7f0bdda637
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/errors.rs
@@ -0,0 +1,49 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+pub type Result<T> = std::result::Result<T, ErrorDb>;
+
+#[derive(Debug, Error)]
+pub enum ErrorDb {
+    #[error("batch already consumed")]
+    BatchConsumed,
+    #[error("Collection '{collection_typename}' not exist on db '{db_version}'")]
+    NonexistentCollection {
+        collection_typename: &'static str,
+        db_version: &'static str,
+    },
+    #[cfg(feature = "explorer")]
+    #[error("Collection '{collection_name}' not exist on db '{db_version}'")]
+    NonexistentCollectionName {
+        collection_name: String,
+        db_version: &'static str,
+    },
+    #[error("DbError: {0}")]
+    DbError(String),
+    #[error("DeserError: {0}")]
+    DeserError(String),
+    #[error("SerError: {0}")]
+    Ser(String),
+    #[error("Fail to create DB folder: {0}")]
+    FailToCreateDbFolder(std::io::Error),
+}
+
+impl From<KvError> for ErrorDb {
+    fn from(e: KvError) -> Self {
+        ErrorDb::DbError(format!("{}", e))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/keys.rs b/rust-libs/duniter-dbs/src/keys.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d05a345d404619552de75b41844f14667445875c
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/keys.rs
@@ -0,0 +1,25 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+pub mod all;
+pub mod block_number;
+pub mod blockstamp;
+pub mod hash;
+pub mod pubkey;
+pub mod pubkey_and_sig;
+pub mod source_key;
+pub mod timestamp;
+pub mod uid;
+pub mod wallet_conditions;
diff --git a/rust-libs/duniter-dbs/src/keys/all.rs b/rust-libs/duniter-dbs/src/keys/all.rs
new file mode 100644
index 0000000000000000000000000000000000000000..067e44616932bc7d3ef733fc10dd8601d38113a0
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/keys/all.rs
@@ -0,0 +1,56 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
+pub struct AllKeyV1;
+
+impl KeyAsBytes for AllKeyV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        f(b"ALL")
+    }
+}
+
+impl kv_typed::prelude::FromBytes for AllKeyV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        if bytes == b"ALL" {
+            Ok(Self)
+        } else {
+            Err(StringErr(format!(
+                "Invalid key: expected '{:?}', found '{:?}'",
+                b"ALL", bytes
+            )))
+        }
+    }
+}
+
+impl ToDumpString for AllKeyV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableKey for AllKeyV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        <Self as kv_typed::prelude::FromBytes>::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_string(&self) -> KvResult<String> {
+        self.as_bytes(|bytes| Ok(unsafe { std::str::from_utf8_unchecked(bytes) }.to_owned()))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/keys/block_number.rs b/rust-libs/duniter-dbs/src/keys/block_number.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b2cd7755bf11987c261bbc08f0757319d76d41ad
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/keys/block_number.rs
@@ -0,0 +1,97 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
+pub struct BlockNumberKeyV1(pub BlockNumber);
+
+impl KeyAsBytes for BlockNumberKeyV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        if self.0 == BlockNumber(u32::MAX) {
+            f(b"0000000NaN")
+        } else {
+            f(format!("{:010}", (self.0).0).as_bytes())
+        }
+    }
+}
+
+impl FromBytes for BlockNumberKeyV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let key_str = std::str::from_utf8(bytes).map_err(|e| StringErr(format!("{}", e)))?;
+        if key_str == "0000000NaN" {
+            Ok(BlockNumberKeyV1(BlockNumber(u32::MAX)))
+        } else {
+            Ok(BlockNumberKeyV1(BlockNumber(
+                key_str
+                    .parse()
+                    .map_err(|e| StringErr(format!("{}: {}", e, key_str)))?,
+            )))
+        }
+    }
+}
+
+impl ToDumpString for BlockNumberKeyV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableKey for BlockNumberKeyV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_string(&self) -> KvResult<String> {
+        Ok(format!("{}", (self.0).0))
+    }
+}
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
+pub struct BlockNumberKeyV2(pub BlockNumber);
+
+impl KeyAsBytes for BlockNumberKeyV2 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        if cfg!(target_endian = "big") {
+            println!("TMP: big");
+            f(unsafe { std::mem::transmute::<u32, [u8; 4]>((self.0).0) }.as_ref())
+        } else {
+            println!("TMP: little");
+            f(unsafe { std::mem::transmute::<u32, [u8; 4]>((self.0).0.to_be()) }.as_ref())
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+
+    #[test]
+    fn test_block_number_str_10_ser() {
+        BlockNumberKeyV1(BlockNumber(35))
+            .as_bytes(|bytes| assert_eq!(bytes, &[48, 48, 48, 48, 48, 48, 48, 48, 51, 53]))
+    }
+
+    #[test]
+    fn block_number_key_v2() {
+        let k = BlockNumberKeyV2(BlockNumber(3));
+        k.as_bytes(|bytes| {
+            assert_eq!(bytes, &[0, 0, 0, 3]);
+        });
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/keys/blockstamp.rs b/rust-libs/duniter-dbs/src/keys/blockstamp.rs
new file mode 100644
index 0000000000000000000000000000000000000000..36cf85b1ccb65c075489b82244b5ba498d45e646
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/keys/blockstamp.rs
@@ -0,0 +1,61 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
+pub struct BlockstampKeyV1(Blockstamp);
+
+impl KeyAsBytes for BlockstampKeyV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        f(format!("{:010}-{}", self.0.number.0, self.0.hash).as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for BlockstampKeyV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let blockstamp_strs: ArrayVec<[&str; 2]> = std::str::from_utf8(bytes)
+            .map_err(|e| StringErr(format!("{}", e)))?
+            .split('-')
+            .collect();
+        let block_number = blockstamp_strs[0]
+            .parse()
+            .map_err(|e| StringErr(format!("{}", e)))?;
+        let block_hash =
+            Hash::from_hex(blockstamp_strs[1]).map_err(|e| StringErr(format!("{}", e)))?;
+        Ok(BlockstampKeyV1(Blockstamp {
+            number: BlockNumber(block_number),
+            hash: BlockHash(block_hash),
+        }))
+    }
+}
+
+impl ToDumpString for BlockstampKeyV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableKey for BlockstampKeyV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_string(&self) -> KvResult<String> {
+        Ok(format!("{}", self.0))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/keys/hash.rs b/rust-libs/duniter-dbs/src/keys/hash.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a3a11f8b0c6a47f1e069960aca8ac90fd5eca7dd
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/keys/hash.rs
@@ -0,0 +1,52 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
+pub struct HashKeyV1(pub Hash);
+
+impl KeyAsBytes for HashKeyV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        f(self.0.to_hex().as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for HashKeyV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let hash_str = std::str::from_utf8(bytes).map_err(|e| StringErr(format!("{}", e)))?;
+        Ok(HashKeyV1(
+            Hash::from_hex(&hash_str).map_err(|e| StringErr(format!("{}", e)))?,
+        ))
+    }
+}
+
+impl ToDumpString for HashKeyV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableKey for HashKeyV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_string(&self) -> KvResult<String> {
+        self.as_bytes(|bytes| Ok(unsafe { std::str::from_utf8_unchecked(bytes) }.to_owned()))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/keys/pubkey.rs b/rust-libs/duniter-dbs/src/keys/pubkey.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9476cca71a0baa350f0d3b892ca7825d77c0d07d
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/keys/pubkey.rs
@@ -0,0 +1,80 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
+pub struct PubKeyKeyV1(pub PublicKey);
+
+impl PubKeyKeyV1 {
+    const ALL: &'static str = "ALL";
+    const ALL_WITH_LEADING_1: &'static str = "11111111111111111111111111111ALL";
+
+    pub fn all() -> Self {
+        Self(PublicKey::from_base58(Self::ALL).expect("invalid PubKeyKeyV1::all()"))
+    }
+}
+
+impl KeyAsBytes for PubKeyKeyV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        let b58_string = self.0.to_base58();
+        if b58_string == Self::ALL_WITH_LEADING_1 {
+            f(Self::ALL.as_bytes())
+        } else {
+            f(self.0.to_base58().as_bytes())
+        }
+    }
+}
+
+impl kv_typed::prelude::FromBytes for PubKeyKeyV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let pubkey_str = std::str::from_utf8(bytes).map_err(|e| StringErr(format!("{}", e)))?;
+        Ok(PubKeyKeyV1(PublicKey::from_base58(&pubkey_str).map_err(
+            |e| StringErr(format!("{}: {}", e, pubkey_str)),
+        )?))
+    }
+}
+
+impl ToDumpString for PubKeyKeyV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+    #[test]
+    fn pubkey_all() {
+        let all = PubKeyKeyV1::all();
+        assert_eq!(
+            all.as_bytes(|bytes| bytes.to_vec()),
+            PubKeyKeyV1::ALL.as_bytes()
+        )
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableKey for PubKeyKeyV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_string(&self) -> KvResult<String> {
+        self.as_bytes(|bytes| Ok(unsafe { std::str::from_utf8_unchecked(bytes) }.to_owned()))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/keys/pubkey_and_sig.rs b/rust-libs/duniter-dbs/src/keys/pubkey_and_sig.rs
new file mode 100644
index 0000000000000000000000000000000000000000..65934750d8e4aee2e073d9cc2aa6a876788fd104
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/keys/pubkey_and_sig.rs
@@ -0,0 +1,69 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub struct PubKeyAndSigV1(PublicKey, Signature);
+
+impl PubKeyAndSigV1 {
+    pub fn all() -> Self {
+        Self(PublicKey::default(), Signature([0u8; 64]))
+    }
+}
+
+impl KeyAsBytes for PubKeyAndSigV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        if self == &Self::all() {
+            f(b"ALL")
+        } else {
+            f(format!("{}:{}", self.0.to_base58(), self.1.to_base64()).as_bytes())
+        }
+    }
+}
+
+impl kv_typed::prelude::FromBytes for PubKeyAndSigV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let raw_str = std::str::from_utf8(bytes).map_err(|e| StringErr(format!("{}", e)))?;
+        if raw_str == "ALL" {
+            Ok(PubKeyAndSigV1::all())
+        } else {
+            let array_str: ArrayVec<[&str; 2]> = raw_str.split(':').collect();
+            let pubkey =
+                PublicKey::from_base58(array_str[0]).map_err(|e| StringErr(format!("{}", e)))?;
+            let sig =
+                Signature::from_base64(array_str[1]).map_err(|e| StringErr(format!("{}", e)))?;
+            Ok(PubKeyAndSigV1(pubkey, sig))
+        }
+    }
+}
+
+impl ToDumpString for PubKeyAndSigV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableKey for PubKeyAndSigV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_string(&self) -> KvResult<String> {
+        self.as_bytes(|bytes| Ok(unsafe { std::str::from_utf8_unchecked(bytes) }.to_owned()))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/keys/source_key.rs b/rust-libs/duniter-dbs/src/keys/source_key.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1c1e781c2863f047c5f1000d28db60729b3c0d33
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/keys/source_key.rs
@@ -0,0 +1,91 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
+pub struct SourceKeyV1 {
+    pub tx_hash: Hash,
+    pub pos: u32,
+    pub consumed: Option<bool>,
+}
+
+impl ToString for SourceKeyV1 {
+    fn to_string(&self) -> String {
+        format!(
+            "{}-{:010}{}",
+            self.tx_hash,
+            self.pos,
+            match self.consumed {
+                Some(true) => "-1",
+                Some(false) => "-0",
+                None => "",
+            }
+        )
+    }
+}
+
+impl KeyAsBytes for SourceKeyV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        f(self.to_string().as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for SourceKeyV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let strs: ArrayVec<[&str; 3]> = std::str::from_utf8(bytes)
+            .map_err(|e| StringErr(format!("{}", e)))?
+            .split('-')
+            .collect();
+        let tx_hash = Hash::from_hex(strs[0]).map_err(|e| StringErr(format!("{}", e)))?;
+        let pos = strs[1].parse().map_err(|e| StringErr(format!("{}", e)))?;
+        let consumed = if strs.len() <= 2 {
+            None
+        } else {
+            match strs[2] {
+                "1" => Some(true),
+                "0" => Some(false),
+                _ => {
+                    return Err(StringErr(
+                        "invalid format: field consumed must be encoded with '0' or '1'".to_owned(),
+                    ))
+                }
+            }
+        };
+        Ok(SourceKeyV1 {
+            tx_hash,
+            pos,
+            consumed,
+        })
+    }
+}
+
+impl ToDumpString for SourceKeyV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableKey for SourceKeyV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_string(&self) -> KvResult<String> {
+        self.as_bytes(|bytes| Ok(unsafe { std::str::from_utf8_unchecked(bytes) }.to_owned()))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/keys/timestamp.rs b/rust-libs/duniter-dbs/src/keys/timestamp.rs
new file mode 100644
index 0000000000000000000000000000000000000000..691e0478468b88bc64f881d093976568fc9be109
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/keys/timestamp.rs
@@ -0,0 +1,58 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
+pub struct TimestampKeyV1(pub u64);
+
+impl KeyAsBytes for TimestampKeyV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        f(format!("{}", self.0).as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for TimestampKeyV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let key_str = std::str::from_utf8(bytes).map_err(|e| StringErr(format!("{}", e)))?;
+        Ok(TimestampKeyV1(
+            key_str
+                .parse()
+                .map_err(|e| StringErr(format!("{}: {}", e, key_str)))?,
+        ))
+    }
+}
+
+impl ToDumpString for TimestampKeyV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableKey for TimestampKeyV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        NaiveDateTime::parse_from_str(source, "%Y-%m-%d %H:%M:%S")
+            .map(|dt| TimestampKeyV1(dt.timestamp() as u64))
+            .map_err(|e| StringErr(format!("{}: {}", e, source)))
+    }
+    fn to_explorer_string(&self) -> KvResult<String> {
+        Ok(NaiveDateTime::from_timestamp(self.0 as i64, 0)
+            .format("%Y-%m-%d %H:%M:%S")
+            .to_string())
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/keys/uid.rs b/rust-libs/duniter-dbs/src/keys/uid.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7a481de4b16c722723570c117b29796ff5944ead
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/keys/uid.rs
@@ -0,0 +1,65 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+const USERNAME_MAX_LEN: usize = 100;
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
+pub struct UidKeyV1(pub ArrayString<[u8; USERNAME_MAX_LEN]>);
+
+impl KeyAsBytes for UidKeyV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        f(self.0.as_str().as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for UidKeyV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let uid_str = std::str::from_utf8(bytes).map_err(|e| StringErr(format!("{}", e)))?;
+        Ok(Self(
+            ArrayString::<[u8; USERNAME_MAX_LEN]>::from_str(uid_str)
+                .map_err(|e| StringErr(format!("{}", e)))?,
+        ))
+    }
+}
+
+impl FromStr for UidKeyV1 {
+    type Err = arrayvec::CapacityError;
+
+    fn from_str(source: &str) -> std::result::Result<Self, Self::Err> {
+        Ok(UidKeyV1(ArrayString::<[u8; USERNAME_MAX_LEN]>::from_str(
+            source,
+        )?))
+    }
+}
+
+impl ToDumpString for UidKeyV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableKey for UidKeyV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_string(&self) -> KvResult<String> {
+        self.as_bytes(|bytes| Ok(unsafe { std::str::from_utf8_unchecked(bytes) }.to_owned()))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/keys/wallet_conditions.rs b/rust-libs/duniter-dbs/src/keys/wallet_conditions.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9432eab0c8d9bc30539c75cb9a814dab4b0782c8
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/keys/wallet_conditions.rs
@@ -0,0 +1,65 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+const CONDITIONS_MAX_LEN: usize = 256;
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
+pub struct WalletConditionsV1(pub ArrayString<[u8; CONDITIONS_MAX_LEN]>);
+
+impl KeyAsBytes for WalletConditionsV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        f(self.0.as_str().as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for WalletConditionsV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let uid_str = std::str::from_utf8(bytes).map_err(|e| StringErr(format!("{}", e)))?;
+        Ok(Self(
+            ArrayString::<[u8; CONDITIONS_MAX_LEN]>::from_str(uid_str)
+                .map_err(|e| StringErr(format!("{}", e)))?,
+        ))
+    }
+}
+
+impl FromStr for WalletConditionsV1 {
+    type Err = arrayvec::CapacityError;
+
+    fn from_str(source: &str) -> std::result::Result<Self, Self::Err> {
+        Ok(WalletConditionsV1(
+            ArrayString::<[u8; CONDITIONS_MAX_LEN]>::from_str(source)?,
+        ))
+    }
+}
+
+impl ToDumpString for WalletConditionsV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableKey for WalletConditionsV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_string(&self) -> KvResult<String> {
+        self.as_bytes(|bytes| Ok(unsafe { std::str::from_utf8_unchecked(bytes) }.to_owned()))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/lib.rs b/rust-libs/duniter-dbs/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0a30b4ae2af2da1afe7284ed957e3b2e5d112bc9
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/lib.rs
@@ -0,0 +1,95 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+#![deny(
+    clippy::unwrap_used,
+    missing_copy_implementations,
+    trivial_casts,
+    trivial_numeric_casts,
+    unstable_features,
+    unused_import_braces
+)]
+
+mod bc_v1;
+mod errors;
+mod keys;
+mod values;
+
+// Re-export dependencies
+pub use arrayvec;
+pub use dubp_common;
+#[cfg(feature = "explorer")]
+pub use kv_typed::regex;
+pub use serde;
+pub use serde_json;
+pub use smallvec;
+
+// Re-export crates
+pub use kv_typed;
+
+// Prelude
+pub mod prelude {
+    pub use crate::errors::ErrorDb;
+    #[cfg(feature = "explorer")]
+    pub use kv_typed::explorer::{
+        DbExplorable, EntryFound, ExplorerAction, ExplorerActionResponse, ValueCaptures,
+    };
+}
+
+// Export technical types
+pub use crate::errors::Result;
+
+// Export profession types
+pub use bc_v1::{BcV1Db, BcV1DbReadable, BcV1DbRo, BcV1DbWritable, MainBlockEvent, UidEvent};
+pub use keys::all::AllKeyV1;
+pub use keys::block_number::BlockNumberKeyV1;
+pub use keys::blockstamp::BlockstampKeyV1;
+pub use keys::hash::HashKeyV1;
+pub use keys::pubkey::PubKeyKeyV1;
+pub use keys::pubkey_and_sig::PubKeyAndSigV1;
+pub use keys::source_key::SourceKeyV1;
+pub use keys::timestamp::TimestampKeyV1;
+pub use keys::uid::UidKeyV1;
+pub use keys::wallet_conditions::WalletConditionsV1;
+pub use values::block_db::{BlockDbEnum, BlockDbV1, TransactionInBlockDbV1};
+pub use values::block_head_db::BlockHeadDbV1;
+pub use values::block_number_array_db::BlockNumberArrayV1;
+pub use values::cindex_db::CIndexDbV1;
+pub use values::iindex_db::IIndexDbV1;
+pub use values::kick_db::KickDbV1;
+pub use values::mindex_db::MIndexDbV1;
+pub use values::pubkey_db::{PublicKeyArrayDbV1, PublicKeySingletonDbV1};
+pub use values::sindex_db::{SIndexDBV1, SourceKeyArrayDbV1};
+pub use values::ud_entry_db::{ConsumedUdDbV1, UdAmountDbV1, UdEntryDbV1};
+pub use values::wallet_db::WalletDbV1;
+
+// Crate imports
+pub(crate) use arrayvec::{ArrayString, ArrayVec};
+#[cfg(feature = "explorer")]
+use chrono::NaiveDateTime;
+pub(crate) use dubp_common::crypto::bases::b58::ToBase58 as _;
+pub(crate) use dubp_common::crypto::bases::BaseConversionError;
+pub(crate) use dubp_common::crypto::hashs::Hash;
+pub(crate) use dubp_common::crypto::keys::ed25519::{PublicKey, Signature};
+pub(crate) use dubp_common::crypto::keys::{PublicKey as _, Signature as _};
+pub(crate) use dubp_common::prelude::*;
+pub(crate) use kv_typed::prelude::*;
+pub(crate) use serde::{Deserialize, Serialize};
+pub(crate) use smallvec::SmallVec;
+pub(crate) use std::{fmt::Debug, iter::Iterator, str::FromStr};
+
+pub trait ToDumpString {
+    fn to_dump_string(&self) -> String;
+}
diff --git a/rust-libs/duniter-dbs/src/values.rs b/rust-libs/duniter-dbs/src/values.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a542fe0bccd32d08919bf6cc1f60d4e87838b9f9
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values.rs
@@ -0,0 +1,26 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+pub mod block_db;
+pub mod block_head_db;
+pub mod block_number_array_db;
+pub mod cindex_db;
+pub mod iindex_db;
+pub mod kick_db;
+pub mod mindex_db;
+pub mod pubkey_db;
+pub mod sindex_db;
+pub mod ud_entry_db;
+pub mod wallet_db;
diff --git a/rust-libs/duniter-dbs/src/values/block_db.rs b/rust-libs/duniter-dbs/src/values/block_db.rs
new file mode 100644
index 0000000000000000000000000000000000000000..eae51fae1efa17d42b85f981e2c364fa4ce8bb27
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values/block_db.rs
@@ -0,0 +1,117 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Debug)]
+pub enum BlockDbEnum {
+    BlockDbV1(BlockDbV1),
+}
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BlockDbV1 {
+    pub version: u64,
+    pub number: u64,
+    pub currency: String,
+    pub hash: String,
+    pub signature: String,
+    #[serde(rename = "inner_hash")]
+    pub inner_hash: String,
+    pub previous_hash: Option<String>,
+    pub issuer: String,
+    pub previous_issuer: Option<String>,
+    pub time: u64,
+    pub pow_min: u64,
+    #[serde(rename = "unitbase")]
+    pub unit_base: u64,
+    pub members_count: u64,
+    pub issuers_count: u64,
+    pub issuers_frame: u64,
+    pub issuers_frame_var: i64,
+    pub identities: Vec<String>,
+    pub joiners: Vec<String>,
+    pub actives: Vec<String>,
+    pub leavers: Vec<String>,
+    pub revoked: Vec<String>,
+    pub excluded: Vec<String>,
+    pub certifications: Vec<String>,
+    pub transactions: Vec<TransactionInBlockDbV1>,
+    pub median_time: u64,
+    pub nonce: u64,
+    pub fork: bool,
+    pub parameters: String,
+    pub monetary_mass: u64,
+    pub dividend: Option<u64>,
+    #[serde(rename = "UDTime")]
+    pub ud_time: Option<u64>,
+    #[serde(rename = "writtenOn")]
+    pub written_on: Option<u64>,
+    #[serde(rename = "written_on")]
+    pub written_on_str: String,
+    pub wrong: bool,
+}
+
+impl ValueAsBytes for BlockDbV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let json =
+            serde_json::to_string(self).map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(json.as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for BlockDbV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        Ok(serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?)
+    }
+}
+
+impl ToDumpString for BlockDbV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for BlockDbV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        serde_json::to_value(self).map_err(|e| KvError::DeserError(format!("{}", e)))
+    }
+}
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TransactionInBlockDbV1 {
+    version: u64,
+    currency: String,
+    #[serde(rename = "locktime")]
+    lock_time: u64,
+    hash: Option<String>,
+    blockstamp: String,
+    blockstamp_time: u64,
+    issuers: SmallVec<[String; 1]>,
+    inputs: SmallVec<[String; 4]>,
+    outputs: SmallVec<[String; 2]>,
+    unlocks: SmallVec<[String; 4]>,
+    signatures: SmallVec<[String; 1]>,
+    comment: String,
+}
diff --git a/rust-libs/duniter-dbs/src/values/block_head_db.rs b/rust-libs/duniter-dbs/src/values/block_head_db.rs
new file mode 100644
index 0000000000000000000000000000000000000000..99d101406c18a5defc58cfc4ce93c9528889671d
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values/block_head_db.rs
@@ -0,0 +1,86 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BlockHeadDbV1 {
+    pub version: u64,
+    pub currency: Option<String>,
+    #[serde(rename = "bsize")]
+    pub block_size: u64,
+    pub avg_block_size: u64,
+    pub ud_time: u64,
+    pub ud_reeval_time: u64,
+    pub mass_reeval: u64,
+    pub mass: u64,
+    pub hash: String,
+    pub previous_hash: Option<String>,
+    pub previous_issuer: Option<String>,
+    pub issuer: String,
+    pub time: u64,
+    pub median_time: u64,
+    pub number: u64,
+    pub pow_min: u64,
+    pub diff_number: u64,
+    pub issuers_count: u64,
+    pub issuers_frame: u64,
+    pub issuers_frame_var: i64,
+    pub issuer_diff: u64,
+    pub pow_zeros: u64,
+    pub pow_remainder: u64,
+    pub speed: f64,
+    pub unit_base: u64,
+    pub members_count: u64,
+    pub dividend: u64,
+    #[serde(rename = "new_dividend")]
+    pub new_dividend: Option<u64>,
+    pub issuer_is_member: bool,
+}
+
+impl ValueAsBytes for BlockHeadDbV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let json =
+            serde_json::to_string(self).map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(json.as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for BlockHeadDbV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        Ok(serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?)
+    }
+}
+
+impl ToDumpString for BlockHeadDbV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for BlockHeadDbV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        serde_json::to_value(self).map_err(|e| KvError::DeserError(format!("{}", e)))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/values/block_number_array_db.rs b/rust-libs/duniter-dbs/src/values/block_number_array_db.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dd4a97937f92f055b886320079cf0ca16280506e
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values/block_number_array_db.rs
@@ -0,0 +1,54 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Debug, Deserialize, PartialEq, Serialize)]
+pub struct BlockNumberArrayV1(pub SmallVec<[BlockNumber; 1]>);
+
+impl ValueAsBytes for BlockNumberArrayV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let json_string =
+            serde_json::to_string(self).map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(format!("[{}]", json_string).as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for BlockNumberArrayV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        //println!("json_str='{}'", &json_str);
+        Ok(serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?)
+    }
+}
+
+impl ToDumpString for BlockNumberArrayV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for BlockNumberArrayV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        serde_json::to_value(self).map_err(|e| KvError::DeserError(format!("{}", e)))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/values/cindex_db.rs b/rust-libs/duniter-dbs/src/values/cindex_db.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7afc8169fd37ea16a3b98b2d459aae8d95349b21
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values/cindex_db.rs
@@ -0,0 +1,99 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Debug, Deserialize, PartialEq, Serialize)]
+pub struct CIndexDbV1 {
+    pub received: SmallVec<[String; 10]>,
+    pub issued: Vec<CIndexLineDbV1>,
+}
+
+impl ValueAsBytes for CIndexDbV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let json_string =
+            serde_json::to_string(self).map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(format!("[{}]", json_string).as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for CIndexDbV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        //println!("json_str='{}'", &json_str);
+        Ok(serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?)
+    }
+}
+
+impl ToDumpString for CIndexDbV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for CIndexDbV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        serde_json::to_value(self).map_err(|e| KvError::DeserError(format!("{}", e)))
+    }
+}
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CIndexLineDbV1 {
+    pub op: String,
+    #[serde(rename = "writtenOn")]
+    pub written_on: Option<u64>,
+    #[serde(rename = "written_on")]
+    pub written_on_str: String,
+    pub issuer: String,
+    pub receiver: String,
+    #[serde(rename = "created_on")]
+    pub created_on: u64,
+    pub sig: Option<String>,
+    #[serde(rename = "chainable_on")]
+    pub chainable_on: Option<u64>,
+    #[serde(rename = "replayable_on")]
+    pub replayable_on: Option<u64>,
+    #[serde(rename = "expires_on")]
+    pub expires_on: Option<u64>,
+    #[serde(rename = "expired_on")]
+    pub expired_on: u64,
+    pub unchainables: Option<u64>,
+    pub age: Option<u64>,
+    pub stock: Option<u64>,
+    pub from_member: Option<bool>,
+    pub to_member: Option<bool>,
+    pub to_newcomer: Option<bool>,
+    pub to_leaver: Option<bool>,
+    pub is_replay: Option<bool>,
+    pub is_replayable: Option<bool>,
+    #[serde(rename = "sigOK")]
+    pub sig_ok: Option<bool>,
+    #[serde(rename = "created_on_ref")]
+    pub created_on_ref: Option<CreatedOnRef>,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CreatedOnRef {
+    pub median_time: u64,
+}
diff --git a/rust-libs/duniter-dbs/src/values/iindex_db.rs b/rust-libs/duniter-dbs/src/values/iindex_db.rs
new file mode 100644
index 0000000000000000000000000000000000000000..70a4ebc32d93225843fae3686882f1241a830f16
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values/iindex_db.rs
@@ -0,0 +1,82 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Debug, Deserialize, PartialEq, Serialize)]
+pub struct IIndexDbV1(pub SmallVec<[IIndexLineDbV1; 1]>);
+
+impl ValueAsBytes for IIndexDbV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let json_string =
+            serde_json::to_string(self).map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(format!("[{}]", json_string).as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for IIndexDbV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        //println!("json_str='{}'", &json_str);
+        Ok(serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?)
+    }
+}
+
+impl ToDumpString for IIndexDbV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for IIndexDbV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        serde_json::to_value(self).map_err(|e| KvError::DeserError(format!("{}", e)))
+    }
+}
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct IIndexLineDbV1 {
+    pub op: String,
+    #[serde(rename = "writtenOn")]
+    pub written_on: Option<u64>,
+    #[serde(rename = "written_on")]
+    pub written_on_str: String,
+    pub uid: Option<String>,
+    #[serde(rename = "pub")]
+    pub pubkey: String,
+    pub hash: Option<String>,
+    pub sig: Option<String>,
+    #[serde(rename = "created_on")]
+    pub created_on: Option<String>,
+    pub member: Option<bool>,
+    pub was_member: Option<bool>,
+    pub kick: Option<bool>,
+    #[serde(rename = "wotb_id")]
+    pub wotb_id: Option<usize>,
+    pub age: Option<u64>,
+    pub pub_unique: Option<bool>,
+    pub excluded_is_member: Option<bool>,
+    pub is_being_kicked: Option<bool>,
+    pub uid_unique: Option<bool>,
+    pub has_to_be_excluded: Option<bool>,
+}
diff --git a/rust-libs/duniter-dbs/src/values/kick_db.rs b/rust-libs/duniter-dbs/src/values/kick_db.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8d7c93f50694ab76dd47ea0bb9c86f5bb085d41c
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values/kick_db.rs
@@ -0,0 +1,56 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+pub struct KickDbV1 {
+    on: Option<u64>,          // The next time that the identity must be kicked
+    done: SmallVec<[u64; 4]>, // The reversion history
+}
+
+impl ValueAsBytes for KickDbV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let json =
+            serde_json::to_string(self).map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(json.as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for KickDbV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        Ok(serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?)
+    }
+}
+
+impl ToDumpString for KickDbV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for KickDbV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        serde_json::to_value(self).map_err(|e| KvError::DeserError(format!("{}", e)))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/values/mindex_db.rs b/rust-libs/duniter-dbs/src/values/mindex_db.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f65f856c460f061b85cac2cf085856a05ef52ce7
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values/mindex_db.rs
@@ -0,0 +1,106 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Debug, Deserialize, PartialEq, Serialize)]
+pub struct MIndexDbV1(pub SmallVec<[MIndexLineDbV1; 1]>);
+
+impl ValueAsBytes for MIndexDbV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let json_string =
+            serde_json::to_string(self).map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(format!("[{}]", json_string).as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for MIndexDbV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        //println!("json_str='{}'", &json_str);
+        Ok(serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?)
+    }
+}
+
+impl ToDumpString for MIndexDbV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for MIndexDbV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        serde_json::to_value(self).map_err(|e| KvError::DeserError(format!("{}", e)))
+    }
+}
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct MIndexLineDbV1 {
+    pub op: String,
+    #[serde(rename = "writtenOn")]
+    pub written_on: Option<u64>,
+    #[serde(rename = "written_on")]
+    pub written_on_str: String,
+    #[serde(rename = "pub")]
+    pub pubkey: String,
+    pub created_on: Option<String>,
+    #[serde(rename = "type")]
+    pub r#type: Option<String>,
+    #[serde(rename = "expires_on")]
+    pub expires_on: Option<u64>,
+    #[serde(rename = "expired_on")]
+    pub expired_on: Option<u64>,
+    pub revocation: Option<String>,
+    #[serde(rename = "revokes_on")]
+    pub revokes_on: Option<u64>,
+    #[serde(rename = "chainable_on")]
+    pub chainable_on: Option<u64>,
+    #[serde(rename = "revoked_on")]
+    pub revoked_on: Option<String>,
+    pub leaving: Option<bool>,
+    pub age: Option<u64>,
+    pub is_being_revoked: Option<bool>,
+    pub unchainables: Option<u64>,
+    pub number_following: Option<bool>,
+    #[serde(rename = "distanceOK")]
+    pub distance_ok: Option<bool>,
+    pub on_revoked: Option<bool>,
+    pub joins_twice: Option<bool>,
+    pub enough_certs: Option<bool>,
+    pub leaver_is_member: Option<bool>,
+    pub active_is_member: Option<bool>,
+    pub revoked_is_member: Option<bool>,
+    pub already_revoked: Option<bool>,
+    #[serde(rename = "revocationSigOK")]
+    pub revocation_sig_ok: Option<bool>,
+    #[serde(rename = "created_on_ref")]
+    pub created_on_ref: Option<BlockstampTimed>,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BlockstampTimed {
+    pub median_time: u64,
+    pub number: u32,
+    pub hash: String,
+}
diff --git a/rust-libs/duniter-dbs/src/values/pubkey_db.rs b/rust-libs/duniter-dbs/src/values/pubkey_db.rs
new file mode 100644
index 0000000000000000000000000000000000000000..78e750ef65a72939f504e007b9920e4e948d301c
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values/pubkey_db.rs
@@ -0,0 +1,109 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub struct PublicKeySingletonDbV1(pub PublicKey);
+
+impl ValueAsBytes for PublicKeySingletonDbV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        f(format!("[\"{}\"]", self.0).as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for PublicKeySingletonDbV1 {
+    type Err = BaseConversionError;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let mut pubkey_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+
+        pubkey_str = &pubkey_str[2..pubkey_str.len() - 2];
+        Ok(Self(PublicKey::from_base58(pubkey_str)?))
+    }
+}
+
+impl ToDumpString for PublicKeySingletonDbV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for PublicKeySingletonDbV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Ok(Self(
+            PublicKey::from_base58(source).map_err(|e| StringErr(format!("{}", e)))?,
+        ))
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        Ok(serde_json::Value::String(self.0.to_base58()))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
+pub struct PublicKeyArrayDbV1(pub SmallVec<[PublicKey; 8]>);
+
+impl ValueAsBytes for PublicKeyArrayDbV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let vec_pub_str = self
+            .0
+            .iter()
+            .map(|pubkey| pubkey.to_base58())
+            .collect::<SmallVec<[String; 8]>>();
+        let json = serde_json::to_string(&vec_pub_str)
+            .map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(json.as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for PublicKeyArrayDbV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        let vec_pub_str: SmallVec<[String; 8]> = serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?;
+        Ok(Self(
+            vec_pub_str
+                .into_iter()
+                .map(|pub_str| {
+                    PublicKey::from_base58(&pub_str).map_err(|e| StringErr(format!("{}", e)))
+                })
+                .collect::<std::result::Result<SmallVec<[PublicKey; 8]>, Self::Err>>()?,
+        ))
+    }
+}
+
+impl ToDumpString for PublicKeyArrayDbV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for PublicKeyArrayDbV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        Ok(serde_json::Value::Array(
+            self.0
+                .iter()
+                .map(|pubkey| serde_json::Value::String(pubkey.to_base58()))
+                .collect(),
+        ))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/values/sindex_db.rs b/rust-libs/duniter-dbs/src/values/sindex_db.rs
new file mode 100644
index 0000000000000000000000000000000000000000..27f7ed0db5db166e528893c03103b460b6c8573a
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values/sindex_db.rs
@@ -0,0 +1,128 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SIndexDBV1 {
+    pub src_type: String,
+    pub tx: Option<String>,
+    pub identifier: String,
+    pub pos: u32,
+    #[serde(rename = "created_on")]
+    pub created_on: Option<String>,
+    #[serde(rename = "written_time")]
+    pub written_time: u64,
+    #[serde(rename = "locktime")]
+    pub lock_time: u64,
+    pub unlock: Option<String>,
+    pub amount: u32,
+    pub base: u32,
+    pub conditions: String,
+    pub consumed: bool,
+    pub tx_obj: TransactionInBlockDbV1,
+    pub age: u64,
+    #[serde(rename = "type")]
+    pub type_: Option<String>,
+    pub available: Option<bool>,
+    pub is_locked: Option<bool>,
+    pub is_time_locked: Option<bool>,
+}
+
+impl ValueAsBytes for SIndexDBV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let json =
+            serde_json::to_string(self).map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(json.as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for SIndexDBV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        Ok(serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?)
+    }
+}
+
+impl ToDumpString for SIndexDBV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for SIndexDBV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        serde_json::to_value(self).map_err(|e| KvError::DeserError(format!("{}", e)))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
+pub struct SourceKeyArrayDbV1(pub SmallVec<[SourceKeyV1; 8]>);
+
+impl ValueAsBytes for SourceKeyArrayDbV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let vec_pub_str = self
+            .0
+            .iter()
+            .map(|source_key| source_key.to_string())
+            .collect::<SmallVec<[String; 8]>>();
+        let json = serde_json::to_string(&vec_pub_str)
+            .map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(json.as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for SourceKeyArrayDbV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        let vec_source_key_str: SmallVec<[String; 8]> = serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?;
+        Ok(Self(
+            vec_source_key_str
+                .into_iter()
+                .map(|source_key_str| {
+                    SourceKeyV1::from_bytes(source_key_str.as_bytes())
+                        .map_err(|e| StringErr(format!("{}", e)))
+                })
+                .collect::<std::result::Result<SmallVec<[SourceKeyV1; 8]>, Self::Err>>()?,
+        ))
+    }
+}
+
+impl ToDumpString for SourceKeyArrayDbV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for SourceKeyArrayDbV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        serde_json::to_value(self).map_err(|e| KvError::DeserError(format!("{}", e)))
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/values/ud_entry_db.rs b/rust-libs/duniter-dbs/src/values/ud_entry_db.rs
new file mode 100644
index 0000000000000000000000000000000000000000..13bf96baa25da7adba3ca00f946cfe2b4ecd684e
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values/ud_entry_db.rs
@@ -0,0 +1,80 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UdEntryDbV1 {
+    #[serde(rename = "pub")]
+    pub pubkey: String,
+    pub member: bool,
+    pub availables: Vec<u32>,
+    pub consumed: Vec<u32>,
+    #[serde(rename = "consumedUDs")]
+    pub consumed_uds: Vec<ConsumedUdDbV1>,
+    pub dividends: Vec<UdAmountDbV1>,
+}
+
+impl ValueAsBytes for UdEntryDbV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let json =
+            serde_json::to_string(self).map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(json.as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for UdEntryDbV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        Ok(serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?)
+    }
+}
+
+impl ToDumpString for UdEntryDbV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for UdEntryDbV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        serde_json::to_value(self).map_err(|e| KvError::DeserError(format!("{}", e)))
+    }
+}
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ConsumedUdDbV1 {
+    pub dividend_number: u32,
+    pub tx_hash: String,
+    pub tx_created_on: String,
+    #[serde(rename = "txLocktime")]
+    pub tx_lock_time: u32,
+    pub dividend: UdAmountDbV1,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
+pub struct UdAmountDbV1 {
+    pub amount: u32,
+    pub base: u32,
+}
diff --git a/rust-libs/duniter-dbs/src/values/wallet_db.rs b/rust-libs/duniter-dbs/src/values/wallet_db.rs
new file mode 100644
index 0000000000000000000000000000000000000000..12b7a3fa9caa3bad9cb6e55e577fbb1c1b8ded77
--- /dev/null
+++ b/rust-libs/duniter-dbs/src/values/wallet_db.rs
@@ -0,0 +1,56 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[derive(Debug, Deserialize, PartialEq, Serialize)]
+pub struct WalletDbV1 {
+    pub conditions: String,
+    pub balance: u64,
+}
+
+impl ValueAsBytes for WalletDbV1 {
+    fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> {
+        let json =
+            serde_json::to_string(self).map_err(|e| KvError::DeserError(format!("{}", e)))?;
+        f(json.as_bytes())
+    }
+}
+
+impl kv_typed::prelude::FromBytes for WalletDbV1 {
+    type Err = StringErr;
+
+    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> {
+        let json_str = std::str::from_utf8(bytes).expect("corrupted db : invalid utf8 bytes");
+        Ok(serde_json::from_str(&json_str)
+            .map_err(|e| StringErr(format!("{}: '{}'", e, json_str)))?)
+    }
+}
+
+impl ToDumpString for WalletDbV1 {
+    fn to_dump_string(&self) -> String {
+        todo!()
+    }
+}
+
+#[cfg(feature = "explorer")]
+impl ExplorableValue for WalletDbV1 {
+    fn from_explorer_str(source: &str) -> std::result::Result<Self, StringErr> {
+        Self::from_bytes(source.as_bytes())
+    }
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        serde_json::to_value(self).map_err(|e| KvError::DeserError(format!("{}", e)))
+    }
+}
diff --git a/rust-libs/duniter-dbs/tests/test_explorer.rs b/rust-libs/duniter-dbs/tests/test_explorer.rs
new file mode 100644
index 0000000000000000000000000000000000000000..96db3271e88cb795236f327ed34b4dad441d1872
--- /dev/null
+++ b/rust-libs/duniter-dbs/tests/test_explorer.rs
@@ -0,0 +1,254 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+#[cfg(feature = "explorer")]
+mod explorer {
+    use dubp_common::crypto::keys::ed25519::PublicKey;
+    use dubp_common::crypto::keys::PublicKey as _;
+    //use dubp_common::prelude::*;
+    use duniter_dbs::kv_typed::prelude::*;
+    use duniter_dbs::kv_typed::regex;
+    use duniter_dbs::prelude::*;
+    use duniter_dbs::smallvec::smallvec;
+    use duniter_dbs::{BcV1Db, BcV1DbWritable, PublicKeySingletonDbV1, Result, UidKeyV1};
+    use std::{num::NonZeroUsize, str::FromStr};
+    use tempdir::TempDir;
+    use unwrap::unwrap;
+
+    const COLLECTION_NAME: &str = "uids";
+
+    fn stringify_json_value_test(v: serde_json::Value) -> serde_json::Value {
+        v
+    }
+
+    #[test]
+    fn explorer_test_leveldb() -> Result<()> {
+        let tmp_dir = unwrap!(TempDir::new("explorer_test_leveldb"));
+
+        let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(tmp_dir.path().to_owned()))?;
+
+        explorer_test(&db)
+    }
+
+    #[test]
+    fn explorer_test_sled() -> Result<()> {
+        let db = BcV1Db::<Sled>::open(SledConf::new().temporary(true))?;
+
+        explorer_test(&db)
+    }
+
+    fn explorer_test<B: Backend>(db: &BcV1Db<B>) -> Result<()> {
+        // Defines test data
+        let k1 = unwrap!(UidKeyV1::from_str("toto"));
+        let k2 = unwrap!(UidKeyV1::from_str("titi"));
+        let v1 = PublicKeySingletonDbV1(unwrap!(PublicKey::from_base58(
+            "ByE9TU6qhktHYYVAqeTcWcaULBx151siQLyL3TrKvY85"
+        )));
+        let v2 = PublicKeySingletonDbV1(unwrap!(PublicKey::from_base58(
+            "8B5XCAHknsckCkMWeGF9FoGibSNZXF9HtAvzxzg3bSyp"
+        )));
+
+        // Insert test data
+        db.uids_write().upsert(k1, v1)?;
+        db.uids_write().upsert(k2, v2)?;
+
+        // Test action count
+        let res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Count,
+            stringify_json_value_test,
+        )?;
+        assert_eq!(Ok(ExplorerActionResponse::Count(2)), res);
+
+        // Test action get
+        let res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Get { key: "unexist" },
+            stringify_json_value_test,
+        )?;
+        assert_eq!(Ok(ExplorerActionResponse::Get(None)), res);
+        let res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Get { key: "toto" },
+            stringify_json_value_test,
+        )?;
+        assert_eq!(
+            Ok(ExplorerActionResponse::Get(Some(
+                serde_json::Value::String(
+                    "ByE9TU6qhktHYYVAqeTcWcaULBx151siQLyL3TrKvY85".to_owned()
+                )
+            ))),
+            res
+        );
+
+        // Test action put
+        let res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Put {
+                key: "titu",
+                value: "Bi6ECSc352gdfEvVzGiQuuDQyaTptHkcxooMGTJk14Tr",
+            },
+            stringify_json_value_test,
+        )?;
+        assert_eq!(Ok(ExplorerActionResponse::PutOk), res);
+        let res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Get { key: "titu" },
+            stringify_json_value_test,
+        )?;
+        assert_eq!(
+            Ok(ExplorerActionResponse::Get(Some(
+                serde_json::Value::String(
+                    "Bi6ECSc352gdfEvVzGiQuuDQyaTptHkcxooMGTJk14Tr".to_owned()
+                )
+            ))),
+            res
+        );
+        let res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Count,
+            stringify_json_value_test,
+        )?;
+        assert_eq!(Ok(ExplorerActionResponse::Count(3)), res);
+
+        // Test action find
+        let range_res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Find {
+                key_min: Some("ti00".to_owned()),
+                key_max: Some("tizz".to_owned()),
+                key_regex: None,
+                value_regex: None,
+                limit: Some(10),
+                reverse: false,
+                step: unsafe { NonZeroUsize::new_unchecked(1) },
+            },
+            stringify_json_value_test,
+        )?;
+        assert_eq!(
+            Ok(ExplorerActionResponse::Find(vec![
+                EntryFound {
+                    key: "titi".to_owned(),
+                    value: serde_json::Value::String(
+                        "8B5XCAHknsckCkMWeGF9FoGibSNZXF9HtAvzxzg3bSyp".to_owned()
+                    ),
+                    captures: None,
+                },
+                EntryFound {
+                    key: "titu".to_owned(),
+                    value: serde_json::Value::String(
+                        "Bi6ECSc352gdfEvVzGiQuuDQyaTptHkcxooMGTJk14Tr".to_owned()
+                    ),
+                    captures: None,
+                },
+            ])),
+            range_res
+        );
+
+        // Test action find with limit
+        let range_res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Find {
+                key_min: Some("ti00".to_owned()),
+                key_max: Some("tizz".to_owned()),
+                key_regex: None,
+                value_regex: None,
+                limit: Some(1),
+                reverse: false,
+                step: unsafe { NonZeroUsize::new_unchecked(1) },
+            },
+            stringify_json_value_test,
+        )?;
+        assert_eq!(
+            Ok(ExplorerActionResponse::Find(vec![EntryFound {
+                key: "titi".to_owned(),
+                value: serde_json::Value::String(
+                    "8B5XCAHknsckCkMWeGF9FoGibSNZXF9HtAvzxzg3bSyp".to_owned()
+                ),
+                captures: None,
+            }])),
+            range_res
+        );
+
+        // Test action find with limit and reverse
+        let range_res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Find {
+                key_min: Some("ti00".to_owned()),
+                key_max: Some("tizz".to_owned()),
+                key_regex: None,
+                value_regex: None,
+                limit: Some(1),
+                reverse: true,
+                step: unsafe { NonZeroUsize::new_unchecked(1) },
+            },
+            stringify_json_value_test,
+        )?;
+        assert_eq!(
+            Ok(ExplorerActionResponse::Find(vec![EntryFound {
+                key: "titu".to_owned(),
+                value: serde_json::Value::String(
+                    "Bi6ECSc352gdfEvVzGiQuuDQyaTptHkcxooMGTJk14Tr".to_owned()
+                ),
+                captures: None,
+            }])),
+            range_res
+        );
+
+        // Test action find with regex capture
+        let range_res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Find {
+                key_min: Some("ti00".to_owned()),
+                key_max: Some("tizz".to_owned()),
+                key_regex: None,
+                value_regex: Some(regex::Regex::new("(E[Cv])[A-Z]").expect("wrong regex")),
+                limit: Some(10),
+                reverse: false,
+                step: unsafe { NonZeroUsize::new_unchecked(1) },
+            },
+            stringify_json_value_test,
+        )?;
+        assert_eq!(
+            Ok(ExplorerActionResponse::Find(vec![EntryFound {
+                key: "titu".to_owned(),
+                value: serde_json::Value::String(
+                    "Bi6ECSc352gdfEvVzGiQuuDQyaTptHkcxooMGTJk14Tr".to_owned()
+                ),
+                captures: Some(ValueCaptures(smallvec![
+                    smallvec![Some("EC".to_owned())],
+                    smallvec![Some("Ev".to_owned())]
+                ])),
+            }])),
+            range_res
+        );
+
+        // Test action delete
+        let res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Delete { key: "toto" },
+            stringify_json_value_test,
+        )?;
+        assert_eq!(Ok(ExplorerActionResponse::DeleteOk), res);
+        let res = db.explore(
+            COLLECTION_NAME,
+            ExplorerAction::Get { key: "toto" },
+            stringify_json_value_test,
+        )?;
+        assert_eq!(Ok(ExplorerActionResponse::Get(None)), res);
+
+        Ok(())
+    }
+}
diff --git a/rust-libs/duniter-dbs/tests/test_read_write.rs b/rust-libs/duniter-dbs/tests/test_read_write.rs
new file mode 100644
index 0000000000000000000000000000000000000000..480c4b6b89dd41b74b113e2b0b423a4984a68f5f
--- /dev/null
+++ b/rust-libs/duniter-dbs/tests/test_read_write.rs
@@ -0,0 +1,350 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use dubp_common::crypto::keys::ed25519::PublicKey;
+use dubp_common::crypto::keys::PublicKey as _;
+use dubp_common::prelude::*;
+use duniter_dbs::kv_typed::prelude::*;
+use duniter_dbs::{
+    BcV1Db, BcV1DbReadable, BcV1DbWritable, BlockDbV1, BlockNumberKeyV1, MainBlockEvent,
+    PublicKeySingletonDbV1, Result, UidKeyV1,
+};
+use kv_typed::channel::TryRecvError;
+use std::str::FromStr;
+use tempdir::TempDir;
+use unwrap::unwrap;
+
+#[test]
+fn write_read_delete_b0_leveldb() -> Result<()> {
+    let tmp_dir = unwrap!(TempDir::new("write_read_delete_b0_leveldb"));
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(tmp_dir.path().to_owned()))?;
+
+    write_read_delete_b0_test(&db)
+}
+
+#[test]
+fn write_read_delete_b0_sled() -> Result<()> {
+    let db = BcV1Db::<Sled>::open(SledConf::new().temporary(true))?;
+
+    write_read_delete_b0_test(&db)
+}
+
+#[test]
+fn iter_test_leveldb() -> Result<()> {
+    let tmp_dir = unwrap!(TempDir::new("batch_test_leveldb"));
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(tmp_dir.path().to_owned()))?;
+
+    write_some_entries_and_iter(&db)
+}
+
+#[test]
+fn iter_test_mem() -> Result<()> {
+    let db = BcV1Db::<Mem>::open(MemConf::default())?;
+
+    write_some_entries_and_iter(&db)
+}
+
+#[test]
+fn iter_test_sled() -> Result<()> {
+    let db = BcV1Db::<Sled>::open(SledConf::new().temporary(true))?;
+
+    write_some_entries_and_iter(&db)
+}
+
+#[test]
+fn batch_test_leveldb() -> Result<()> {
+    let tmp_dir = unwrap!(TempDir::new("batch_test_leveldb"));
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(tmp_dir.path().to_owned()))?;
+
+    batch_test(&db)
+}
+
+#[test]
+fn batch_test_mem() -> Result<()> {
+    let db = BcV1Db::<Mem>::open(MemConf::default())?;
+
+    batch_test(&db)
+}
+
+#[test]
+fn batch_test_sled() -> Result<()> {
+    let db = BcV1Db::<Sled>::open(SledConf::new().temporary(true))?;
+
+    batch_test(&db)
+}
+
+fn write_read_delete_b0_test<B: Backend>(db: &BcV1Db<B>) -> Result<()> {
+    let main_blocks_reader = db.main_blocks();
+
+    let (subscriber, events_recv) = kv_typed::channel::unbounded();
+
+    main_blocks_reader.subscribe(subscriber)?;
+
+    // Empty db
+    assert_eq!(
+        main_blocks_reader.get(&BlockNumberKeyV1(BlockNumber(0)))?,
+        None
+    );
+    assert_eq!(
+        main_blocks_reader.get(&BlockNumberKeyV1(BlockNumber(1)))?,
+        None
+    );
+    assert_eq!(main_blocks_reader.iter(..).keys().next_res()?, None);
+    assert_eq!(main_blocks_reader.iter(..).values().next_res()?, None);
+    if let Err(TryRecvError::Empty) = events_recv.try_recv() {
+    } else {
+        panic!("should not receive event");
+    }
+
+    // Insert b0
+    let b0 = BlockDbV1::default();
+    let main_blocks_writer = db.main_blocks_write();
+    main_blocks_writer.upsert(BlockNumberKeyV1(BlockNumber(0)), b0.clone())?;
+    assert_eq!(
+        main_blocks_reader
+            .get(&BlockNumberKeyV1(BlockNumber(0)))?
+            .as_ref(),
+        Some(&b0)
+    );
+    assert_eq!(
+        main_blocks_reader.get(&BlockNumberKeyV1(BlockNumber(1)))?,
+        None
+    );
+    let mut keys_iter = main_blocks_reader.iter(..).keys();
+    assert_eq!(
+        keys_iter.next_res()?,
+        Some(BlockNumberKeyV1(BlockNumber(0)))
+    );
+    assert_eq!(keys_iter.next_res()?, None);
+    let mut values_iter = main_blocks_reader.iter(..).values();
+    assert_eq!(values_iter.next_res()?, Some(b0.clone()));
+    assert_eq!(values_iter.next_res()?, None);
+    if let Ok(events) = events_recv.try_recv() {
+        assert_eq!(events.len(), 1);
+        let event = &events[0];
+        assert_eq!(
+            event,
+            &MainBlockEvent::Upsert {
+                key: BlockNumberKeyV1(BlockNumber(0)),
+                value: b0,
+            },
+        );
+    } else {
+        panic!("should receive event");
+    }
+
+    // Delete b0
+    main_blocks_writer.remove(BlockNumberKeyV1(BlockNumber(0)))?;
+    assert_eq!(
+        main_blocks_reader.get(&BlockNumberKeyV1(BlockNumber(0)))?,
+        None
+    );
+    assert_eq!(
+        main_blocks_reader.get(&BlockNumberKeyV1(BlockNumber(1)))?,
+        None
+    );
+    assert_eq!(main_blocks_reader.iter(..).keys().next_res()?, None);
+    assert_eq!(main_blocks_reader.iter(..).values().next_res()?, None);
+    if let Ok(events) = events_recv.try_recv() {
+        assert_eq!(events.len(), 1);
+        let event = &events[0];
+        assert_eq!(
+            event,
+            &MainBlockEvent::Remove {
+                key: BlockNumberKeyV1(BlockNumber(0)),
+            },
+        );
+    } else {
+        panic!("should receive event");
+    }
+
+    Ok(())
+}
+
+fn write_some_entries_and_iter<B: Backend>(db: &BcV1Db<B>) -> Result<()> {
+    let k1 = unwrap!(UidKeyV1::from_str("titi"));
+    let p1 = PublicKeySingletonDbV1(unwrap!(PublicKey::from_base58(
+        "42jMJtb8chXrpHMAMcreVdyPJK7LtWjEeRqkPw4eSEVp"
+    )));
+    let k2 = unwrap!(UidKeyV1::from_str("titu"));
+    let p2 = PublicKeySingletonDbV1(unwrap!(PublicKey::from_base58(
+        "D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3C"
+    )));
+    let k3 = unwrap!(UidKeyV1::from_str("toto"));
+    let p3 = PublicKeySingletonDbV1(unwrap!(PublicKey::from_base58(
+        "8B5XCAHknsckCkMWeGF9FoGibSNZXF9HtAvzxzg3bSyp"
+    )));
+    let uids_writer = db.uids_write();
+    uids_writer.upsert(k1, p1)?;
+    uids_writer.upsert(k2, p2)?;
+    uids_writer.upsert(k3, p3)?;
+
+    let uids_reader = db.uids();
+    {
+        let mut values_iter_step_2 = uids_reader.iter(..).values().step_by(2);
+        assert_eq!(Some(p1), values_iter_step_2.next_res()?);
+        assert_eq!(Some(p3), values_iter_step_2.next_res()?);
+        assert_eq!(None, values_iter_step_2.next_res()?);
+
+        let mut entries_iter_step_2 = uids_reader.iter(..).step_by(2);
+        assert_eq!(Some((k1, p1)), entries_iter_step_2.next_res()?);
+        assert_eq!(Some((k3, p3)), entries_iter_step_2.next_res()?);
+        assert_eq!(None, entries_iter_step_2.next_res()?);
+
+        let mut entries_iter = uids_reader.iter(k2..);
+        assert_eq!(Some((k2, p2)), entries_iter.next_res()?);
+        assert_eq!(Some((k3, p3)), entries_iter.next_res()?);
+        assert_eq!(None, entries_iter.next_res()?);
+
+        let mut entries_iter = uids_reader.iter(..=k2);
+        assert_eq!(Some((k1, p1)), entries_iter.next_res()?);
+        assert_eq!(Some((k2, p2)), entries_iter.next_res()?);
+        assert_eq!(None, entries_iter.next_res()?);
+
+        let mut entries_iter_rev = uids_reader.iter(k2..).reverse();
+        assert_eq!(Some((k3, p3)), entries_iter_rev.next_res()?);
+        assert_eq!(Some((k2, p2)), entries_iter_rev.next_res()?);
+        assert_eq!(None, entries_iter_rev.next_res()?);
+
+        let mut entries_iter_rev = uids_reader.iter(..=k2).reverse();
+        assert_eq!(Some((k2, p2)), entries_iter_rev.next_res()?);
+        assert_eq!(Some((k1, p1)), entries_iter_rev.next_res()?);
+        assert_eq!(None, entries_iter.next_res()?);
+
+        let mut keys_iter_rev = uids_reader.iter(..=k2).keys().reverse();
+        assert_eq!(Some(k2), keys_iter_rev.next_res()?);
+        assert_eq!(Some(k1), keys_iter_rev.next_res()?);
+        assert_eq!(None, keys_iter_rev.next_res()?);
+    }
+
+    uids_writer.remove(k3)?;
+
+    let mut keys_iter_rev = uids_reader.iter(..).keys();
+    assert_eq!(Some(k1), keys_iter_rev.next_res()?);
+    assert_eq!(Some(k2), keys_iter_rev.next_res()?);
+    assert_eq!(None, keys_iter_rev.next_res()?);
+
+    Ok(())
+}
+
+fn batch_test<B: Backend>(db: &BcV1Db<B>) -> Result<()> {
+    let main_blocks_reader = db.main_blocks();
+
+    let mut batch = db.new_batch();
+
+    let (subscriber, events_recv) = kv_typed::channel::unbounded();
+
+    main_blocks_reader.subscribe(subscriber)?;
+
+    // Empty db
+    assert_eq!(
+        main_blocks_reader.get(&BlockNumberKeyV1(BlockNumber(0)))?,
+        None
+    );
+    assert_eq!(
+        main_blocks_reader.get(&BlockNumberKeyV1(BlockNumber(1)))?,
+        None
+    );
+    assert_eq!(main_blocks_reader.iter(..).keys().next_res()?, None);
+    assert_eq!(main_blocks_reader.iter(..).values().next_res()?, None);
+    if let Err(TryRecvError::Empty) = events_recv.try_recv() {
+    } else {
+        panic!("should not receive event");
+    }
+
+    // Insert b0 in batch
+    let b0 = BlockDbV1::default();
+    batch
+        .main_blocks()
+        .upsert(BlockNumberKeyV1(BlockNumber(0)), b0.clone());
+
+    // bo should written in batch
+    assert_eq!(
+        batch.main_blocks().get(&BlockNumberKeyV1(BlockNumber(0))),
+        Some(&b0)
+    );
+
+    // bo should not written in db
+    assert_eq!(
+        db.main_blocks().get(&BlockNumberKeyV1(BlockNumber(0)))?,
+        None
+    );
+
+    if let Err(TryRecvError::Empty) = events_recv.try_recv() {
+    } else {
+        panic!("should not receive event");
+    }
+
+    // Insert b1 in batch
+    let b1 = BlockDbV1 {
+        number: 1,
+        ..Default::default()
+    };
+    batch
+        .main_blocks()
+        .upsert(BlockNumberKeyV1(BlockNumber(1)), b1.clone());
+
+    // Write batch in db
+    db.write_batch(batch)?;
+
+    // bo should written in db
+    assert_eq!(
+        db.main_blocks()
+            .get(&BlockNumberKeyV1(BlockNumber(0)))?
+            .as_ref(),
+        Some(&b0)
+    );
+    let mut keys_iter = db.main_blocks().iter(..).keys();
+    assert_eq!(
+        keys_iter.next_res()?,
+        Some(BlockNumberKeyV1(BlockNumber(0)))
+    );
+    assert_eq!(
+        keys_iter.next_res()?,
+        Some(BlockNumberKeyV1(BlockNumber(1)))
+    );
+    assert_eq!(keys_iter.next_res()?, None);
+    let mut values_iter = db.main_blocks().iter(..).values();
+    assert_eq!(values_iter.next_res()?.as_ref(), Some(&b0));
+    assert_eq!(values_iter.next_res()?.as_ref(), Some(&b1));
+    assert_eq!(values_iter.next_res()?, None);
+    if let Ok(events) = events_recv.try_recv() {
+        assert_eq!(events.len(), 2);
+        assert!(assert_eq_pairs(
+            [&events[0], &events[1]],
+            [
+                &MainBlockEvent::Upsert {
+                    key: BlockNumberKeyV1(BlockNumber(0)),
+                    value: b0,
+                },
+                &MainBlockEvent::Upsert {
+                    key: BlockNumberKeyV1(BlockNumber(1)),
+                    value: b1,
+                }
+            ]
+        ));
+    } else {
+        panic!("should receive event");
+    }
+
+    Ok(())
+}
+
+fn assert_eq_pairs<T: PartialEq>(a: [T; 2], b: [T; 2]) -> bool {
+    (a[0] == b[0] && a[1] == b[1]) || (a[1] == b[0] && a[0] == b[1])
+}
diff --git a/rust-libs/duniter-dbs/tests/test_tmp_real.rs b/rust-libs/duniter-dbs/tests/test_tmp_real.rs
new file mode 100644
index 0000000000000000000000000000000000000000..00b2c81cd1e4b353deaadb97c440706e5411c69c
--- /dev/null
+++ b/rust-libs/duniter-dbs/tests/test_tmp_real.rs
@@ -0,0 +1,736 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use dubp_common::crypto::bases::b58::ToBase58 as _;
+use dubp_common::crypto::hashs::Hash;
+use dubp_common::crypto::keys::PublicKey;
+use dubp_common::prelude::*;
+use duniter_dbs::kv_typed::prelude::*;
+use duniter_dbs::*;
+use duniter_dbs::{
+    BcV1Db, BcV1DbReadable, BcV1DbWritable, BlockDbV1, BlockNumberKeyV1, PublicKeySingletonDbV1,
+    Result, UidKeyV1,
+};
+use once_cell::sync::Lazy;
+use std::{path::PathBuf, str::FromStr, sync::Mutex};
+use unwrap::unwrap;
+
+// Empty mutex used to ensure that only one test runs at a time
+static MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
+
+//const DB_PATH: &str = "/home/elois/.config/duniter/duniter_default/data";
+const DB_PATH: &str = "/home/elois/Documents/ml/leveldb-archives/g1-317499/leveldb";
+
+#[test]
+fn db_v1_main_blocks__() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    //let block52 = db.get::<MainBlocksColV1>(&MainBlockKeyV1(52))?;
+    //println!("{:#?}", block52);
+
+    let current_block_number_opt = db
+        .main_blocks()
+        .iter(..)
+        .keys()
+        .reverse()
+        .next()
+        .transpose()?;
+    if let Some(current_block_number) = current_block_number_opt {
+        println!("current_block_number={:#?}", current_block_number);
+        let current_block = db.main_blocks().get(&current_block_number)?;
+        println!("current_block={:#?}", current_block);
+    }
+
+    /*// Collect all main blocks
+    let entries = db
+        .main_blocks()
+        .iter(..)
+        .collect::<KvResult<Vec<(BlockNumberKeyV1, BlockDbV1)>>>()?;
+    println!("entries_len={}", entries.len());*/
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_main_blocks_idty() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let all = unwrap!(db.mb_idty().get(&PubKeyKeyV1::all())?);
+    assert!(all.0.len() > 2);
+
+    // Collect all main blocks idty
+    let entries = db
+        .mb_idty()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, BlockNumberArrayV1)>>>()?;
+    println!("identities_count={}", entries.len() - 1);
+    for (k, v) in &entries {
+        if v.0.len() == 2 {
+            println!("{:?}", k.0.as_ref());
+        }
+    }
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_main_blocks_certs() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all main blocks idty
+    let entries = db
+        .mb_certs()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, BlockNumberArrayV1)>>>()?;
+    println!("certifications_count={}", entries.len() - 1);
+    for (k, v) in &entries[..10] {
+        if v.0.len() > 1 {
+            println!("{}={:?}", k.0, v.0);
+        }
+    }
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_main_blocks_joiners() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let all = unwrap!(db.mb_joiners().get(&PubKeyKeyV1::all())?);
+    assert!(all.0.len() > 100);
+
+    // Collect all main blocks joiners
+    let entries = db
+        .mb_joiners()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, BlockNumberArrayV1)>>>()?;
+    println!("joiners_count={}", entries.len() - 1);
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_main_blocks_actives() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let all = unwrap!(db.mb_actives().get(&PubKeyKeyV1::all())?);
+    assert!(all.0.len() > 100);
+
+    // Collect all main blocks actives
+    let entries = db
+        .mb_actives()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, BlockNumberArrayV1)>>>()?;
+    println!("actives_count={}", entries.len() - 1);
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_main_blocks_leavers() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let all = unwrap!(db.mb_leavers().get(&PubKeyKeyV1::all())?);
+    assert!(all.0.len() >= 3);
+
+    // Collect all main blocks with leavers
+    let entries = db
+        .mb_leavers()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, BlockNumberArrayV1)>>>()?;
+    println!("leavers_count={}", entries.len() - 1);
+    for (k, v) in entries {
+        println!("{}={:?}", k.0, v.0);
+    }
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_main_blocks_excluded() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let all = unwrap!(db.mb_excluded().get(&PubKeyKeyV1::all())?);
+    assert!(all.0.len() >= 50);
+
+    // Collect all main blocks with excluded
+    let entries = db
+        .mb_excluded()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, BlockNumberArrayV1)>>>()?;
+    println!("excluded_count={}", entries.len() - 1);
+    /*for (k, v) in entries {
+        println!("{}={:?}", k.0, v.0);
+    }*/
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_main_blocks_revoked() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let all = unwrap!(db.mb_revoked().get(&PubKeyAndSigV1::all())?);
+    assert!(all.0.len() >= 20);
+
+    // Collect all main blocks with revoked
+    let entries = db
+        .mb_revoked()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyAndSigV1, BlockNumberArrayV1)>>>()?;
+    println!("revoked_count={}", entries.len() - 1);
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_main_blocks_dividend() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let all = unwrap!(db.mb_dividends().get(&AllKeyV1)?);
+    assert!(all.0.len() >= 900);
+    println!("blocks with dividend={}", all.0.len());
+    println!("last block with dividend={:?}", all.0.last());
+
+    // Collect all main blocks with dividends
+    let entries = db
+        .mb_dividends()
+        .iter(..)
+        .collect::<KvResult<Vec<(AllKeyV1, BlockNumberArrayV1)>>>()?;
+    println!("dividends_keys={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_main_blocks_transactions() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let all = unwrap!(db.mb_transactions().get(&AllKeyV1)?);
+    assert!(all.0.len() >= 900);
+    println!("blocks with tx={}", all.0.len());
+    println!("last block with tx={:?}", all.0.last());
+
+    // Collect all main blocks with transactions
+    let entries = db
+        .mb_transactions()
+        .iter(..)
+        .collect::<KvResult<Vec<(AllKeyV1, BlockNumberArrayV1)>>>()?;
+    println!("transactions_keys={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_fork_blocks() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    /*let fork_blocks_keys = db
+        .keys_iter::<ForkBlocksColV1, _>(..)
+        .take(1)
+        .collect::<KvResult<Vec<BlockstampKeyV1>>>()?;
+    let one_fork_block = unwrap!(db.get::<ForkBlocksColV1>(&fork_blocks_keys[0])?);
+
+    println!("{:#?}", one_fork_block);*/
+
+    // Collect all fork blocks
+    let entries = db
+        .fork_blocks()
+        .iter(..)
+        .collect::<KvResult<Vec<(BlockstampKeyV1, BlockDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_bindex() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all bindex entries
+    let entries = db
+        .bindex()
+        .iter(..)
+        .collect::<KvResult<Vec<(BlockNumberKeyV1, BlockHeadDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+    //println!("last_bindex={:?}", entries.last());
+    //for (_k, v) in entries {}
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_iindex__() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let iindex_keys = db
+        .iindex()
+        .iter(..)
+        .keys()
+        .take(1)
+        .collect::<KvResult<Vec<PubKeyKeyV1>>>()?;
+    let one_iindex_db = unwrap!(db.iindex().get(&iindex_keys[0])?);
+    assert_eq!(one_iindex_db.0[0].pubkey, iindex_keys[0].0.to_base58());
+
+    //println!("{:#?}", one_iindex_db);
+
+    if let Some(ref hash) = one_iindex_db.0[0].hash {
+        let pubkey = unwrap!(db
+            .iindex_hash()
+            .get(&HashKeyV1(unwrap!(Hash::from_hex(hash))))?);
+        assert_eq!(pubkey.0, iindex_keys[0].0);
+    }
+
+    // Count iindex entries
+    let count = db.iindex().count()?;
+    println!("iindex size={}", count);
+
+    // Count members
+    let count_members = db
+        .iindex()
+        .iter(..)
+        .filter_map(KvResult::ok)
+        .filter(|(_k, v)| v.0[0].member.is_some() && unwrap!(v.0[0].member))
+        .count();
+    println!("count_members={}", count_members);
+
+    // Collect all iindex entries
+    let entries = db
+        .iindex()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, IIndexDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_iindex_hash() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let iindex_entries = db
+        .iindex_hash()
+        .iter(..)
+        .take(3)
+        .collect::<KvResult<Vec<(HashKeyV1, PublicKeySingletonDbV1)>>>()?;
+
+    println!(
+        "(hash, pub)=({:#?},{:#?})",
+        iindex_entries[0].0, iindex_entries[0].1
+    );
+
+    // Collect all iindex/hash entries
+    let entries = db
+        .iindex_hash()
+        .iter(..)
+        .collect::<KvResult<Vec<(HashKeyV1, PublicKeySingletonDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_iindex_kick() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let entries = db
+        .iindex_kick()
+        .iter(..)
+        .take(3)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, KickDbV1)>>>()?;
+
+    println!("(pub, kick)=({:#?},{:#?})", entries[0].0, entries[0].1);
+
+    // Collect all iindex/kick entries
+    let entries = db
+        .iindex_kick()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, KickDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_iindex_written_on() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all iindex/written_on entries
+    let entries = db
+        .iindex_written_on()
+        .iter(..)
+        .collect::<KvResult<Vec<(BlockNumberKeyV1, PublicKeyArrayDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+    println!("entries={:?}", entries);
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_uid_col() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let uid_keys = db
+        .uids()
+        .iter(..)
+        .keys()
+        .take(1)
+        .collect::<KvResult<Vec<UidKeyV1>>>()?;
+    let one_pubkey_db = db.uids().get(&uid_keys[0])?;
+
+    println!(
+        "(uid, pubkey) = ({}, {:#?})",
+        uid_keys[0].0.as_str(),
+        one_pubkey_db
+    );
+
+    let start_key = unwrap!(UidKeyV1::from_str("1b"));
+    let end_key = unwrap!(UidKeyV1::from_str("404_not_found"));
+    let uid_index = db
+        .uids()
+        .iter(start_key..end_key)
+        .collect::<KvResult<Vec<(UidKeyV1, PublicKeySingletonDbV1)>>>()?;
+    assert_eq!(
+        uid_index,
+        vec![(
+            unwrap!(UidKeyV1::from_str("1claude1")),
+            PublicKeySingletonDbV1(unwrap!(PublicKey::from_base58(
+                "8B5XCAHknsckCkMWeGF9FoGibSNZXF9HtAvzxzg3bSyp"
+            )))
+        )],
+    );
+
+    // Collect all iindex/uid entries
+    let entries = db
+        .uids()
+        .iter(..)
+        .collect::<KvResult<Vec<(UidKeyV1, PublicKeySingletonDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_mindex__() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    let mindex_keys = db
+        .mindex()
+        .iter(..)
+        .keys()
+        .take(1)
+        .collect::<KvResult<Vec<PubKeyKeyV1>>>()?;
+    let one_mindex_db = unwrap!(db.mindex().get(&mindex_keys[0])?);
+    assert_eq!(one_mindex_db.0[0].pubkey, mindex_keys[0].0.to_base58());
+
+    //println!("{:#?}", one_mindex_db);
+
+    // Count mindex entries
+    let count = db.mindex().count()?;
+    println!("mindex size={}", count);
+
+    // Collect all mindex entries
+    let entries = db
+        .mindex()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, MIndexDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_mindex_expires_on() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all mindex/expires_on entries
+    let entries = db
+        .mindex_expires_on()
+        .iter(..)
+        .collect::<KvResult<Vec<(TimestampKeyV1, PublicKeyArrayDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+    /*for (k, v) in entries {
+        if k.0 == BlockNumber(u32::MAX) {
+            println!("{:?}", v.0)
+        }
+    }*/
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_mindex_revokes_on() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all mindex/revokes_on entries
+    let entries = db
+        .mindex_revokes_on()
+        .iter(..)
+        .collect::<KvResult<Vec<(TimestampKeyV1, PublicKeyArrayDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_mindex_written_on() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all mindex/written_on entries
+    let entries = db
+        .mindex_written_on()
+        .iter(..)
+        .collect::<KvResult<Vec<(BlockNumberKeyV1, PublicKeyArrayDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+    for (k, v) in entries {
+        if k.0 == BlockNumber(u32::MAX) {
+            println!("{:?}", v.0)
+        }
+    }
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_cindex__() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all bindex entries
+    let entries = db
+        .cindex()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, CIndexDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+    //println!("last_bindex={:?}", entries.last());
+    for (_k, v) in entries {
+        for cindex_line in v.issued {
+            if cindex_line.created_on_ref.is_some() {
+                println!("cindex_line={:?}", cindex_line)
+            }
+        }
+    }
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_cindex_expires_on() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all cindex/expires_on entries
+    let entries = db
+        .cindex_expires_on()
+        .iter(..)
+        .collect::<KvResult<Vec<(BlockNumberKeyV1, PublicKeyArrayDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_cindex_written_on() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all cindex/written_on entries
+    let entries = db
+        .cindex_written_on()
+        .iter(..)
+        .collect::<KvResult<Vec<(BlockNumberKeyV1, PublicKeyArrayDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_wallet() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all wallet entries
+    let entries = db
+        .wallet()
+        .iter(..)
+        .collect::<KvResult<Vec<(WalletConditionsV1, WalletDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+    let mut max_cond_len = 0;
+    for (k, _v) in entries {
+        if k.0.len() > max_cond_len {
+            max_cond_len = k.0.len();
+            println!("k={}", k.0.as_str());
+        }
+    }
+    println!("max_cond_len={}", max_cond_len);
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_dividend() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all level_dividend entries
+    let entries = db
+        .uds()
+        .iter(..)
+        .collect::<KvResult<Vec<(PubKeyKeyV1, UdEntryDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    println!("entries[0]=({:?}, {:?})", entries[0].0, entries[0].1);
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_dividend_written_on() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all level_dividend/level_dividend_trim_index entries
+    let entries = db
+        .uds_trim()
+        .iter(..)
+        .collect::<KvResult<Vec<(BlockNumberKeyV1, PublicKeyArrayDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_sindex() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all level_sindex entries
+    let entries = db
+        .sindex()
+        .iter(..)
+        .collect::<KvResult<Vec<(SourceKeyV1, SIndexDBV1)>>>()?;
+    println!("entries_len={}", entries.len());
+
+    println!("entries[0]=({:?}, {:?})", entries[0].0, entries[0].1);
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_sindex_written_on() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all mindex/written_on entries
+    let entries = db
+        .sindex_written_on()
+        .iter(..)
+        .collect::<KvResult<Vec<(BlockNumberKeyV1, SourceKeyArrayDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+    for (k, v) in entries {
+        if k.0 == BlockNumber(u32::MAX) {
+            println!("{:?}", v.0)
+        }
+    }
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_sindex_consumed_on() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+    // Collect all mindex/written_on entries
+    let entries = db
+        .sindex_consumed_on()
+        .iter(..)
+        .collect::<KvResult<Vec<(BlockNumberKeyV1, SourceKeyArrayDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+    for (k, v) in entries {
+        println!("{:?} => {:?}", k.0, v.0)
+    }
+
+    Ok(())
+}
+
+#[test]
+fn db_v1_sindex_conditions_on() -> Result<()> {
+    let _lock = MUTEX.lock().expect("MUTEX poisoned");
+
+    let db = BcV1Db::<LevelDb>::open(LevelDbConf::path(PathBuf::from(DB_PATH)))?;
+
+    // Collect all mindex/written_on entries
+    let entries = db
+        .sindex_conditions()
+        .iter(..)
+        .collect::<KvResult<Vec<(WalletConditionsV1, SourceKeyArrayDbV1)>>>()?;
+    println!("entries_len={}", entries.len());
+    /*for (k, v) in entries {
+        println!("{:?} => {:?}", k.0, v.0)
+    }*/
+
+    Ok(())
+}
diff --git a/rust-libs/tools/kv_typed/Cargo.toml b/rust-libs/tools/kv_typed/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..e87cbd58b3ad21ffe8325999f0a566157f94b3ff
--- /dev/null
+++ b/rust-libs/tools/kv_typed/Cargo.toml
@@ -0,0 +1,57 @@
+[package]
+name = "kv_typed"
+version = "0.1.0"
+authors = ["elois <c@elo.tf>"]
+description = "Strongly typed key-value storage"
+repository = "https://git.duniter.org/nodes/typescript/duniter"
+keywords = ["database", "key", "sled"]
+license = "AGPL-3.0"
+edition = "2018"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
+async-channel = { version = "1.4.2", optional = true }
+crossbeam-channel = { version = "0.4.4", optional = true }
+kv_typed_code_gen = { path = "../kv_typed_code_gen" }
+leveldb_minimal = { version = "0.1.0", optional = true }
+mockall = { version = "0.8.0", optional = true }
+parking_lot = { version = "0.11.0", optional = true }
+rayon = { version = "1.3.1", optional = true }
+regex = { version = "1.3.9", optional = true }
+serde_json = { version = "1.0.53", optional = true }
+sled = { version = "0.34.4", optional = true }
+smallvec = { version = "1.4.0", features = ["serde"] }
+thiserror = "1.0.20"
+
+[[bench]]
+name = "compare_backends"
+harness = false
+required-features = ["leveldb_backend", "memory_backend", "sled_backend"]
+
+[dev-dependencies]
+async-std = { version = "1.6.3", features = ["attributes"] }
+maybe-async = "0.2.0"
+smallvec = { version = "1.4.0", features = ["serde"] }
+unwrap = "1.2.1"
+
+# Benches dependencies
+criterion = { version = "0.3.1" }
+
+[features]
+#default = ["memory_backend"]
+
+async = ["async-channel"]
+explorer = ["rayon", "regex", "serde_json"]
+leveldb_backend = ["leveldb_minimal"]
+memory_backend = ["parking_lot"]
+sled_backend = ["sled"]
+subscription = ["parking_lot"]
+sync = ["crossbeam-channel"]
+
+mock = ["mockall"]
+
+default = ["memory_backend", "subscription", "sync"]
+#default = ["memory_backend", "subscription", "sync", "explorer"]
+#default = ["memory_backend", "subscription", "sync", "mock"]
diff --git a/rust-libs/tools/kv_typed/benches/compare_backends.rs b/rust-libs/tools/kv_typed/benches/compare_backends.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b89d4be1a2256a7e63a16f2c5c097cd56dfb24f9
--- /dev/null
+++ b/rust-libs/tools/kv_typed/benches/compare_backends.rs
@@ -0,0 +1,101 @@
+use criterion::{criterion_group, criterion_main, AxisScale, Criterion, PlotConfiguration};
+use kv_typed::prelude::*;
+use std::{fmt::Debug, path::PathBuf};
+
+db_schema!(Test, [["c1", col_1, u32, String],]);
+const LEVELDB_DIR_PATH: &str = "/dev/shm/kv_typed/benches/compare_backends/leveldb";
+static SMALL_VAL: &str = "abcdefghijklmnopqrst";
+static LARGE_VAL: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+fn read_n_entries<B: Backend>(db: &TestDb<B>, n: u32, val: String) {
+    let mut iter = db.col_1().iter(..).values();
+    for _ in 0..n {
+        assert_eq!(iter.next_res().expect(""), Some(val.clone()));
+        //assert_eq!(db.col_1().get(&i).expect(""), Some(val.clone()));
+    }
+    assert_eq!(iter.next_res().expect(""), None);
+}
+fn write_n_entries<B: Backend>(db: &TestDb<B>, n: u32, val: String) {
+    for i in 0..n {
+        db.col_1_write().remove(i).expect("fail to write");
+        db.col_1_write()
+            .upsert(i, val.clone())
+            .expect("fail to write");
+    }
+}
+
+pub fn benchmark(c: &mut Criterion) {
+    // Read chart config
+    let read_chart_config = PlotConfiguration::default().summary_scale(AxisScale::Logarithmic);
+
+    // Create DBs
+    std::fs::create_dir_all(LEVELDB_DIR_PATH).expect("fail to create leveldb dir");
+    let leveldb_db = TestDb::<LevelDb>::open(LevelDbConf {
+        db_path: PathBuf::from(LEVELDB_DIR_PATH),
+        ..Default::default()
+    })
+    .expect("fail to open db");
+    let mem_db = TestDb::<Mem>::open(MemConf::default()).expect("fail to open db");
+    let sled_db =
+        TestDb::<Sled>::open(SledConf::default().temporary(true)).expect("fail to open db");
+
+    // Test write small values
+    let mut group = c.benchmark_group("write small values");
+    group.bench_function("leveldb", |b| {
+        b.iter(|| write_n_entries::<LevelDb>(&leveldb_db, 100, String::from(SMALL_VAL)))
+    });
+    group.bench_function("mem", |b| {
+        b.iter(|| write_n_entries::<Mem>(&mem_db, 100, String::from(SMALL_VAL)))
+    });
+    group.bench_function("sled", |b| {
+        b.iter(|| write_n_entries::<Sled>(&sled_db, 100, String::from(SMALL_VAL)))
+    });
+    group.finish();
+
+    // Test read small values
+    let mut group = c.benchmark_group("read small values");
+    group.plot_config(read_chart_config.clone());
+    group.bench_function("leveldb", |b| {
+        b.iter(|| read_n_entries(&leveldb_db, 100, String::from(SMALL_VAL)))
+    });
+    group.bench_function("mem", |b| {
+        b.iter(|| read_n_entries(&mem_db, 100, String::from(SMALL_VAL)))
+    });
+    group.bench_function("sled", |b| {
+        b.iter(|| read_n_entries(&sled_db, 100, String::from(SMALL_VAL)))
+    });
+    group.finish();
+
+    // Test write large values
+    let mut group = c.benchmark_group("write large values");
+    group.bench_function("leveldb", |b| {
+        b.iter(|| write_n_entries::<LevelDb>(&leveldb_db, 100, String::from(LARGE_VAL)))
+    });
+    group.bench_function("mem", |b| {
+        b.iter(|| write_n_entries::<Mem>(&mem_db, 100, String::from(LARGE_VAL)))
+    });
+    group.bench_function("sled", |b| {
+        b.iter(|| write_n_entries::<Sled>(&sled_db, 100, String::from(LARGE_VAL)))
+    });
+    group.finish();
+
+    // Test read large values
+    let mut group = c.benchmark_group("read large values");
+    group.plot_config(read_chart_config);
+    group.bench_function("leveldb", |b| {
+        b.iter(|| read_n_entries(&leveldb_db, 100, String::from(LARGE_VAL)))
+    });
+    group.bench_function("mem", |b| {
+        b.iter(|| read_n_entries(&mem_db, 100, String::from(LARGE_VAL)))
+    });
+    group.bench_function("sled", |b| {
+        b.iter(|| read_n_entries(&sled_db, 100, String::from(LARGE_VAL)))
+    });
+    group.finish();
+
+    // Close DBs
+    std::fs::remove_dir_all(LEVELDB_DIR_PATH).expect("fail to create leveldb dir");
+}
+
+criterion_group!(benches, benchmark);
+criterion_main!(benches);
diff --git a/rust-libs/tools/kv_typed/src/as_bytes.rs b/rust-libs/tools/kv_typed/src/as_bytes.rs
new file mode 100644
index 0000000000000000000000000000000000000000..33cb6012dc24aefb96564437d37eb1fcea7c80c2
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/as_bytes.rs
@@ -0,0 +1,36 @@
+use crate::*;
+
+pub trait KeyAsBytes {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, f: F) -> T;
+}
+pub trait ValueAsBytes {
+    fn as_bytes<T, F: FnMut(&[u8]) -> Result<T, KvError>>(&self, f: F) -> Result<T, KvError>;
+}
+
+impl KeyAsBytes for String {
+    fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+        f(self.as_bytes())
+    }
+}
+impl ValueAsBytes for String {
+    fn as_bytes<T, F: FnMut(&[u8]) -> Result<T, KvError>>(&self, mut f: F) -> Result<T, KvError> {
+        f(self.as_bytes())
+    }
+}
+
+macro_rules! impl_as_bytes_for_numbers {
+    ($($T:ty),*) => {$(
+        impl KeyAsBytes for $T {
+            fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T {
+                f(&self.to_be_bytes()[..])
+            }
+        }
+        impl ValueAsBytes for $T {
+            fn as_bytes<T, F: FnMut(&[u8]) -> Result<T, KvError>>(&self, mut f: F) -> Result<T, KvError> {
+                f(&self.to_be_bytes()[..])
+            }
+        }
+    )*};
+}
+
+impl_as_bytes_for_numbers!(usize, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64);
diff --git a/rust-libs/tools/kv_typed/src/backend.rs b/rust-libs/tools/kv_typed/src/backend.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6992e41f8bb8e547ee49f264d00e1ed38ac99fa7
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/backend.rs
@@ -0,0 +1,74 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed Backend Trait
+
+#[cfg(feature = "leveldb_backend")]
+pub mod leveldb;
+#[cfg(feature = "memory_backend")]
+pub mod memory;
+#[cfg(feature = "mock")]
+pub mod mock;
+#[cfg(feature = "sled_backend")]
+pub mod sled;
+
+use crate::*;
+
+pub trait TransactionalBackend<DbReader: From<Vec<Self::TxCol>>, DbWriter: From<Vec<Self::TxCol>>>:
+    Backend
+{
+    type Err: Error + Send + Sync + 'static;
+    type TxCol: BackendCol;
+
+    fn read<A: Debug, D, F: Fn(&DbReader) -> TransactionResult<D, A, Self::Err>>(
+        &self,
+        f: F,
+    ) -> TransactionResult<D, A, Self::Err>;
+    fn write<A: Debug, F: Fn(&DbWriter) -> TransactionResult<(), A, Self::Err>>(
+        &self,
+        f: F,
+    ) -> TransactionResult<(), A, Self::Err>;
+}
+
+pub trait Backend: 'static + Clone + Sized {
+    const NAME: &'static str;
+    type Col: BackendCol;
+    type Conf: Default;
+
+    fn open(conf: &Self::Conf) -> KvResult<Self>;
+    fn open_col(&mut self, conf: &Self::Conf, col_name: &str) -> KvResult<Self::Col>;
+}
+
+pub trait BackendCol: 'static + Clone {
+    type Batch: BackendBatch;
+    type KeyBytes: AsRef<[u8]>;
+    type ValueBytes: AsRef<[u8]>;
+    type Iter: Iterator<Item = Result<(Self::KeyBytes, Self::ValueBytes), DynErr>>
+        + ReversableIterator;
+
+    fn get<K: Key, V: Value>(&self, k: &K) -> KvResult<Option<V>>;
+    fn count(&self) -> KvResult<usize>;
+    fn iter<K: Key, V: Value>(&self, range: RangeBytes) -> Self::Iter;
+    fn put<K: Key, V: Value>(&self, k: &K, value: &V) -> KvResult<()>;
+    fn delete<K: Key>(&self, k: &K) -> KvResult<()>;
+    fn new_batch() -> Self::Batch;
+    fn write_batch(&self, inner_batch: Self::Batch) -> KvResult<()>;
+}
+
+#[cfg_attr(feature = "mock", mockall::automock)]
+pub trait BackendBatch: Default {
+    fn upsert(&mut self, k: &[u8], v: &[u8]);
+    fn remove(&mut self, k: &[u8]);
+}
diff --git a/rust-libs/tools/kv_typed/src/backend/leveldb.rs b/rust-libs/tools/kv_typed/src/backend/leveldb.rs
new file mode 100644
index 0000000000000000000000000000000000000000..83892d4cfe9ed574bb3dab3b14753b47458eb3cb
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/backend/leveldb.rs
@@ -0,0 +1,208 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! LevelDb backend for KV Typed
+
+use crate::*;
+pub use leveldb_minimal::database::batch::{Batch as _, Writebatch as WriteBatch};
+use leveldb_minimal::database::cache::Cache as LevelDbCache;
+pub use leveldb_minimal::database::error::Error as LevelDbError;
+use leveldb_minimal::database::iterator::Iterator as LevelDbIterator;
+pub use leveldb_minimal::database::Database as LevelDbDb;
+use leveldb_minimal::iterator::{Iterable, LevelDBIterator as _};
+use leveldb_minimal::kv::KV as _;
+pub use leveldb_minimal::options::{Options as LevelDbOptions, ReadOptions, WriteOptions};
+use leveldb_minimal::Compression;
+use std::path::PathBuf;
+
+#[derive(Clone, Copy, Debug)]
+pub struct LevelDb;
+
+impl Backend for LevelDb {
+    const NAME: &'static str = "leveldb";
+    type Col = LevelDbCol;
+    type Conf = LevelDbConf;
+
+    fn open(_conf: &Self::Conf) -> KvResult<Self> {
+        Ok(LevelDb)
+    }
+    fn open_col(&mut self, conf: &Self::Conf, col_name: &str) -> KvResult<Self::Col> {
+        Ok(LevelDbCol(Arc::new(LevelDbDb::open(
+            &conf.db_path.join(col_name),
+            conf.clone().into(),
+        )?)))
+    }
+}
+
+#[derive(Clone)]
+pub struct LevelDbCol(Arc<LevelDbDb>);
+
+impl Debug for LevelDbCol {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("LevelDbCol")
+            .field("0", &"Arc<LevelDbDb>")
+            .finish()
+    }
+}
+
+impl BackendBatch for WriteBatch {
+    fn upsert(&mut self, k: &[u8], v: &[u8]) {
+        self.put(k, v)
+    }
+
+    fn remove(&mut self, k: &[u8]) {
+        self.delete(k)
+    }
+}
+
+impl BackendCol for LevelDbCol {
+    type Batch = WriteBatch;
+    type KeyBytes = Vec<u8>;
+    type ValueBytes = Vec<u8>;
+    type Iter = LevelDbIter;
+
+    #[inline(always)]
+    fn new_batch() -> Self::Batch {
+        WriteBatch::default()
+    }
+    #[inline(always)]
+    fn count(&self) -> KvResult<usize> {
+        Ok(self
+            .0
+            .iter(ReadOptions {
+                verify_checksums: false,
+                fill_cache: false,
+                snapshot: None,
+            })
+            .count())
+    }
+    #[inline(always)]
+    fn get<K: Key, V: Value>(&self, k: &K) -> KvResult<Option<V>> {
+        k.as_bytes(|k_bytes| {
+            self.0
+                .get(ReadOptions::new(), k_bytes)?
+                .map(|bytes| {
+                    V::from_bytes(&bytes).map_err(|e| KvError::DeserError(format!("{}", e)))
+                })
+                .transpose()
+        })
+    }
+    #[inline(always)]
+    fn delete<K: Key>(&self, k: &K) -> KvResult<()> {
+        k.as_bytes(|k_bytes| self.0.delete(WriteOptions::new(), k_bytes))?;
+        Ok(())
+    }
+    #[inline(always)]
+    fn put<K: Key, V: Value>(&self, k: &K, value: &V) -> KvResult<()> {
+        value.as_bytes(|value_bytes| {
+            k.as_bytes(|k_bytes| self.0.put(WriteOptions::new(), k_bytes, value_bytes))?;
+            Ok(())
+        })
+    }
+    #[inline(always)]
+    fn write_batch(&self, inner_batch: Self::Batch) -> KvResult<()> {
+        self.0.write(WriteOptions::new(), &inner_batch)?;
+        Ok(())
+    }
+    #[inline(always)]
+    fn iter<K: Key, V: Value>(&self, _range: RangeBytes) -> Self::Iter {
+        LevelDbIter(self.0.iter(ReadOptions::new()))
+    }
+}
+
+pub struct LevelDbIter(LevelDbIterator);
+impl Debug for LevelDbIter {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("LevelDbIter")
+            .field("0", &"LevelDbIterator<'db>")
+            .finish()
+    }
+}
+
+impl Iterator for LevelDbIter {
+    type Item = Result<(Vec<u8>, Vec<u8>), DynErr>;
+
+    #[inline(always)]
+    fn next(&mut self) -> Option<Self::Item> {
+        self.0.next().map(Ok)
+    }
+}
+impl ReversableIterator for LevelDbIter {
+    #[inline(always)]
+    fn reverse(self) -> Self {
+        Self(self.0.reverse())
+    }
+}
+
+#[derive(Clone, Debug)]
+/// leveldb configuration
+pub struct LevelDbConf {
+    pub create_if_missing: bool,
+    pub db_path: PathBuf,
+    pub error_if_exists: bool,
+    pub paranoid_checks: bool,
+    pub write_buffer_size: Option<usize>,
+    pub max_open_files: Option<i32>,
+    pub block_size: Option<usize>,
+    pub block_restart_interval: Option<i32>,
+    pub compression: bool,
+    pub cache: Option<usize>,
+}
+
+impl LevelDbConf {
+    pub fn path(db_path: PathBuf) -> Self {
+        Self {
+            db_path,
+            ..Default::default()
+        }
+    }
+}
+
+impl Default for LevelDbConf {
+    fn default() -> Self {
+        LevelDbConf {
+            create_if_missing: true,
+            db_path: PathBuf::default(),
+            error_if_exists: false,
+            paranoid_checks: false,
+            write_buffer_size: None,
+            max_open_files: None,
+            block_size: None,
+            block_restart_interval: None,
+            compression: true,
+            cache: None,
+        }
+    }
+}
+
+impl Into<LevelDbOptions> for LevelDbConf {
+    fn into(self) -> LevelDbOptions {
+        LevelDbOptions {
+            create_if_missing: self.create_if_missing,
+            error_if_exists: self.error_if_exists,
+            paranoid_checks: self.paranoid_checks,
+            write_buffer_size: self.write_buffer_size,
+            max_open_files: self.max_open_files,
+            block_size: self.block_size,
+            block_restart_interval: self.block_restart_interval,
+            compression: if self.compression {
+                Compression::Snappy
+            } else {
+                Compression::No
+            },
+            cache: self.cache.map(LevelDbCache::new),
+        }
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/backend/memory.rs b/rust-libs/tools/kv_typed/src/backend/memory.rs
new file mode 100644
index 0000000000000000000000000000000000000000..68aa9275b501af4cbcb18c1e1dffdd1f372b4489
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/backend/memory.rs
@@ -0,0 +1,200 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! Memory backend for KV Typed,
+
+use crate::*;
+use parking_lot::{RwLock, RwLockReadGuard};
+use std::collections::BTreeMap;
+
+#[derive(Clone, Copy, Debug)]
+pub struct Mem;
+
+#[derive(Clone, Copy, Debug, Default)]
+pub struct MemConf {
+    // Allows to prevent `MemConf` from being instantiated without using the `Default` trait.
+    // Thus the eventual addition of a field in the future will not be a breaking change.
+    phantom: PhantomData<()>,
+}
+
+type KeyBytes = IVec;
+type ValueBytes = IVec;
+type Map = BTreeMap<KeyBytes, ValueBytes>;
+type ArcSharedMap = Arc<RwLock<Map>>;
+
+impl Backend for Mem {
+    const NAME: &'static str = "mem";
+    type Col = MemCol;
+    type Conf = MemConf;
+
+    fn open(_conf: &Self::Conf) -> KvResult<Self> {
+        Ok(Mem)
+    }
+    fn open_col(&mut self, _conf: &Self::Conf, _col_name: &str) -> KvResult<Self::Col> {
+        Ok(MemCol(Arc::new(RwLock::new(BTreeMap::new()))))
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct MemBatch {
+    upsert_ops: Vec<(KeyBytes, ValueBytes)>,
+    remove_ops: Vec<KeyBytes>,
+}
+
+impl BackendBatch for MemBatch {
+    fn upsert(&mut self, k: &[u8], v: &[u8]) {
+        self.upsert_ops.push((k.into(), v.into()));
+    }
+
+    fn remove(&mut self, k: &[u8]) {
+        self.remove_ops.push(k.into());
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct MemCol(ArcSharedMap);
+
+impl BackendCol for MemCol {
+    type Batch = MemBatch;
+    type KeyBytes = KeyBytes;
+    type ValueBytes = ValueBytes;
+    type Iter = MemIter;
+
+    #[inline(always)]
+    fn new_batch() -> Self::Batch {
+        MemBatch::default()
+    }
+    #[inline(always)]
+    fn count(&self) -> KvResult<usize> {
+        let reader = self.0.read();
+        Ok(reader.len())
+    }
+    #[inline(always)]
+    fn get<K: Key, V: Value>(&self, k: &K) -> KvResult<Option<V>> {
+        k.as_bytes(|k_bytes| {
+            let reader = self.0.read();
+            reader
+                .get(k_bytes)
+                .map(|bytes| {
+                    V::from_bytes(&bytes).map_err(|e| KvError::DeserError(format!("{}", e)))
+                })
+                .transpose()
+        })
+    }
+    #[inline(always)]
+    fn delete<K: Key>(&self, k: &K) -> KvResult<()> {
+        k.as_bytes(|k_bytes| {
+            let mut writer = self.0.write();
+            writer.remove(k_bytes)
+        });
+        Ok(())
+    }
+    #[inline(always)]
+    fn put<K: Key, V: Value>(&self, k: &K, value: &V) -> KvResult<()> {
+        value.as_bytes(|value_bytes| {
+            k.as_bytes(|k_bytes| {
+                let mut writer = self.0.write();
+                writer.insert(k_bytes.into(), value_bytes.into());
+            });
+            Ok(())
+        })
+    }
+    #[inline(always)]
+    fn write_batch(&self, inner_batch: Self::Batch) -> KvResult<()> {
+        let mut writer = self.0.write();
+        for (k, v) in inner_batch.upsert_ops {
+            writer.insert(k, v);
+        }
+        for k in inner_batch.remove_ops {
+            writer.remove(&k);
+        }
+        Ok(())
+    }
+    #[inline(always)]
+    fn iter<K: Key, V: Value>(&self, range: RangeBytes) -> Self::Iter {
+        let map_shared_arc = self.0.clone();
+        let map_shared_ref = map_shared_arc.as_ref();
+
+        let reader = map_shared_ref.read();
+        self.iter_inner(range, reader)
+    }
+}
+
+impl MemCol {
+    fn iter_inner(
+        &self,
+        range: RangeBytes,
+        reader: RwLockReadGuard<BTreeMap<KeyBytes, ValueBytes>>,
+    ) -> MemIter {
+        let reader = unsafe {
+            std::mem::transmute::<
+                RwLockReadGuard<BTreeMap<KeyBytes, ValueBytes>>,
+                RwLockReadGuard<'static, BTreeMap<KeyBytes, ValueBytes>>,
+            >(reader)
+        };
+        let reader_ref = unsafe {
+            std::mem::transmute::<
+                &RwLockReadGuard<BTreeMap<KeyBytes, ValueBytes>>,
+                &'static RwLockReadGuard<'static, BTreeMap<KeyBytes, ValueBytes>>,
+            >(&reader)
+        };
+        let iter = reader_ref.range(range);
+
+        MemIter {
+            col: self.clone(),
+            reader: Some(reader),
+            iter,
+            reversed: false,
+        }
+    }
+}
+
+pub struct MemIter {
+    #[allow(dead_code)]
+    // Needed for safety
+    col: MemCol,
+    #[allow(dead_code)]
+    // Needed for safety
+    reader: Option<RwLockReadGuard<'static, BTreeMap<KeyBytes, ValueBytes>>>,
+    iter: std::collections::btree_map::Range<'static, KeyBytes, ValueBytes>,
+    reversed: bool,
+}
+
+impl Debug for MemIter {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("MemIter").field("0", &"???").finish()
+    }
+}
+impl Iterator for MemIter {
+    type Item = Result<(KeyBytes, ValueBytes), DynErr>;
+
+    #[inline(always)]
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.reversed {
+            self.iter.next_back()
+        } else {
+            self.iter.next()
+        }
+        .map(|(k, v)| Ok((k.to_owned(), v.to_owned())))
+    }
+}
+
+impl ReversableIterator for MemIter {
+    #[inline(always)]
+    fn reverse(mut self) -> Self {
+        self.reversed = !self.reversed;
+        self
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/backend/mock.rs b/rust-libs/tools/kv_typed/src/backend/mock.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5e8b99879d96f18fc8ed01159f4139c750ce7c02
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/backend/mock.rs
@@ -0,0 +1,70 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed mock backend
+
+use super::MockBackendBatch;
+use crate::*;
+
+#[cfg(feature = "mock")]
+mockall::mock! {
+    pub BackendIter {}
+    trait Iterator {
+        type Item = Result<(Vec<u8>, Vec<u8>), DynErr>;
+
+        fn next(&mut self) -> Option<<Self as Iterator>::Item>;
+    }
+    trait ReversableIterator {
+        fn reverse(self) -> Self;
+    }
+}
+
+#[cfg(feature = "mock")]
+mockall::mock! {
+    pub BackendCol {}
+    trait Clone {
+        fn clone(&self) -> Self;
+    }
+    trait BackendCol {
+        type Batch = MockBackendBatch;
+        type KeyBytes = Vec<u8>;
+        type ValueBytes = Vec<u8>;
+        type Iter = MockBackendIter;
+
+        fn get<K: Key, V: Value>(&self, k: &K) -> KvResult<Option<V>>;
+        fn count(&self) -> KvResult<usize>;
+        fn iter<K: Key, V: Value>(&self, range: RangeBytes) -> MockBackendIter;
+        fn put<K: Key, V: Value>(&self, k: &K, value: &V) -> KvResult<()>;
+        fn delete<K: Key>(&self, k: &K) -> KvResult<()>;
+        fn new_batch() -> MockBackendBatch;
+        fn write_batch(&self, inner_batch: MockBackendBatch) -> KvResult<()>;
+    }
+}
+
+#[cfg(feature = "mock")]
+mockall::mock! {
+    pub Backend {}
+    trait Clone {
+        fn clone(&self) -> Self;
+    }
+    trait Backend: 'static + Clone + Sized {
+        const NAME: &'static str = "mock";
+        type Col = MockBackendCol;
+        type Conf = ();
+
+        fn open(conf: &()) -> KvResult<Self>;
+        fn open_col(&mut self, conf: &(), col_name: &str) -> KvResult<MockBackendCol>;
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/backend/sled.rs b/rust-libs/tools/kv_typed/src/backend/sled.rs
new file mode 100644
index 0000000000000000000000000000000000000000..348ca2bdd04a24866fac3bf8c3f688ceca0e628d
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/backend/sled.rs
@@ -0,0 +1,145 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! Sled backend for KV Typed,
+
+mod transactional;
+
+pub use sled::Config;
+
+use crate::*;
+
+#[derive(Clone, Debug)]
+pub struct Sled {
+    db: sled::Db,
+    trees: Vec<sled::Tree>,
+}
+
+impl Backend for Sled {
+    const NAME: &'static str = "sled";
+    type Col = SledCol;
+    type Conf = Config;
+
+    fn open(conf: &Self::Conf) -> KvResult<Self> {
+        Ok(Sled {
+            db: conf.open()?,
+            trees: Vec::new(),
+        })
+    }
+    fn open_col(&mut self, _conf: &Self::Conf, col_name: &str) -> KvResult<Self::Col> {
+        let tree = self.db.open_tree(col_name)?;
+        self.trees.push(tree.clone());
+        Ok(SledCol(tree))
+    }
+}
+
+impl BackendBatch for sled::Batch {
+    fn upsert(&mut self, k: &[u8], v: &[u8]) {
+        self.insert(k, v)
+    }
+
+    fn remove(&mut self, k: &[u8]) {
+        self.remove(k)
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct SledCol(sled::Tree);
+
+impl BackendCol for SledCol {
+    type Batch = sled::Batch;
+    type KeyBytes = IVec;
+    type ValueBytes = IVec;
+    type Iter = SledIter;
+
+    #[inline(always)]
+    fn new_batch() -> Self::Batch {
+        sled::Batch::default()
+    }
+    #[inline(always)]
+    fn count(&self) -> KvResult<usize> {
+        Ok(self.0.len())
+    }
+    #[inline(always)]
+    fn get<K: Key, V: Value>(&self, k: &K) -> KvResult<Option<V>> {
+        k.as_bytes(|k_bytes| {
+            self.0
+                .get(k_bytes)?
+                .map(|bytes| {
+                    V::from_bytes(&bytes).map_err(|e| KvError::DeserError(format!("{}", e)))
+                })
+                .transpose()
+        })
+    }
+    #[inline(always)]
+    fn delete<K: Key>(&self, k: &K) -> KvResult<()> {
+        k.as_bytes(|k_bytes| self.0.remove(k_bytes))?;
+        Ok(())
+    }
+    #[inline(always)]
+    fn put<K: Key, V: Value>(&self, k: &K, value: &V) -> KvResult<()> {
+        value.as_bytes(|value_bytes| {
+            k.as_bytes(|k_bytes| self.0.insert(k_bytes, value_bytes))?;
+            Ok(())
+        })
+    }
+    #[inline(always)]
+    fn write_batch(&self, inner_batch: Self::Batch) -> KvResult<()> {
+        self.0.apply_batch(inner_batch)?;
+        Ok(())
+    }
+    #[inline(always)]
+    fn iter<K: Key, V: Value>(&self, range: RangeBytes) -> Self::Iter {
+        SledIter {
+            iter: self.0.range(range),
+            reversed: false,
+        }
+    }
+}
+
+pub struct SledIter {
+    iter: sled::Iter,
+    reversed: bool,
+}
+
+impl Debug for SledIter {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("SledIter")
+            .field("0", &"sled::Iter")
+            .finish()
+    }
+}
+impl Iterator for SledIter {
+    type Item = Result<(IVec, IVec), DynErr>;
+
+    #[inline(always)]
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.reversed {
+            self.iter.next_back()
+        } else {
+            self.iter.next()
+        }
+        .map(|res| res.map_err(Box::new).map_err(Into::into))
+    }
+}
+impl ReversableIterator for SledIter {
+    #[inline(always)]
+    fn reverse(self) -> Self {
+        SledIter {
+            iter: self.iter,
+            reversed: !self.reversed,
+        }
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/backend/sled/transactional.rs b/rust-libs/tools/kv_typed/src/backend/sled/transactional.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5ec9b43bbb8166b28c1472f8a322996805504484
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/backend/sled/transactional.rs
@@ -0,0 +1,83 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! Sled backend for KV Typed,
+
+/*use crate::*;
+use sled::transaction::{ConflictableTransactionError, Transactional, TransactionalTree};
+
+enum AbortType<A> {
+    User(A),
+    Kv(KvError),
+}
+
+#[allow(missing_copy_implementations)]
+pub struct SledTxCol(&'static TransactionalTree);
+
+impl Debug for SledTxCol {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("SledTxCol")
+            .field("0", &"TransactionalTree")
+            .finish()
+    }
+}
+
+impl<DbReader: From<Vec<SledTxCol>>, DbWriter> TransactionalBackend<DbReader, DbWriter> for Sled {
+    type Err = sled::Error;
+    type TxCol = SledTxCol;
+
+    fn read<A: Debug, D, F: Fn(&DbReader) -> TransactionResult<D, A, Self::Err>>(
+        &self,
+        f: F,
+    ) -> TransactionResult<D, A, Self::Err> {
+        match self
+            .trees
+            .transaction::<_, D>(|tx_trees: &Vec<TransactionalTree>| {
+                let reader = DbReader::from(
+                    tx_trees
+                        .iter()
+                        .map(|tx_tree| SledTxCol(unsafe { to_static_ref(tx_tree) }))
+                        .collect(),
+                );
+                f(&reader).map_err(|e| match e {
+                    TransactionError::Abort(a) => {
+                        ConflictableTransactionError::Abort(AbortType::User(a))
+                    }
+                    TransactionError::BackendErr(e) => ConflictableTransactionError::Storage(e),
+                    TransactionError::KvError(e) => {
+                        ConflictableTransactionError::Abort(AbortType::Kv(e))
+                    }
+                })
+            }) {
+            Ok(t) => Ok(t),
+            Err(e) => match e {
+                sled::transaction::TransactionError::Abort(a) => match a {
+                    AbortType::User(a) => Err(TransactionError::Abort(a)),
+                    AbortType::Kv(e) => Err(TransactionError::KvError(e)),
+                },
+                sled::transaction::TransactionError::Storage(e) => {
+                    Err(TransactionError::BackendErr(e))
+                }
+            },
+        }
+    }
+
+    fn write<A: Debug, F: Fn(&DbWriter) -> TransactionResult<(), A, Self::Err>>(
+        &self,
+        _f: F,
+    ) -> TransactionResult<(), A, Self::Err> {
+        todo!()
+    }
+}*/
diff --git a/rust-libs/tools/kv_typed/src/batch.rs b/rust-libs/tools/kv_typed/src/batch.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c7d3d8c64e7e70dc603be1418a89dee5b5cbcf2c
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/batch.rs
@@ -0,0 +1,74 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+use std::collections::{BTreeSet, HashMap, HashSet};
+
+#[derive(Debug)]
+pub struct Batch<BC: BackendCol, C: DbCollectionRw> {
+    backend_batch: BC::Batch,
+    upsert_keys_bytes: BTreeSet<IVec>,
+    upsert_ops: HashMap<C::K, C::V>,
+    delete_ops: HashSet<C::K>,
+}
+
+impl<BC: BackendCol, C: DbCollectionRw> Default for Batch<BC, C> {
+    fn default() -> Self {
+        Batch {
+            backend_batch: BC::Batch::default(),
+            upsert_keys_bytes: BTreeSet::default(),
+            upsert_ops: HashMap::default(),
+            delete_ops: HashSet::default(),
+        }
+    }
+}
+
+impl<BC: BackendCol, C: DbCollectionRw> Batch<BC, C> {
+    pub fn get(&self, k: &C::K) -> Option<&C::V> {
+        if self.delete_ops.contains(k) {
+            None
+        } else {
+            self.upsert_ops.get(k)
+        }
+    }
+    pub fn upsert(&mut self, k: C::K, v: C::V) {
+        let _ = k.as_bytes(|k_bytes| {
+            self.upsert_keys_bytes.insert(k_bytes.into());
+            v.as_bytes(|v_bytes| {
+                self.backend_batch.upsert(k_bytes, v_bytes);
+                Ok(())
+            })
+        });
+        self.upsert_ops.insert(k, v);
+    }
+    pub fn remove(&mut self, k: C::K) {
+        let _ = k.as_bytes(|k_bytes| self.backend_batch.remove(k_bytes));
+        self.delete_ops.insert(k);
+    }
+    #[cfg(not(feature = "subscription"))]
+    pub fn into_backend_batch(self) -> BC::Batch {
+        self.backend_batch
+    }
+    #[cfg(feature = "subscription")]
+    pub fn into_backend_batch_and_events(self) -> (BC::Batch, SmallVec<[C::Event; 4]>) {
+        let mut events: SmallVec<[C::Event; 4]> = self
+            .upsert_ops
+            .into_iter()
+            .map(|(k, v)| C::Event::upsert(k, v))
+            .collect();
+        events.extend(self.delete_ops.into_iter().map(C::Event::remove));
+        (self.backend_batch, events)
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/collection_ro.rs b/rust-libs/tools/kv_typed/src/collection_ro.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9161e8df1065458f628f32e37588d77174a052e0
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/collection_ro.rs
@@ -0,0 +1,85 @@
+use crate::*;
+
+pub trait DbCollectionRo: Sized {
+    type BackendCol: BackendCol;
+    type K: Key;
+    type V: Value;
+    type Event: EventTrait<K = Self::K, V = Self::V>;
+
+    fn count(&self) -> KvResult<usize>;
+    fn get(&self, k: &Self::K) -> KvResult<Option<Self::V>>;
+    fn iter<R: 'static + RangeBounds<Self::K>>(
+        &self,
+        range: R,
+    ) -> KvIter<Self::BackendCol, Self::K, Self::V>;
+    #[cfg(feature = "subscription")]
+    fn subscribe(&self, subscriber_sender: Subscriber<Self::Event>) -> KvResult<()>;
+}
+
+#[cfg(feature = "mock")]
+mockall::mock! {
+    pub ColRo<E: EventTrait> {}
+    trait DbCollectionRo {
+        type BackendCol = MockBackendCol;
+        type K = E::K;
+        type V = E::V;
+        type Event = E;
+
+        fn count(&self) -> KvResult<usize>;
+        fn get(&self, k: &E::K) -> KvResult<Option<E::V>>;
+        fn iter<R: 'static + RangeBounds<E::K>>(&self, range: R)
+        -> KvIter<MockBackendCol, E::K, E::V>;
+        #[cfg(feature = "subscription")]
+        fn subscribe(&self, subscriber_sender: Subscriber<E>) -> KvResult<()>;
+    }
+}
+
+#[derive(Debug)]
+pub struct ColRo<BC: BackendCol, E: EventTrait> {
+    pub(crate) inner: BC,
+    #[cfg(not(feature = "subscription"))]
+    pub(crate) phantom: PhantomData<E>,
+    #[cfg(feature = "subscription")]
+    pub(crate) subscription_sender: crate::subscription::SubscriptionsSender<E>,
+}
+impl<BC: BackendCol, E: EventTrait> Clone for ColRo<BC, E> {
+    fn clone(&self) -> Self {
+        Self {
+            inner: self.inner.clone(),
+            #[cfg(not(feature = "subscription"))]
+            phantom: PhantomData,
+            #[cfg(feature = "subscription")]
+            subscription_sender: self.subscription_sender.clone(),
+        }
+    }
+}
+impl<BC: BackendCol, E: EventTrait> DbCollectionRo for ColRo<BC, E> {
+    type BackendCol = BC;
+    type K = E::K;
+    type V = E::V;
+    type Event = E;
+
+    #[inline(always)]
+    fn count(&self) -> KvResult<usize> {
+        self.inner.count()
+    }
+    #[inline(always)]
+    fn get(&self, k: &Self::K) -> KvResult<Option<Self::V>> {
+        self.inner.get(k)
+    }
+    #[inline(always)]
+    fn iter<R: 'static + RangeBounds<Self::K>>(
+        &self,
+        range: R,
+    ) -> KvIter<Self::BackendCol, Self::K, Self::V> {
+        let range: RangeBytes = KvIter::<BC, Self::K, Self::V>::convert_range::<R>(range);
+        KvIter::new(self.inner.iter::<Self::K, Self::V>(range.clone()), range)
+    }
+    #[cfg(feature = "subscription")]
+    #[inline(always)]
+    fn subscribe(&self, subscriber_sender: Subscriber<Self::Event>) -> KvResult<()> {
+        self.subscription_sender
+            .try_send(subscriber_sender)
+            .map_err(|_| KvError::FailToSubscribe)
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/collection_rw.rs b/rust-libs/tools/kv_typed/src/collection_rw.rs
new file mode 100644
index 0000000000000000000000000000000000000000..962e9428912f607f6a064e1da701c49b12759c0a
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/collection_rw.rs
@@ -0,0 +1,121 @@
+use crate::*;
+#[cfg(feature = "subscription")]
+use parking_lot::Mutex;
+
+pub trait DbCollectionRw {
+    type K: Key;
+    type V: Value;
+    type Event: EventTrait<K = Self::K, V = Self::V>;
+
+    fn remove(&self, k: Self::K) -> KvResult<()>;
+    fn upsert(&self, k: Self::K, v: Self::V) -> KvResult<()>;
+}
+
+#[derive(Debug)]
+pub struct ColRw<BC: BackendCol, E: EventTrait> {
+    inner: ColRo<BC, E>,
+    #[cfg(feature = "subscription")]
+    pending_events_receiver: Receiver<Events<E>>,
+    #[cfg(feature = "subscription")]
+    pending_events_sender: Sender<Events<E>>,
+    #[cfg(feature = "subscription")]
+    subscribers: Arc<Mutex<ColSubscribers<E>>>,
+}
+
+impl<BC: BackendCol, E: EventTrait> Clone for ColRw<BC, E> {
+    fn clone(&self) -> Self {
+        Self {
+            inner: self.inner.clone(),
+            #[cfg(feature = "subscription")]
+            pending_events_receiver: self.pending_events_receiver.clone(),
+            #[cfg(feature = "subscription")]
+            pending_events_sender: self.pending_events_sender.clone(),
+            #[cfg(feature = "subscription")]
+            subscribers: self.subscribers.clone(),
+        }
+    }
+}
+
+impl<BC: BackendCol, E: EventTrait> DbCollectionRw for ColRw<BC, E> {
+    type K = E::K;
+    type V = E::V;
+    type Event = E;
+
+    fn remove(&self, k: Self::K) -> KvResult<()> {
+        self.inner.inner.delete(&k)?;
+        #[cfg(feature = "subscription")]
+        {
+            let events = smallvec::smallvec![E::remove(k)];
+            self.notify_subscribers(events);
+        }
+        Ok(())
+    }
+    fn upsert(&self, k: Self::K, v: Self::V) -> KvResult<()> {
+        self.inner.inner.put(&k, &v)?;
+        #[cfg(feature = "subscription")]
+        {
+            let events = smallvec::smallvec![E::upsert(k, v)];
+            self.notify_subscribers(events);
+        }
+        Ok(())
+    }
+}
+
+impl<BC: BackendCol, E: EventTrait> ColRw<BC, E> {
+    #[cfg(not(feature = "subscription"))]
+    pub fn new(col_backend: BC) -> Self {
+        Self {
+            inner: ColRo {
+                inner: col_backend,
+                phantom: PhantomData,
+            },
+        }
+    }
+    #[cfg(feature = "subscription")]
+    pub fn new(col_backend: BC) -> Self {
+        let subscribers = ColSubscribers::<E>::default();
+        let subscription_sender = subscribers.get_subscription_sender();
+        let inner = ColRo {
+            inner: col_backend,
+            subscription_sender,
+        };
+        let (pending_events_sender, pending_events_receiver) = unbounded();
+        Self {
+            inner,
+            pending_events_sender,
+            pending_events_receiver,
+            subscribers: Arc::new(Mutex::new(subscribers)),
+        }
+    }
+    pub fn to_ro(&self) -> &ColRo<BC, E> {
+        &self.inner
+    }
+    #[cfg(feature = "subscription")]
+    fn notify_subscribers(&self, mut events: Events<E>) {
+        if let Some(mut subscribers_guard) = self.subscribers.try_lock() {
+            // Take pending events
+            while let Ok(pending_events) = self.pending_events_receiver.try_recv() {
+                events.extend(pending_events);
+            }
+            // Add new subscribers, notify all subscribers them prune died subscribers
+            subscribers_guard.add_new_subscribers();
+            let died_subscribers = subscribers_guard.notify_subscribers(Arc::new(events));
+            subscribers_guard.prune_subscribers(died_subscribers);
+        } else if !events.is_empty() {
+            // Push pending events into the queue
+            let _ = self.pending_events_sender.try_send(events);
+        }
+    }
+    #[cfg(not(feature = "subscription"))]
+    pub fn write_batch(&self, batch: Batch<BC, Self>) -> KvResult<()> {
+        self.inner.inner.write_batch(batch.into_backend_batch())?;
+        Ok(())
+    }
+    #[cfg(feature = "subscription")]
+    pub fn write_batch(&self, batch: Batch<BC, Self>) -> KvResult<()> {
+        let (backend_batch, events) = batch.into_backend_batch_and_events();
+        self.inner.inner.write_batch(backend_batch)?;
+        self.notify_subscribers(events);
+        Ok(())
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/error.rs b/rust-libs/tools/kv_typed/src/error.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dd9a362f531fc00fb55600d1d8d033ca0736ab45
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/error.rs
@@ -0,0 +1,65 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed error type
+
+use crate::*;
+
+#[derive(Clone, Debug, Error, PartialEq)]
+#[error("{0}")]
+pub struct StringErr(pub String);
+
+pub type DynErr = Box<dyn Error + Send + Sync + 'static>;
+
+/// KV Typed error
+pub type KvResult<T> = Result<T, KvError>;
+
+/// KV Typed error
+#[derive(Debug, Error)]
+pub enum KvError {
+    /// Backend error
+    #[error("Backend error: {0}")]
+    BackendError(DynErr),
+    // Error at serialisation or deserialisation
+    #[error("DeserError: {0}")]
+    DeserError(String),
+    /// FailToCreateDbFolder
+    #[error("FailToCreateDbFolder: {0}")]
+    FailToCreateDbFolder(std::io::Error),
+    /// FailToSubscribe
+    #[error("FailToSubscribe")]
+    FailToSubscribe,
+}
+
+#[cfg(feature = "leveldb_backend")]
+impl From<crate::backend::leveldb::LevelDbError> for KvError {
+    fn from(e: crate::backend::leveldb::LevelDbError) -> Self {
+        KvError::BackendError(Box::new(e).into())
+    }
+}
+#[cfg(feature = "sled_backend")]
+impl From<sled::Error> for KvError {
+    fn from(e: sled::Error) -> Self {
+        KvError::BackendError(Box::new(e).into())
+    }
+}
+
+pub type TransactionResult<D, A, BE> = Result<D, TransactionError<A, BE>>;
+#[derive(Debug)]
+pub enum TransactionError<A: Debug, BE: Error + Send + Sync + 'static> {
+    Abort(A),
+    BackendErr(BE),
+    KvError(KvError),
+}
diff --git a/rust-libs/tools/kv_typed/src/event.rs b/rust-libs/tools/kv_typed/src/event.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b2faa43959ba135d951cc9296023102a510cfa37
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/event.rs
@@ -0,0 +1,30 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed event
+
+use crate::*;
+
+/// Database events
+pub type Events<E> = SmallVec<[E; 4]>;
+
+/// Event trait
+pub trait EventTrait: 'static + Debug + PartialEq + Send + Sync {
+    type K: Key;
+    type V: Value;
+
+    fn upsert(k: Self::K, v: Self::V) -> Self;
+    fn remove(k: Self::K) -> Self;
+}
diff --git a/rust-libs/tools/kv_typed/src/explorer.rs b/rust-libs/tools/kv_typed/src/explorer.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2d912b836aed6aa5dc7264643ea208c1d9da6179
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/explorer.rs
@@ -0,0 +1,371 @@
+use crate::*;
+#[cfg(not(feature = "async"))]
+use rayon::{iter::ParallelBridge, prelude::*};
+use std::num::NonZeroUsize;
+
+pub trait DbExplorable {
+    fn explore<'a>(
+        &self,
+        collection_name: &str,
+        action: ExplorerAction<'a>,
+        stringify_json_value: fn(serde_json::Value) -> serde_json::Value,
+    ) -> KvResult<Result<ExplorerActionResponse, StringErr>>;
+    fn list_collections(&self) -> Vec<(&'static str, &'static str, &'static str)>;
+}
+
+pub trait ExplorableKey: Sized {
+    fn from_explorer_str(source: &str) -> Result<Self, StringErr>;
+    fn to_explorer_string(&self) -> KvResult<String>;
+}
+
+impl ExplorableKey for String {
+    fn from_explorer_str(source: &str) -> Result<Self, StringErr> {
+        Ok(source.to_owned())
+    }
+
+    fn to_explorer_string(&self) -> KvResult<String> {
+        Ok(self.clone())
+    }
+}
+
+macro_rules! impl_explorable_key_for_numbers {
+    ($($T:ty),*) => {$(
+        impl ExplorableKey for $T {
+            fn from_explorer_str(source: &str) -> Result<Self, StringErr> {
+                source.parse().map_err(|e| StringErr(format!("{}", e)))
+            }
+
+            fn to_explorer_string(&self) -> KvResult<String> {
+                Ok(format!("{}", self))
+            }
+        }
+    )*};
+}
+
+impl_explorable_key_for_numbers!(usize, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64);
+
+pub trait ExplorableValue: Sized {
+    fn from_explorer_str(source: &str) -> Result<Self, StringErr>;
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value>;
+}
+
+impl ExplorableValue for String {
+    fn from_explorer_str(source: &str) -> Result<Self, StringErr> {
+        Ok(source.to_owned())
+    }
+
+    fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+        Ok(serde_json::Value::String(self.clone()))
+    }
+}
+
+macro_rules! impl_explorable_value_for_numbers {
+    ($($T:ty),*) => {$(
+        impl ExplorableValue for $T {
+            fn from_explorer_str(source: &str) -> Result<Self, StringErr> {
+                source.parse().map_err(|e| StringErr(format!("{}", e)))
+            }
+
+            #[allow(trivial_numeric_casts)]
+            fn to_explorer_json(&self) -> KvResult<serde_json::Value> {
+                Ok(serde_json::Value::Number(serde_json::Number::from_f64(*self as f64).expect("too large number")))
+            }
+        }
+    )*};
+}
+
+impl_explorable_value_for_numbers!(
+    usize, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64
+);
+
+#[derive(Debug)]
+pub enum ExplorerAction<'a> {
+    Count,
+    Get {
+        key: &'a str,
+    },
+    Find {
+        key_min: Option<String>,
+        key_max: Option<String>,
+        key_regex: Option<regex::Regex>,
+        value_regex: Option<regex::Regex>,
+        limit: Option<usize>,
+        reverse: bool,
+        step: NonZeroUsize,
+    },
+    Put {
+        key: &'a str,
+        value: &'a str,
+    },
+    Delete {
+        key: &'a str,
+    },
+}
+
+#[derive(Debug, PartialEq)]
+pub struct EntryFound {
+    pub key: String,
+    pub value: serde_json::Value,
+    pub captures: Option<ValueCaptures>,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct ValueCaptures(pub SmallVec<[SmallVec<[Option<String>; 8]>; 8]>);
+
+#[derive(Debug, PartialEq)]
+pub enum ExplorerActionResponse {
+    Count(usize),
+    Get(Option<serde_json::Value>),
+    Find(Vec<EntryFound>),
+    PutOk,
+    DeleteOk,
+}
+
+impl<'a> ExplorerAction<'a> {
+    pub fn exec<BC: BackendCol, E: EventTrait>(
+        self,
+        col: &ColRw<BC, E>,
+        stringify_json_value: fn(serde_json::Value) -> serde_json::Value,
+    ) -> KvResult<Result<ExplorerActionResponse, StringErr>> {
+        Ok(match self {
+            Self::Count => Ok(ExplorerActionResponse::Count(col.to_ro().count()?)),
+            Self::Get { key } => match E::K::from_explorer_str(key) {
+                Ok(k) => Ok(ExplorerActionResponse::Get(
+                    col.to_ro()
+                        .get(&k)?
+                        .map(|v| v.to_explorer_json())
+                        .transpose()?,
+                )),
+                Err(e) => Err(e),
+            },
+            Self::Find {
+                key_min,
+                key_max,
+                key_regex,
+                value_regex,
+                limit,
+                reverse,
+                step,
+            } => match define_range::<E::K>(key_min, key_max) {
+                Ok(range) => Ok(ExplorerActionResponse::Find(match range {
+                    Range::Full => Self::get_range_inner(
+                        col.to_ro(),
+                        ..,
+                        key_regex,
+                        value_regex,
+                        limit,
+                        reverse,
+                        step,
+                        stringify_json_value,
+                    )?,
+                    Range::From(range) => Self::get_range_inner(
+                        col.to_ro(),
+                        range,
+                        key_regex,
+                        value_regex,
+                        limit,
+                        reverse,
+                        step,
+                        stringify_json_value,
+                    )?,
+                    Range::FromTo(range) => Self::get_range_inner(
+                        col.to_ro(),
+                        range,
+                        key_regex,
+                        value_regex,
+                        limit,
+                        reverse,
+                        step,
+                        stringify_json_value,
+                    )?,
+                    Range::To(range) => Self::get_range_inner(
+                        col.to_ro(),
+                        range,
+                        key_regex,
+                        value_regex,
+                        limit,
+                        reverse,
+                        step,
+                        stringify_json_value,
+                    )?,
+                })),
+                Err(e) => Err(e),
+            },
+            Self::Put { key, value } => match E::K::from_explorer_str(key) {
+                Ok(k) => match E::V::from_explorer_str(value) {
+                    Ok(v) => {
+                        col.upsert(k, v)?;
+                        Ok(ExplorerActionResponse::PutOk)
+                    }
+                    Err(e) => Err(e),
+                },
+                Err(e) => Err(e),
+            },
+            Self::Delete { key } => match E::K::from_explorer_str(key) {
+                Ok(k) => {
+                    col.remove(k)?;
+                    Ok(ExplorerActionResponse::DeleteOk)
+                }
+                Err(e) => Err(e),
+            },
+        })
+    }
+    #[allow(clippy::too_many_arguments)]
+    fn get_range_inner<BC: BackendCol, E: EventTrait, R: 'static + RangeBounds<E::K>>(
+        col: &ColRo<BC, E>,
+        range: R,
+        key_regex: Option<regex::Regex>,
+        value_regex: Option<regex::Regex>,
+        limit: Option<usize>,
+        reverse: bool,
+        step: NonZeroUsize,
+        stringify_json_value: fn(serde_json::Value) -> serde_json::Value,
+    ) -> KvResult<Vec<EntryFound>> {
+        let filter_map_closure = move |entry_res| {
+            stringify_and_filter_entry_res::<E::K, E::V>(
+                entry_res,
+                key_regex.as_ref(),
+                value_regex.as_ref(),
+                stringify_json_value,
+            )
+        };
+
+        if let Some(limit) = limit {
+            let iter = col.iter(range);
+
+            if reverse {
+                iter.reverse()
+                    .step_by(step.get())
+                    .filter_map(filter_map_closure)
+                    .take(limit)
+                    .collect()
+            } else {
+                iter.step_by(step.get())
+                    .filter_map(filter_map_closure)
+                    .take(limit)
+                    .collect()
+            }
+        } else {
+            #[cfg(feature = "async")]
+            {
+                if reverse {
+                    col.iter(range)
+                        .reverse()
+                        .step_by(step.get())
+                        .filter_map(filter_map_closure)
+                        .collect()
+                } else {
+                    col.iter(range)
+                        .step_by(step.get())
+                        .filter_map(filter_map_closure)
+                        .collect()
+                }
+            }
+            #[cfg(not(feature = "async"))]
+            {
+                let (send, recv) = unbounded();
+
+                let handler = std::thread::spawn(move || {
+                    let iter = recv.into_iter().step_by(step.get()).par_bridge();
+
+                    iter.filter_map(filter_map_closure).collect()
+                });
+
+                if reverse {
+                    for entry_res in col.iter(range).reverse() {
+                        if send.try_send(entry_res).is_err() {
+                            return handler.join().expect("child thread panic");
+                        }
+                    }
+                } else {
+                    for entry_res in col.iter(range) {
+                        if send.try_send(entry_res).is_err() {
+                            return handler.join().expect("child thread panic");
+                        }
+                    }
+                }
+                drop(send);
+
+                handler.join().expect("child thread panic")
+            }
+        }
+    }
+}
+
+enum Range<K> {
+    Full,
+    From(core::ops::RangeFrom<K>),
+    To(core::ops::RangeToInclusive<K>),
+    FromTo(core::ops::RangeInclusive<K>),
+}
+
+fn define_range<K: Key>(
+    key_min_opt: Option<String>,
+    key_max_opt: Option<String>,
+) -> Result<Range<K>, StringErr> {
+    if let Some(key_min) = key_min_opt {
+        let k_min = K::from_explorer_str(&key_min)?;
+        if let Some(key_max) = key_max_opt {
+            let k_max = K::from_explorer_str(&key_max)?;
+            Ok(Range::FromTo(core::ops::RangeInclusive::new(k_min, k_max)))
+        } else {
+            Ok(Range::From(core::ops::RangeFrom { start: k_min }))
+        }
+    } else if let Some(key_max) = key_max_opt {
+        let k_max = K::from_explorer_str(&key_max)?;
+        Ok(Range::To(core::ops::RangeToInclusive { end: k_max }))
+    } else {
+        Ok(Range::Full)
+    }
+}
+
+fn stringify_and_filter_entry_res<K: Key, V: Value>(
+    entry_res: KvResult<(K, V)>,
+    key_regex_opt: Option<&regex::Regex>,
+    value_regex_opt: Option<&regex::Regex>,
+    stringify_json_value: fn(serde_json::Value) -> serde_json::Value,
+) -> Option<KvResult<EntryFound>> {
+    match entry_res {
+        Ok((k, v)) => match k.to_explorer_string() {
+            Ok(key_string) => {
+                if let Some(key_regex) = key_regex_opt {
+                    if !key_regex.is_match(&key_string) {
+                        return None;
+                    }
+                }
+                match v.to_explorer_json() {
+                    Ok(mut value_json) => {
+                        value_json = stringify_json_value(value_json);
+                        let captures = if let Some(value_regex) = value_regex_opt {
+                            let value_string = value_json.to_string();
+                            if !value_regex.is_match(&value_string) {
+                                return None;
+                            }
+                            Some(ValueCaptures(
+                                value_regex
+                                    .captures_iter(&value_string)
+                                    .map(|caps| {
+                                        caps.iter()
+                                            .skip(1)
+                                            .map(|m_opt| m_opt.map(|m| m.as_str().to_owned()))
+                                            .collect::<SmallVec<[Option<String>; 8]>>()
+                                    })
+                                    .collect(),
+                            ))
+                        } else {
+                            None
+                        };
+                        Some(Ok(EntryFound {
+                            key: key_string,
+                            value: value_json,
+                            captures,
+                        }))
+                    }
+                    Err(e) => Some(Err(e)),
+                }
+            }
+            Err(e) => Some(Err(e)),
+        },
+        Err(e) => Some(Err(e)),
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/from_bytes.rs b/rust-libs/tools/kv_typed/src/from_bytes.rs
new file mode 100644
index 0000000000000000000000000000000000000000..41329b14a1bc159ab76c63d5ae77988489c6bcf7
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/from_bytes.rs
@@ -0,0 +1,22 @@
+use crate::*;
+
+pub trait FromBytes: Sized {
+    type Err: Error;
+
+    /// Create Self from bytes.
+    fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Err>;
+}
+
+macro_rules! impl_from_bytes_for_numbers {
+    ($($T:ty),*) => {$(
+        impl FromBytes for $T {
+            type Err = std::array::TryFromSliceError;
+
+            fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Err> {
+                Ok(<$T>::from_be_bytes(bytes.try_into()?))
+            }
+        }
+    )*};
+}
+
+impl_from_bytes_for_numbers!(usize, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64);
diff --git a/rust-libs/tools/kv_typed/src/iter.rs b/rust-libs/tools/kv_typed/src/iter.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b177bb65d422de75b3917a68b77edd9196e907c0
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/iter.rs
@@ -0,0 +1,116 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed iterators
+
+pub mod keys;
+mod range;
+pub mod values;
+
+use crate::*;
+
+pub trait ReversableIterator: Iterator + Sized {
+    fn reverse(self) -> Self;
+
+    #[inline(always)]
+    fn last(self) -> Option<Self::Item> {
+        self.reverse().next()
+    }
+}
+
+pub trait ResultIter<T, E>: Iterator<Item = Result<T, E>> + Sized {
+    #[inline(always)]
+    fn next_res(&mut self) -> Result<Option<T>, E> {
+        self.next().transpose()
+    }
+}
+impl<I, T, E> ResultIter<T, E> for I where I: Iterator<Item = Result<T, E>> + Sized {}
+
+pub type RangeBytes = (Bound<IVec>, Bound<IVec>);
+
+#[derive(Debug)]
+pub struct KvIter<C: BackendCol, K: Key, V: Value> {
+    range_iter: range::RangeIter<C>,
+    phantom_key: PhantomData<K>,
+    phantom_value: PhantomData<V>,
+}
+
+impl<C: BackendCol, K: Key, V: Value> Iterator for KvIter<C, K, V> {
+    type Item = KvResult<(K, V)>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match self.range_iter.next() {
+            Some(Ok((key_bytes, value_bytes))) => match K::from_bytes(key_bytes.as_ref()) {
+                Ok(key) => match V::from_bytes(value_bytes.as_ref()) {
+                    Ok(value) => Some(Ok((key, value))),
+                    Err(e) => Some(Err(KvError::DeserError(format!("{}", e)))),
+                },
+                Err(e) => Some(Err(KvError::DeserError(format!("{}", e)))),
+            },
+            Some(Err(e)) => Some(Err(KvError::BackendError(e))),
+            None => None,
+        }
+    }
+}
+
+impl<C: BackendCol, K: Key, V: Value> ReversableIterator for KvIter<C, K, V> {
+    #[inline(always)]
+    fn reverse(self) -> Self {
+        Self {
+            range_iter: self.range_iter.reverse(),
+            phantom_key: PhantomData,
+            phantom_value: PhantomData,
+        }
+    }
+}
+
+impl<C: BackendCol, K: Key, V: Value> KvIter<C, K, V> {
+    pub fn keys(self) -> KvIterKeys<C, K> {
+        KvIterKeys::new(self.range_iter)
+    }
+    pub fn values(self) -> KvIterValues<C, K, V> {
+        KvIterValues::new(self.range_iter)
+    }
+    pub(crate) fn convert_range<RK: RangeBounds<K>>(range: RK) -> RangeBytes {
+        let range_start = convert_bound(range.start_bound());
+        let range_end = convert_bound(range.end_bound());
+        (range_start, range_end)
+    }
+    #[cfg(feature = "mock")]
+    pub fn new(backend_iter: C::Iter, range: RangeBytes) -> Self {
+        Self {
+            range_iter: range::RangeIter::new(backend_iter, range.0, range.1),
+            phantom_key: PhantomData,
+            phantom_value: PhantomData,
+        }
+    }
+    #[cfg(not(feature = "mock"))]
+    pub(crate) fn new(backend_iter: C::Iter, range: RangeBytes) -> Self {
+        Self {
+            range_iter: range::RangeIter::new(backend_iter, range.0, range.1),
+            phantom_key: PhantomData,
+            phantom_value: PhantomData,
+        }
+    }
+}
+
+#[inline(always)]
+fn convert_bound<K: Key>(bound_key: Bound<&K>) -> Bound<IVec> {
+    match bound_key {
+        Bound::Included(key) => Bound::Included(key.as_bytes(|key_bytes| key_bytes.into())),
+        Bound::Excluded(key) => Bound::Excluded(key.as_bytes(|key_bytes| key_bytes.into())),
+        Bound::Unbounded => Bound::Unbounded,
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/iter/keys.rs b/rust-libs/tools/kv_typed/src/iter/keys.rs
new file mode 100644
index 0000000000000000000000000000000000000000..bbe9a0d74e5f6231ff022471f9d0e2d30f9b3564
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/iter/keys.rs
@@ -0,0 +1,58 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed iterators
+
+use crate::*;
+
+#[derive(Debug)]
+pub struct KvIterKeys<C: BackendCol, K: Key> {
+    range_iter: super::range::RangeIter<C>,
+    phantom_key: PhantomData<K>,
+}
+
+impl<C: BackendCol, K: Key> Iterator for KvIterKeys<C, K> {
+    type Item = KvResult<K>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match self.range_iter.next() {
+            Some(Ok((key_bytes, _value_bytes))) => match K::from_bytes(key_bytes.as_ref()) {
+                Ok(key) => Some(Ok(key)),
+                Err(e) => Some(Err(KvError::DeserError(format!("{}", e)))),
+            },
+            Some(Err(e)) => Some(Err(KvError::BackendError(e))),
+            None => None,
+        }
+    }
+}
+
+impl<C: BackendCol, K: Key> ReversableIterator for KvIterKeys<C, K> {
+    #[inline(always)]
+    fn reverse(self) -> Self {
+        Self {
+            range_iter: self.range_iter.reverse(),
+            phantom_key: PhantomData,
+        }
+    }
+}
+
+impl<C: BackendCol, K: Key> KvIterKeys<C, K> {
+    pub(super) fn new(range_iter: super::range::RangeIter<C>) -> Self {
+        Self {
+            range_iter,
+            phantom_key: PhantomData,
+        }
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/iter/range.rs b/rust-libs/tools/kv_typed/src/iter/range.rs
new file mode 100644
index 0000000000000000000000000000000000000000..58afce651111c2bb4ffdc0e6782cd1db0df90c53
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/iter/range.rs
@@ -0,0 +1,110 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed iterators
+
+use crate::*;
+
+// V2
+pub(super) struct RangeIter<C: BackendCol> {
+    backend_iter: C::Iter,
+    reversed: bool,
+    range_start: Bound<IVec>,
+    range_end: Bound<IVec>,
+}
+
+impl<C: BackendCol> Debug for RangeIter<C> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("LevelDbCol")
+            .field("backend_iter", &"BackendIter")
+            .field("reversed", &format!("{:?}", self.reversed))
+            .field("range_start", &format!("{:?}", self.range_start))
+            .field("range_end", &format!("{:?}", self.range_end))
+            .finish()
+    }
+}
+
+impl<C: BackendCol> RangeIter<C> {
+    #[inline(always)]
+    pub(crate) fn new(
+        backend_iter: C::Iter,
+        range_start: Bound<IVec>,
+        range_end: Bound<IVec>,
+    ) -> Self {
+        RangeIter {
+            backend_iter,
+            reversed: false,
+            range_start,
+            range_end,
+        }
+    }
+}
+
+impl<C: BackendCol> Iterator for RangeIter<C> {
+    type Item = <C::Iter as Iterator>::Item;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        loop {
+            match self.backend_iter.next() {
+                Some(Ok((key_bytes, value_bytes))) => {
+                    let start_bound_ok = match &self.range_start {
+                        Bound::Included(start_bytes) => key_bytes.as_ref() >= start_bytes.as_ref(),
+                        Bound::Excluded(start_bytes) => key_bytes.as_ref() > start_bytes.as_ref(),
+                        Bound::Unbounded => true,
+                    };
+                    let end_bound_ok = match &self.range_end {
+                        Bound::Included(end_bytes) => key_bytes.as_ref() <= end_bytes.as_ref(),
+                        Bound::Excluded(end_bytes) => key_bytes.as_ref() < end_bytes.as_ref(),
+                        Bound::Unbounded => true,
+                    };
+                    if start_bound_ok {
+                        if end_bound_ok {
+                            break Some(Ok((key_bytes, value_bytes)));
+                        } else if self.reversed {
+                            // The interval has not yet begun.
+                            continue;
+                        } else {
+                            // The range has been fully traversed, the iterator is finished.
+                            break None;
+                        }
+                    } else if end_bound_ok {
+                        if self.reversed {
+                            // The range has been fully traversed, the iterator is finished.
+                            break None;
+                        } else {
+                            // The interval has not yet begun.
+                            continue;
+                        }
+                    } else {
+                        // Empty range, the iterator is finished.
+                        break None;
+                    }
+                }
+                other => break other,
+            }
+        }
+    }
+}
+impl<C: BackendCol> ReversableIterator for RangeIter<C> {
+    #[inline(always)]
+    fn reverse(self) -> Self {
+        RangeIter {
+            backend_iter: self.backend_iter.reverse(),
+            reversed: !self.reversed,
+            range_start: self.range_start,
+            range_end: self.range_end,
+        }
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/iter/values.rs b/rust-libs/tools/kv_typed/src/iter/values.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d75986e46ddb7b3a492ac3f80451108a76672d65
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/iter/values.rs
@@ -0,0 +1,61 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed iterators
+
+use crate::*;
+
+#[derive(Debug)]
+pub struct KvIterValues<C: BackendCol, K: Key, V: Value> {
+    range_iter: super::range::RangeIter<C>,
+    phantom_key: PhantomData<K>,
+    phantom_value: PhantomData<V>,
+}
+
+impl<C: BackendCol, K: Key, V: Value> Iterator for KvIterValues<C, K, V> {
+    type Item = KvResult<V>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match self.range_iter.next() {
+            Some(Ok((_key_bytes, value_bytes))) => match V::from_bytes(value_bytes.as_ref()) {
+                Ok(value) => Some(Ok(value)),
+                Err(e) => Some(Err(KvError::DeserError(format!("{}", e)))),
+            },
+            Some(Err(e)) => Some(Err(KvError::BackendError(e))),
+            None => None,
+        }
+    }
+}
+
+impl<C: BackendCol, K: Key, V: Value> ReversableIterator for KvIterValues<C, K, V> {
+    #[inline(always)]
+    fn reverse(self) -> Self {
+        Self {
+            range_iter: self.range_iter.reverse(),
+            phantom_key: PhantomData,
+            phantom_value: PhantomData,
+        }
+    }
+}
+
+impl<C: BackendCol, K: Key, V: Value> KvIterValues<C, K, V> {
+    pub(super) fn new(range_iter: super::range::RangeIter<C>) -> Self {
+        Self {
+            range_iter,
+            phantom_key: PhantomData,
+            phantom_value: PhantomData,
+        }
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/key.rs b/rust-libs/tools/kv_typed/src/key.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7d820dfa7b4453454f5a0642e0e76bd8dd96d929
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/key.rs
@@ -0,0 +1,62 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed Key trait
+
+use crate::*;
+
+/// Trait to be implemented by the collection key
+
+#[cfg(not(feature = "explorer"))]
+pub trait Key:
+    'static + KeyAsBytes + Debug + Eq + FromBytes + std::hash::Hash + Send + Sync + Sized
+{
+}
+
+#[cfg(feature = "explorer")]
+pub trait Key:
+    'static
+    + KeyAsBytes
+    + Debug
+    + Eq
+    + ExplorableKey
+    + FromBytes
+    + std::hash::Hash
+    + Send
+    + Sync
+    + Sized
+{
+}
+
+#[cfg(not(feature = "explorer"))]
+impl<T> Key for T where
+    T: 'static + KeyAsBytes + Debug + Eq + FromBytes + std::hash::Hash + Send + Sync + Sized
+{
+}
+
+#[cfg(feature = "explorer")]
+impl<T> Key for T where
+    T: 'static
+        + KeyAsBytes
+        + Debug
+        + Eq
+        + ExplorableKey
+        + FromBytes
+        + std::hash::Hash
+        + Send
+        + Sync
+        + Sized
+{
+}
diff --git a/rust-libs/tools/kv_typed/src/lib.rs b/rust-libs/tools/kv_typed/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2920e4c00d9cd4dfc221e91728f2b7d09330aa1f
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/lib.rs
@@ -0,0 +1,111 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! Strongly typed key-value storage
+
+#![deny(
+    clippy::unwrap_used,
+    missing_copy_implementations,
+    missing_debug_implementations,
+    trivial_casts,
+    trivial_numeric_casts,
+    unstable_features,
+    unused_import_braces,
+    unused_qualifications
+)]
+
+mod as_bytes;
+pub mod backend;
+mod batch;
+mod collection_ro;
+mod collection_rw;
+mod error;
+mod event;
+#[cfg(feature = "explorer")]
+pub mod explorer;
+mod from_bytes;
+mod iter;
+mod key;
+#[cfg(feature = "subscription")]
+mod subscription;
+mod utils;
+mod value;
+
+// Re-export dependencies
+#[cfg(feature = "async")]
+pub use async_channel as channel;
+#[cfg(all(not(feature = "async"), feature = "sync"))]
+pub use crossbeam_channel as channel;
+#[cfg(feature = "explorer")]
+pub use regex;
+
+/// Kv Typed prelude
+pub mod prelude {
+    pub use crate::as_bytes::{KeyAsBytes, ValueAsBytes};
+    #[cfg(feature = "leveldb_backend")]
+    pub use crate::backend::leveldb::{LevelDb, LevelDbConf};
+    #[cfg(feature = "memory_backend")]
+    pub use crate::backend::memory::{Mem, MemConf};
+    #[cfg(feature = "mock")]
+    pub use crate::backend::mock::{MockBackend, MockBackendCol, MockBackendIter};
+    #[cfg(feature = "sled_backend")]
+    pub use crate::backend::sled::{Config as SledConf, Sled};
+    pub use crate::backend::{Backend, BackendCol, TransactionalBackend};
+    pub use crate::batch::Batch;
+    #[cfg(feature = "mock")]
+    pub use crate::collection_ro::MockColRo;
+    pub use crate::collection_ro::{ColRo, DbCollectionRo};
+    pub use crate::collection_rw::{ColRw, DbCollectionRw};
+    pub use crate::error::{
+        DynErr, KvError, KvResult, StringErr, TransactionError, TransactionResult,
+    };
+    pub use crate::event::{EventTrait, Events};
+    #[cfg(feature = "explorer")]
+    pub use crate::explorer::{ExplorableKey, ExplorableValue};
+    pub use crate::from_bytes::FromBytes;
+    pub use crate::iter::{
+        keys::KvIterKeys, values::KvIterValues, KvIter, ResultIter, ReversableIterator,
+    };
+    pub use crate::key::Key;
+    #[cfg(feature = "subscription")]
+    pub use crate::subscription::{NewSubscribers, Subscriber, Subscribers};
+    pub use crate::value::Value;
+    pub use kv_typed_code_gen::db_schema;
+}
+
+// Internal crate imports
+pub(crate) use crate::backend::BackendBatch;
+#[cfg(feature = "explorer")]
+pub(crate) use crate::explorer::{ExplorableKey, ExplorableValue};
+pub(crate) use crate::iter::RangeBytes;
+pub(crate) use crate::prelude::*;
+#[cfg(feature = "subscription")]
+pub(crate) use crate::subscription::ColSubscribers;
+pub(crate) use crate::utils::arc::Arc;
+pub(crate) use crate::utils::ivec::IVec;
+#[cfg(feature = "async")]
+use async_channel::{unbounded, Receiver, Sender, TrySendError};
+#[cfg(all(not(feature = "async"), feature = "sync"))]
+#[allow(unused_imports)]
+use crossbeam_channel::{unbounded, Receiver, Sender, TrySendError};
+pub(crate) use smallvec::SmallVec;
+pub(crate) use std::{
+    convert::TryInto,
+    error::Error,
+    fmt::Debug,
+    marker::PhantomData,
+    ops::{Bound, RangeBounds},
+};
+pub(crate) use thiserror::Error;
diff --git a/rust-libs/tools/kv_typed/src/subscription.rs b/rust-libs/tools/kv_typed/src/subscription.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3053328907df3907704235fa313d10b847aeb641
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/subscription.rs
@@ -0,0 +1,108 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed subscription
+
+use crate::*;
+
+/// Subscriber
+pub type Subscriber<E> = Sender<Arc<Events<E>>>;
+/// Subscriptions sender
+pub(crate) type SubscriptionsSender<E> = Sender<Subscriber<E>>;
+/// Subscribers
+pub type Subscribers<E> = std::collections::BTreeMap<usize, Subscriber<E>>;
+/// New subscribers
+pub type NewSubscribers<E> = SmallVec<[Subscriber<E>; 4]>;
+
+#[derive(Debug)]
+#[doc(hidden)]
+pub struct ColSubscribers<E: EventTrait> {
+    subscription_sender: SubscriptionsSender<E>,
+    subscription_receiver: Receiver<Subscriber<E>>,
+    subscribers: Subscribers<E>,
+    subscriber_index: usize,
+}
+
+impl<E: EventTrait> Default for ColSubscribers<E> {
+    fn default() -> Self {
+        let (subscription_sender, subscription_receiver) = unbounded();
+        ColSubscribers {
+            subscription_sender,
+            subscription_receiver,
+            subscribers: std::collections::BTreeMap::new(),
+            subscriber_index: 0,
+        }
+    }
+}
+
+impl<E: EventTrait> ColSubscribers<E> {
+    pub(crate) fn get_subscription_sender(&self) -> Sender<Subscriber<E>> {
+        self.subscription_sender.clone()
+    }
+    #[inline(always)]
+    pub(crate) fn get_new_subscribers(&self) -> NewSubscribers<E> {
+        if !self.subscription_receiver.is_empty() {
+            let mut new_subscribers = SmallVec::new();
+            while let Ok(subscriber) = self.subscription_receiver.try_recv() {
+                new_subscribers.push(subscriber)
+            }
+            new_subscribers
+        } else {
+            SmallVec::new()
+        }
+    }
+    pub(crate) fn notify_subscribers(&self, events: Arc<Events<E>>) -> Vec<usize> {
+        let mut died_subscribers = Vec::with_capacity(self.subscribers.len());
+        let mut unsend_events_opt = None;
+        for (id, subscriber) in &self.subscribers {
+            if let Err(e) = subscriber.try_send(
+                unsend_events_opt
+                    .take()
+                    .unwrap_or_else(|| Arc::clone(&events)),
+            ) {
+                match e {
+                    #[cfg(feature = "async")]
+                    TrySendError::Closed(events_) => {
+                        unsend_events_opt = Some(events_);
+                        died_subscribers.push(*id);
+                    }
+                    #[cfg(not(feature = "async"))]
+                    TrySendError::Disconnected(events_) => {
+                        unsend_events_opt = Some(events_);
+                        died_subscribers.push(*id);
+                    }
+                    TrySendError::Full(events_) => {
+                        unsend_events_opt = Some(events_);
+                    }
+                }
+            }
+        }
+        died_subscribers
+    }
+    #[inline(always)]
+    pub(crate) fn add_new_subscribers(&mut self) {
+        for new_subscriber in self.get_new_subscribers() {
+            self.subscribers
+                .insert(self.subscriber_index, new_subscriber);
+            self.subscriber_index += 1;
+        }
+    }
+    #[inline(always)]
+    pub(crate) fn prune_subscribers(&mut self, died_subscribers: Vec<usize>) {
+        for died_subscriber in died_subscribers {
+            self.subscribers.remove(&died_subscriber);
+        }
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/utils.rs b/rust-libs/tools/kv_typed/src/utils.rs
new file mode 100644
index 0000000000000000000000000000000000000000..68bd74c49c3a93239ea3bb150c947044037b820e
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/utils.rs
@@ -0,0 +1,24 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed utils
+
+pub mod arc;
+#[cfg(not(feature = "sled_backend"))]
+pub mod ivec;
+#[cfg(feature = "sled_backend")]
+pub mod ivec {
+    pub use sled::IVec;
+}
diff --git a/rust-libs/tools/kv_typed/src/utils/arc.rs b/rust-libs/tools/kv_typed/src/utils/arc.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6eef0caad58526107af45b9e14d24632496d3a15
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/utils/arc.rs
@@ -0,0 +1,267 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV typed Arc
+
+#![allow(clippy::unwrap_used, dead_code, unsafe_code)]
+/// We use this because we never use the weak count on the std
+/// `Arc`, but we use a LOT of `Arc`'s, so the extra 8 bytes
+/// turn into a huge overhead.
+use std::{
+    alloc::{alloc, dealloc, Layout},
+    convert::TryFrom,
+    fmt::{self, Debug},
+    mem,
+    ops::Deref,
+    ptr,
+    sync::atomic::{AtomicUsize, Ordering},
+};
+
+// we make this repr(C) because we do a raw
+// write to the beginning where we expect
+// the rc to be.
+#[repr(C)]
+struct ArcInner<T: ?Sized> {
+    rc: AtomicUsize,
+    inner: T,
+}
+
+pub struct Arc<T: ?Sized> {
+    ptr: *mut ArcInner<T>,
+}
+
+unsafe impl<T: Send + Sync + ?Sized> Send for Arc<T> {}
+unsafe impl<T: Send + Sync + ?Sized> Sync for Arc<T> {}
+
+impl<T: Debug + ?Sized> Debug for Arc<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        Debug::fmt(&**self, f)
+    }
+}
+
+impl<T> Arc<T> {
+    pub fn new(inner: T) -> Arc<T> {
+        let bx = Box::new(ArcInner {
+            inner,
+            rc: AtomicUsize::new(1),
+        });
+        let ptr = Box::into_raw(bx);
+        Arc { ptr }
+    }
+
+    // See std::sync::arc::Arc::copy_from_slice,
+    // "Unsafe because the caller must either take ownership or bind `T: Copy`"
+    unsafe fn copy_from_slice(s: &[T]) -> Arc<[T]> {
+        let align = std::cmp::max(mem::align_of::<T>(), mem::align_of::<AtomicUsize>());
+
+        let rc_width = std::cmp::max(align, mem::size_of::<AtomicUsize>());
+        let data_width = mem::size_of::<T>().checked_mul(s.len()).unwrap();
+
+        let size_unpadded = rc_width.checked_add(data_width).unwrap();
+        // Pad size out to alignment
+        let size_padded = (size_unpadded + align - 1) & !(align - 1);
+
+        let layout = Layout::from_size_align(size_padded, align).unwrap();
+
+        let ptr = alloc(layout);
+
+        assert!(!ptr.is_null(), "failed to allocate Arc");
+        #[allow(clippy::cast_ptr_alignment)]
+        ptr::write(ptr as _, AtomicUsize::new(1));
+
+        let data_ptr = ptr.add(rc_width);
+        ptr::copy_nonoverlapping(s.as_ptr(), data_ptr as _, s.len());
+
+        let fat_ptr: *const ArcInner<[T]> = Arc::fatten(ptr, s.len());
+
+        Arc {
+            ptr: fat_ptr as *mut _,
+        }
+    }
+
+    /// <https://users.rust-lang.org/t/construct-fat-pointer-to-struct/29198/9>
+    #[allow(trivial_casts)]
+    fn fatten(data: *const u8, len: usize) -> *const ArcInner<[T]> {
+        // Requirements of slice::from_raw_parts.
+        assert!(!data.is_null());
+        assert!(isize::try_from(len).is_ok());
+
+        let slice = unsafe { core::slice::from_raw_parts(data as *const (), len) };
+        slice as *const [()] as *const _
+    }
+
+    pub fn into_raw(arc: Arc<T>) -> *const T {
+        let ptr = unsafe { &(*arc.ptr).inner };
+        #[allow(clippy::mem_forget)]
+        mem::forget(arc);
+        ptr
+    }
+
+    pub unsafe fn from_raw(ptr: *const T) -> Arc<T> {
+        let align = std::cmp::max(mem::align_of::<T>(), mem::align_of::<AtomicUsize>());
+
+        let rc_width = std::cmp::max(align, mem::size_of::<AtomicUsize>());
+
+        let sub_ptr = (ptr as *const u8).sub(rc_width) as *mut ArcInner<T>;
+
+        Arc { ptr: sub_ptr }
+    }
+}
+
+impl<T: ?Sized> Arc<T> {
+    pub fn strong_count(arc: &Arc<T>) -> usize {
+        unsafe { (*arc.ptr).rc.load(Ordering::Acquire) }
+    }
+
+    pub fn get_mut(arc: &mut Arc<T>) -> Option<&mut T> {
+        if Arc::strong_count(arc) == 1 {
+            Some(unsafe { &mut arc.ptr.as_mut().unwrap().inner })
+        } else {
+            None
+        }
+    }
+}
+
+impl<T: ?Sized + Clone> Arc<T> {
+    pub fn make_mut(arc: &mut Arc<T>) -> &mut T {
+        if Arc::strong_count(arc) != 1 {
+            *arc = Arc::new((**arc).clone());
+            assert_eq!(Arc::strong_count(arc), 1);
+        }
+        Arc::get_mut(arc).unwrap()
+    }
+}
+
+impl<T: Default> Default for Arc<T> {
+    fn default() -> Arc<T> {
+        Arc::new(T::default())
+    }
+}
+
+impl<T: ?Sized> Clone for Arc<T> {
+    fn clone(&self) -> Arc<T> {
+        // safe to use Relaxed ordering below because
+        // of the required synchronization for passing
+        // any objects to another thread.
+        let last_count = unsafe { (*self.ptr).rc.fetch_add(1, Ordering::Relaxed) };
+
+        if last_count == usize::max_value() {
+            #[cold]
+            std::process::abort();
+        }
+
+        Arc { ptr: self.ptr }
+    }
+}
+
+impl<T: ?Sized> Drop for Arc<T> {
+    fn drop(&mut self) {
+        unsafe {
+            let rc = (*self.ptr).rc.fetch_sub(1, Ordering::Release) - 1;
+            if rc == 0 {
+                std::sync::atomic::fence(Ordering::Acquire);
+                Box::from_raw(self.ptr);
+            }
+        }
+    }
+}
+
+impl<T: Copy> From<&[T]> for Arc<[T]> {
+    #[inline]
+    fn from(s: &[T]) -> Arc<[T]> {
+        unsafe { Arc::copy_from_slice(s) }
+    }
+}
+
+#[allow(clippy::fallible_impl_from)]
+impl<T> From<Box<[T]>> for Arc<[T]> {
+    #[inline]
+    fn from(b: Box<[T]>) -> Arc<[T]> {
+        let len = b.len();
+        unsafe {
+            let src = Box::into_raw(b);
+            let value_layout = Layout::for_value(&*src);
+            let align = std::cmp::max(value_layout.align(), mem::align_of::<AtomicUsize>());
+            let rc_width = std::cmp::max(align, mem::size_of::<AtomicUsize>());
+            let unpadded_size = rc_width.checked_add(value_layout.size()).unwrap();
+            // pad the total `Arc` allocation size to the alignment of
+            // `max(value, AtomicUsize)`
+            let size = (unpadded_size + align - 1) & !(align - 1);
+            let dst_layout = Layout::from_size_align(size, align).unwrap();
+            let dst = alloc(dst_layout);
+            assert!(!dst.is_null(), "failed to allocate Arc");
+
+            #[allow(clippy::cast_ptr_alignment)]
+            ptr::write(dst as _, AtomicUsize::new(1));
+            let data_ptr = dst.add(rc_width);
+            ptr::copy_nonoverlapping(src as *const u8, data_ptr, value_layout.size());
+
+            // free the old box memory without running Drop
+            if value_layout.size() != 0 {
+                dealloc(src as *mut u8, value_layout);
+            }
+
+            let fat_ptr: *const ArcInner<[T]> = Arc::fatten(dst, len);
+
+            Arc {
+                ptr: fat_ptr as *mut _,
+            }
+        }
+    }
+}
+
+#[test]
+fn boxed_slice_to_arc_slice() {
+    let box1: Box<[u8]> = Box::new([1, 2, 3]);
+    let arc1: Arc<[u8]> = box1.into();
+    assert_eq!(&*arc1, &*vec![1, 2, 3]);
+    let box2: Box<[u64]> = Box::new([1, 2, 3]);
+    let arc2: Arc<[u64]> = box2.into();
+    assert_eq!(&*arc2, &*vec![1, 2, 3]);
+}
+
+impl<T> From<Vec<T>> for Arc<[T]> {
+    #[inline]
+    fn from(mut v: Vec<T>) -> Arc<[T]> {
+        unsafe {
+            let arc = Arc::copy_from_slice(&v);
+
+            // Allow the Vec to free its memory, but not destroy its contents
+            v.set_len(0);
+
+            arc
+        }
+    }
+}
+
+impl<T: ?Sized> Deref for Arc<T> {
+    type Target = T;
+
+    fn deref(&self) -> &T {
+        unsafe { &(*self.ptr).inner }
+    }
+}
+
+impl<T: ?Sized> std::borrow::Borrow<T> for Arc<T> {
+    fn borrow(&self) -> &T {
+        &**self
+    }
+}
+
+impl<T: ?Sized> AsRef<T> for Arc<T> {
+    fn as_ref(&self) -> &T {
+        &**self
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/utils/ivec.rs b/rust-libs/tools/kv_typed/src/utils/ivec.rs
new file mode 100644
index 0000000000000000000000000000000000000000..50a8d7528ea75e7f59b839a7326f7eff7a2ba104
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/utils/ivec.rs
@@ -0,0 +1,314 @@
+use std::{
+    convert::TryFrom,
+    fmt,
+    hash::{Hash, Hasher},
+    ops::{Deref, DerefMut},
+    sync::Arc,
+};
+
+const CUTOFF: usize = 22;
+
+type Inner = [u8; CUTOFF];
+
+/// A buffer that may either be inline or remote and protected
+/// by an Arc
+#[derive(Clone)]
+pub struct IVec(IVecInner);
+
+impl Default for IVec {
+    fn default() -> Self {
+        Self::from(&[])
+    }
+}
+
+impl Hash for IVec {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.deref().hash(state);
+    }
+}
+
+#[derive(Clone)]
+enum IVecInner {
+    Inline(u8, Inner),
+    Remote(Arc<[u8]>),
+    Subslice {
+        base: Arc<[u8]>,
+        offset: usize,
+        len: usize,
+    },
+}
+
+const fn is_inline_candidate(length: usize) -> bool {
+    length <= CUTOFF
+}
+
+impl IVec {
+    pub fn subslice(&self, slice_offset: usize, len: usize) -> Self {
+        assert!(self.len().checked_sub(slice_offset).expect("") >= len);
+
+        let inner = match self.0 {
+            IVecInner::Remote(ref base) => IVecInner::Subslice {
+                base: base.clone(),
+                offset: slice_offset,
+                len,
+            },
+            IVecInner::Inline(_, old_inner) => {
+                // old length already checked above in assertion
+                let mut new_inner = Inner::default();
+                new_inner[..len].copy_from_slice(&old_inner[slice_offset..slice_offset + len]);
+
+                IVecInner::Inline(u8::try_from(len).expect(""), new_inner)
+            }
+            IVecInner::Subslice {
+                ref base,
+                ref offset,
+                ..
+            } => IVecInner::Subslice {
+                base: base.clone(),
+                offset: offset + slice_offset,
+                len,
+            },
+        };
+
+        IVec(inner)
+    }
+
+    fn inline(slice: &[u8]) -> Self {
+        assert!(is_inline_candidate(slice.len()));
+
+        let mut data = Inner::default();
+
+        #[allow(unsafe_code)]
+        unsafe {
+            std::ptr::copy_nonoverlapping(slice.as_ptr(), data.as_mut_ptr(), slice.len());
+        }
+
+        Self(IVecInner::Inline(
+            u8::try_from(slice.len()).expect(""),
+            data,
+        ))
+    }
+
+    fn remote(arc: Arc<[u8]>) -> Self {
+        Self(IVecInner::Remote(arc))
+    }
+
+    fn make_mut(&mut self) {
+        match self.0 {
+            IVecInner::Remote(ref mut buf) if Arc::strong_count(buf) != 1 => {
+                self.0 = IVecInner::Remote(buf.to_vec().into());
+            }
+            IVecInner::Subslice {
+                ref mut base,
+                offset,
+                len,
+            } if Arc::strong_count(base) != 1 => {
+                self.0 = IVecInner::Remote(base[offset..offset + len].to_vec().into());
+            }
+            _ => {}
+        }
+    }
+}
+
+impl From<Box<[u8]>> for IVec {
+    fn from(b: Box<[u8]>) -> Self {
+        if is_inline_candidate(b.len()) {
+            Self::inline(&b)
+        } else {
+            Self::remote(Arc::from(b))
+        }
+    }
+}
+
+impl From<&[u8]> for IVec {
+    fn from(slice: &[u8]) -> Self {
+        if is_inline_candidate(slice.len()) {
+            Self::inline(slice)
+        } else {
+            Self::remote(Arc::from(slice))
+        }
+    }
+}
+
+impl From<Arc<[u8]>> for IVec {
+    fn from(arc: Arc<[u8]>) -> Self {
+        if is_inline_candidate(arc.len()) {
+            Self::inline(&arc)
+        } else {
+            Self::remote(arc)
+        }
+    }
+}
+
+impl From<&IVec> for IVec {
+    fn from(v: &Self) -> Self {
+        v.clone()
+    }
+}
+
+impl std::borrow::Borrow<[u8]> for IVec {
+    fn borrow(&self) -> &[u8] {
+        self.as_ref()
+    }
+}
+
+impl std::borrow::Borrow<[u8]> for &IVec {
+    fn borrow(&self) -> &[u8] {
+        self.as_ref()
+    }
+}
+
+macro_rules! from_array {
+    ($($s:expr),*) => {
+        $(
+            impl From<&[u8; $s]> for IVec {
+                fn from(v: &[u8; $s]) -> Self {
+                    Self::from(&v[..])
+                }
+            }
+        )*
+    }
+}
+
+from_array!(
+    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
+    26, 27, 28, 29, 30, 31, 32
+);
+
+impl Into<Arc<[u8]>> for IVec {
+    fn into(self) -> Arc<[u8]> {
+        match self.0 {
+            IVecInner::Inline(..) => Arc::from(self.as_ref()),
+            IVecInner::Remote(arc) => arc,
+            IVecInner::Subslice { .. } => self.deref().into(),
+        }
+    }
+}
+
+impl Deref for IVec {
+    type Target = [u8];
+
+    #[inline]
+    fn deref(&self) -> &[u8] {
+        self.as_ref()
+    }
+}
+
+impl AsRef<[u8]> for IVec {
+    #[inline]
+    #[allow(unsafe_code)]
+    fn as_ref(&self) -> &[u8] {
+        match &self.0 {
+            IVecInner::Inline(sz, buf) => unsafe { buf.get_unchecked(..*sz as usize) },
+            IVecInner::Remote(buf) => buf,
+            IVecInner::Subslice {
+                ref base,
+                offset,
+                len,
+            } => &base[*offset..*offset + *len],
+        }
+    }
+}
+
+impl DerefMut for IVec {
+    #[inline]
+    fn deref_mut(&mut self) -> &mut [u8] {
+        self.as_mut()
+    }
+}
+
+impl AsMut<[u8]> for IVec {
+    #[inline]
+    #[allow(unsafe_code)]
+    fn as_mut(&mut self) -> &mut [u8] {
+        self.make_mut();
+
+        match &mut self.0 {
+            IVecInner::Inline(ref sz, ref mut buf) => unsafe {
+                std::slice::from_raw_parts_mut(buf.as_mut_ptr(), *sz as usize)
+            },
+            IVecInner::Remote(ref mut buf) => Arc::get_mut(buf).expect(""),
+            IVecInner::Subslice {
+                ref mut base,
+                offset,
+                len,
+            } => &mut Arc::get_mut(base).expect("")[*offset..*offset + *len],
+        }
+    }
+}
+
+impl Ord for IVec {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.as_ref().cmp(other.as_ref())
+    }
+}
+
+impl PartialOrd for IVec {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl<T: AsRef<[u8]>> PartialEq<T> for IVec {
+    fn eq(&self, other: &T) -> bool {
+        self.as_ref() == other.as_ref()
+    }
+}
+
+impl PartialEq<[u8]> for IVec {
+    fn eq(&self, other: &[u8]) -> bool {
+        self.as_ref() == other
+    }
+}
+
+impl Eq for IVec {}
+
+impl fmt::Debug for IVec {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.as_ref().fmt(f)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn ivec_usage() {
+        let iv2 = IVec::from(&[4; 128][..]);
+        assert_eq!(iv2, vec![4; 128]);
+    }
+
+    #[test]
+    fn boxed_slice_conversion() {
+        let boite1: Box<[u8]> = Box::new([1, 2, 3]);
+        let iv1: IVec = boite1.into();
+        assert_eq!(iv1, vec![1, 2, 3]);
+        let boite2: Box<[u8]> = Box::new([4; 128]);
+        let iv2: IVec = boite2.into();
+        assert_eq!(iv2, vec![4; 128]);
+    }
+
+    #[test]
+    #[should_panic]
+    fn subslice_usage_00() {
+        let iv1 = IVec::from(&[1, 2, 3][..]);
+        let _subslice = iv1.subslice(0, 4);
+    }
+
+    #[test]
+    #[should_panic]
+    fn subslice_usage_01() {
+        let iv1 = IVec::from(&[1, 2, 3][..]);
+        let _subslice = iv1.subslice(3, 1);
+    }
+
+    #[test]
+    fn ivec_as_mut_identity() {
+        let initial = &[1];
+        let mut iv = IVec::from(initial);
+        assert_eq!(&*initial, &*iv);
+        assert_eq!(&*initial, &mut *iv);
+        assert_eq!(&*initial, iv.as_mut());
+    }
+}
diff --git a/rust-libs/tools/kv_typed/src/value.rs b/rust-libs/tools/kv_typed/src/value.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2681c1457ac3100cb96231dc6f2e524e5c67bd8e
--- /dev/null
+++ b/rust-libs/tools/kv_typed/src/value.rs
@@ -0,0 +1,59 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! KV Typed Value trait
+
+use crate::*;
+
+/// Trait to be implemented by the collection value
+#[cfg(not(feature = "explorer"))]
+pub trait Value:
+    'static + ValueAsBytes + Debug + FromBytes + PartialEq + Send + Sync + Sized
+{
+}
+
+#[cfg(feature = "explorer")]
+pub trait Value:
+    'static + ValueAsBytes + Debug + ExplorableValue + FromBytes + PartialEq + Send + Sync + Sized
+{
+}
+
+#[cfg(not(feature = "explorer"))]
+impl<T> Value for T where
+    T: 'static + ValueAsBytes + Debug + FromBytes + PartialEq + Send + Sync + Sized
+{
+}
+
+#[cfg(feature = "explorer")]
+impl<T> Value for T where
+    T: 'static
+        + ValueAsBytes
+        + Debug
+        + ExplorableValue
+        + FromBytes
+        + PartialEq
+        + Send
+        + Sync
+        + Sized
+{
+}
+
+impl FromBytes for String {
+    type Err = std::str::Utf8Error;
+
+    fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Err> {
+        Ok(std::str::from_utf8(bytes)?.to_owned())
+    }
+}
diff --git a/rust-libs/tools/kv_typed/tests/db_schema.rs b/rust-libs/tools/kv_typed/tests/db_schema.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c3594138496e66f37582cfcd5bbe3b649aa39e96
--- /dev/null
+++ b/rust-libs/tools/kv_typed/tests/db_schema.rs
@@ -0,0 +1,73 @@
+#[cfg(feature = "memory_backend")]
+mod tests {
+    use kv_typed::prelude::*;
+    use std::fmt::Debug;
+
+    db_schema!(
+        TestV1,
+        [
+            ["c1", col_1, i32, String],
+            ["c2", col_2, usize, i128],
+            ["c2", col_3, u64, u128],
+        ]
+    );
+
+    #[maybe_async::test(not(feature = "async"), async(feature = "async", async_std::test))]
+    async fn test_db_schema() -> KvResult<()> {
+        let db = TestV1Db::<Mem>::open(MemConf::default())?;
+
+        #[cfg(feature = "subscription")]
+        let (sender, recv) = kv_typed::channel::unbounded();
+        #[cfg(feature = "subscription")]
+        db.col_1().subscribe(sender)?;
+
+        let db2 = db.clone();
+
+        let handler = std::thread::spawn(move || db2.col_1_write().upsert(3, "toto".to_owned()));
+        handler.join().expect("thread panic")?;
+
+        #[cfg(feature = "subscription")]
+        {
+            let expected_events: Events<Col1Event> = smallvec::smallvec![Col1Event::Upsert {
+                key: 3,
+                value: "toto".to_owned(),
+            }];
+            #[allow(unused_parens)]
+            if let Ok(msg) = recv.recv().await {
+                assert_eq!(msg.as_ref(), &expected_events,)
+            } else {
+                panic!("must be receive event")
+            }
+        }
+
+        assert_eq!(db.col_1().get(&3)?, Some("toto".to_owned()),);
+
+        assert_eq!(db.col_2().get(&3)?, None,);
+
+        db.col_1_write().upsert(5, "tutu".to_owned())?;
+
+        {
+            let mut iter = db.col_1().iter(..);
+
+            assert_eq!(iter.next_res()?, Some((3, "toto".to_owned())));
+            assert_eq!(iter.next_res()?, Some((5, "tutu".to_owned())));
+            assert_eq!(iter.next_res()?, None);
+
+            let mut iter = db.col_1().iter(..).values().reverse();
+
+            assert_eq!(iter.next_res()?, Some("tutu".to_owned()));
+            assert_eq!(iter.next_res()?, Some("toto".to_owned()));
+            assert_eq!(iter.next_res()?, None);
+        }
+
+        db.col_1_write().upsert(7, "titi".to_owned())?;
+
+        let mut iter = db.col_1().iter(..).values().reverse().step_by(2);
+
+        assert_eq!(iter.next_res()?, Some("titi".to_owned()));
+        assert_eq!(iter.next_res()?, Some("toto".to_owned()));
+        assert_eq!(iter.next_res()?, None);
+
+        Ok(())
+    }
+}
diff --git a/rust-libs/tools/kv_typed/tests/test_mock.rs b/rust-libs/tools/kv_typed/tests/test_mock.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8f68164a295009d6fc9c5f0e2ce2dbfa7cdad678
--- /dev/null
+++ b/rust-libs/tools/kv_typed/tests/test_mock.rs
@@ -0,0 +1,54 @@
+#[cfg(feature = "mock")]
+mod tests {
+
+    use kv_typed::prelude::*;
+    use mockall::predicate::*;
+    use std::fmt::Debug;
+    use std::ops::{Bound, RangeFull};
+
+    db_schema!(
+        Test,
+        [["c1", col_1, u32, String], ["c2", col_2, String, u64],]
+    );
+
+    fn use_readable_db<DB: TestDbReadable>(db: &DB) -> KvResult<()> {
+        let col1_reader = db.col_1();
+        assert_eq!(col1_reader.count()?, 899);
+        assert_eq!(col1_reader.get(&42)?, Some("toto".to_owned()));
+        let mut iter = col1_reader.iter(..);
+        assert_eq!(iter.next_res()?, Some((42, "toto".to_owned())));
+        assert_eq!(iter.next_res()?, None);
+        Ok(())
+    }
+
+    #[test]
+    fn test_mock_db() -> KvResult<()> {
+        let mut db = MockTestDbReadable::new();
+        db.expect_col_1().times(1).returning(|| {
+            let mut col_1 = MockColRo::<Col1Event>::new();
+            col_1.expect_count().times(1).returning(|| Ok(899));
+            col_1
+                .expect_get()
+                .times(1)
+                .returning(|_| Ok(Some("toto".to_owned())));
+            col_1.expect_iter::<RangeFull>().times(1).returning(|_| {
+                let mut b_iter = MockBackendIter::new();
+                #[allow(clippy::string_lit_as_bytes)]
+                let mut items = vec![
+                    None,
+                    Some(Ok((vec![0u8, 0, 0, 42], "toto".as_bytes().to_vec()))),
+                ];
+                b_iter
+                    .expect_next()
+                    .times(2)
+                    .returning(move || items.pop().unwrap_or(None));
+                KvIter::new(b_iter, (Bound::Unbounded, Bound::Unbounded))
+            });
+            col_1
+        });
+
+        use_readable_db(&db)?;
+
+        Ok(())
+    }
+}
diff --git a/rust-libs/tools/kv_typed_code_gen/Cargo.toml b/rust-libs/tools/kv_typed_code_gen/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..6f09757d612f13252c5a3c8aab458c5e680f93cc
--- /dev/null
+++ b/rust-libs/tools/kv_typed_code_gen/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "kv_typed_code_gen"
+version = "0.1.0"
+authors = ["elois <c@elo.tf>"]
+description = "Macros to generate code for kv_typed users"
+repository = "https://git.duniter.org/nodes/typescript/duniter"
+keywords = ["database", "macro", "key", "sled"]
+license = "AGPL-3.0"
+edition = "2018"
+
+[lib]
+proc-macro = true
+path = "src/lib.rs"
+
+[dependencies]
+Inflector = "0.11.4"
+proc-macro2 = "1.0.20"
+quote = "1.0"
+syn = { version = "1.0.39", features = ["full"] }
diff --git a/rust-libs/tools/kv_typed_code_gen/src/col_schema.rs b/rust-libs/tools/kv_typed_code_gen/src/col_schema.rs
new file mode 100644
index 0000000000000000000000000000000000000000..085d0839a085a7ad9dd5ff47c1d853425f5cf697
--- /dev/null
+++ b/rust-libs/tools/kv_typed_code_gen/src/col_schema.rs
@@ -0,0 +1,81 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+pub(crate) struct ColSchema {
+    pub(crate) name_class: Ident,
+    pub(crate) name_snake: Ident,
+    pub(crate) path: LitStr,
+    pub(crate) key_ty: Ident,
+    pub(crate) value_ty: Ident,
+}
+
+impl From<ExprArray> for ColSchema {
+    fn from(expr_array: ExprArray) -> Self {
+        let mut expr_iter = expr_array.elems.into_iter();
+
+        let col_path = if let Some(Expr::Lit(ExprLit { lit, .. })) = expr_iter.next() {
+            if let Lit::Str(lit_str) = lit {
+                lit_str
+            } else {
+                panic!("Collection must be defined by array of one literal followed by three identifiers");
+            }
+        } else {
+            panic!(
+                "Collection must be defined by array of one literal followed by three identifiers"
+            );
+        };
+        let col_name = if let Some(Expr::Path(ExprPath { path, .. })) = expr_iter.next() {
+            path.get_ident()
+                .expect("Collection name must be a plain identifier")
+                .to_owned()
+        } else {
+            panic!(
+                "Collection must be defined by array of one literal followed by three identifiers"
+            );
+        };
+        let key_ty = if let Some(Expr::Path(ExprPath { path, .. })) = expr_iter.next() {
+            path.get_ident()
+                .expect("Collection key type must be a plain identifier")
+                .to_owned()
+        } else {
+            panic!(
+                "Collection must be defined by array of one literal followed by three identifiers"
+            );
+        };
+        let value_ty = if let Some(Expr::Path(ExprPath { path, .. })) = expr_iter.next() {
+            path.get_ident()
+                .expect("Collection value type must be a plain identifier")
+                .to_owned()
+        } else {
+            panic!(
+                "Collection must be defined by array of one literal followed by three identifiers"
+            );
+        };
+        if expr_iter.next().is_some() {
+            panic!(
+                "Collection must be defined by array of one literal followed by three identifiers"
+            );
+        }
+        ColSchema {
+            path: col_path,
+            name_class: format_ident!("{}", ident_to_class_case_string(&col_name)),
+            name_snake: format_ident!("{}", ident_to_snake_case_string(&col_name)),
+            key_ty,
+            value_ty,
+        }
+    }
+}
diff --git a/rust-libs/tools/kv_typed_code_gen/src/db_readable.rs b/rust-libs/tools/kv_typed_code_gen/src/db_readable.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9773269a951f47c9ca1f2adf5ff71652bf4c4535
--- /dev/null
+++ b/rust-libs/tools/kv_typed_code_gen/src/db_readable.rs
@@ -0,0 +1,49 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[allow(clippy::too_many_arguments)]
+pub(crate) fn impl_db_readable(
+    db: &Ident,
+    db_ro: &Ident,
+    db_readable: &Ident,
+    col_name_class: &[Ident],
+    col_field: &[Ident],
+    col_event_type: &[Ident],
+    col_key_type: &[Ident],
+    col_value_type: &[Ident],
+) -> proc_macro2::TokenStream {
+    quote! {
+        pub trait #db_readable: Sized {
+            type Backend: Backend;
+            #(type #col_name_class: DbCollectionRo<Event=#col_event_type, K=#col_key_type, V=#col_value_type>;)*
+
+            #(fn #col_field(&self) -> Self::#col_name_class;)*
+        }
+        impl<B: Backend> #db_readable for #db<B> {
+            type Backend = B;
+            #(type #col_name_class = ColRo<B::Col, #col_event_type>;)*
+
+            #(fn #col_field(&self) -> Self::#col_name_class { self.collections.#col_field.to_ro().clone() })*
+        }
+        impl<B: Backend> #db_readable for #db_ro<B>{
+            type Backend = B;
+            #(type #col_name_class = ColRo<B::Col, #col_event_type>;)*
+
+            #(fn #col_field(&self) -> Self::#col_name_class { self.collections.#col_field.clone() })*
+        }
+    }
+}
diff --git a/rust-libs/tools/kv_typed_code_gen/src/db_schema.rs b/rust-libs/tools/kv_typed_code_gen/src/db_schema.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c60ee621f3f3793847619f36fad00597df205152
--- /dev/null
+++ b/rust-libs/tools/kv_typed_code_gen/src/db_schema.rs
@@ -0,0 +1,48 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+/// Parses the following syntax
+///
+/// DbVersion, [collection_name, ColKeyType, ColValueType]
+///
+pub(crate) struct DbSchema {
+    pub(crate) version: Ident,
+    pub(crate) collections: VecExprArray,
+}
+
+pub(crate) struct VecExprArray(pub(crate) Vec<ExprArray>);
+
+impl Parse for VecExprArray {
+    fn parse(input: ParseStream) -> Result<Self> {
+        let tmp = Punctuated::<ExprArray, Token![,]>::parse_terminated(input)?;
+        Ok(VecExprArray(tmp.into_iter().collect()))
+    }
+}
+
+impl Parse for DbSchema {
+    fn parse(input: ParseStream) -> Result<Self> {
+        let version: Ident = input.parse()?;
+        input.parse::<Token![,]>()?;
+        let collections_group: Group = input.parse()?;
+        let collections: VecExprArray = syn::parse(collections_group.stream().into())?;
+
+        Ok(DbSchema {
+            version,
+            collections,
+        })
+    }
+}
diff --git a/rust-libs/tools/kv_typed_code_gen/src/db_writable.rs b/rust-libs/tools/kv_typed_code_gen/src/db_writable.rs
new file mode 100644
index 0000000000000000000000000000000000000000..01c7e157bf49e0d11de7f1372fc58ed99ba42737
--- /dev/null
+++ b/rust-libs/tools/kv_typed_code_gen/src/db_writable.rs
@@ -0,0 +1,85 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+use crate::*;
+
+#[allow(clippy::too_many_arguments)]
+pub(crate) fn impl_db_writable(
+    db: &Ident,
+    db_ro: &Ident,
+    db_readable: &Ident,
+    db_writable: &Ident,
+    col_name_class: &[Ident],
+    col_field: &[Ident],
+    col_path: &[LitStr],
+    col_event_type: &[Ident],
+) -> proc_macro2::TokenStream {
+    let db_batch = format_ident!("{}Batch", db);
+    let db_cols_rw = format_ident!("{}ColsRw", db);
+    let col_method_rw: Vec<Ident> = col_field
+        .iter()
+        .map(|field_name| format_ident!("{}_write", field_name))
+        .collect();
+    quote! {
+        pub trait #db_writable: #db_readable {
+            type Backend: Backend;
+            type Batch;
+            #(type #col_name_class: DbCollectionRw;)*
+            type DbRo: Sized;
+
+            fn get_ro_handler(&self) -> Self::DbRo;
+            fn open(
+                backend_conf: <<Self as #db_writable>::Backend as kv_typed::backend::Backend>::Conf,
+            ) -> KvResult <Self>;
+            fn new_batch(&self) -> Self::Batch;
+            fn write_batch(&self, batch: Self::Batch) -> KvResult<()>;
+            #(fn #col_method_rw(&self) -> &Self::#col_name_class;)*
+        }
+        impl<B: Backend> #db_writable for #db<B> {
+            type Backend = B;
+            type Batch = #db_batch<B>;
+            #(type #col_name_class = ColRw<B::Col, #col_event_type>;)*
+            type DbRo = #db_ro<B>;
+
+            #[inline(always)]
+            fn get_ro_handler(&self) -> Self::DbRo {
+                #db_ro {
+                    collections: self.collections.to_ro(),
+                }
+            }
+            #[inline(always)]
+            fn new_batch(&self) -> Self::Batch {
+                <#db_batch::<B>>::default()
+            }
+            fn write_batch(&self, batch: Self::Batch) -> KvResult<()> {
+                #(self.collections.#col_field.write_batch(batch.#col_field)?;)*
+                Ok(())
+            }
+            fn open(
+                backend_conf: <<Self as #db_writable>::Backend as kv_typed::backend::Backend>::Conf,
+            ) -> KvResult <Self> {
+                let mut db = B::open(&backend_conf)?;
+                Ok(#db {
+                    collections: #db_cols_rw {
+                        #(#col_field: <ColRw<B::Col, #col_event_type>>::new(
+                            db.open_col(&backend_conf, #col_path)?
+                        ),)*
+                    },
+                })
+            }
+            #(fn #col_method_rw(&self) -> &ColRw<B::Col, #col_event_type> { &self.collections.#col_field })*
+        }
+    }
+}
diff --git a/rust-libs/tools/kv_typed_code_gen/src/lib.rs b/rust-libs/tools/kv_typed_code_gen/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b93df173fd8d2978f71744ae1ef4494bb86a9c3c
--- /dev/null
+++ b/rust-libs/tools/kv_typed_code_gen/src/lib.rs
@@ -0,0 +1,236 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// 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/>.
+
+//! Strongly typed key-value storage
+//!
+//! Macro to generate database code from their schema
+
+mod col_schema;
+mod db_readable;
+mod db_schema;
+mod db_writable;
+
+use inflector::Inflector;
+use proc_macro::TokenStream;
+use proc_macro2::Group;
+use quote::{format_ident, quote};
+use syn::{
+    parse::{Parse, ParseStream, Result},
+    parse_macro_input,
+    punctuated::Punctuated,
+    Expr, ExprArray, ExprLit, ExprPath, Ident, Lit, LitStr, Token,
+};
+
+use crate::col_schema::ColSchema;
+use crate::db_readable::impl_db_readable;
+use crate::db_schema::DbSchema;
+use crate::db_writable::impl_db_writable;
+
+fn ident_to_class_case_string(ident: &Ident) -> String {
+    Inflector::to_class_case(&format!("{}", ident))
+}
+fn ident_to_snake_case_string(ident: &Ident) -> String {
+    Inflector::to_snake_case(&format!("{}", ident))
+}
+
+#[allow(clippy::let_and_return)]
+#[proc_macro]
+pub fn db_schema(input: TokenStream) -> TokenStream {
+    let db_schema = parse_macro_input!(input as DbSchema);
+
+    let version = db_schema.version;
+    let name = ident_to_snake_case_string(&version);
+    let collections: Vec<ColSchema> = db_schema
+        .collections
+        .0
+        .into_iter()
+        .map(Into::into)
+        .collect();
+
+    let db = format_ident!("{}Db", version);
+    let db_batch = format_ident!("{}Batch", db);
+    let db_ro = format_ident!("{}Ro", db);
+    let db_cols_ro = format_ident!("{}ColsRo", db);
+    let db_cols_rw = format_ident!("{}ColsRw", db);
+
+    let col_path: Vec<LitStr> = collections.iter().map(|col| col.path.clone()).collect();
+    let col_field: Vec<Ident> = collections
+        .iter()
+        .map(|col| col.name_snake.clone())
+        .collect();
+    let col_event_type: Vec<Ident> = collections
+        .iter()
+        .map(|col| format_ident!("{}Event", &col.name_class))
+        .collect();
+    let col_key_type: Vec<Ident> = collections.iter().map(|col| col.key_ty.clone()).collect();
+    let col_name_class: Vec<Ident> = collections
+        .iter()
+        .map(|col| col.name_class.clone())
+        .collect();
+    let col_name_class_rw: Vec<Ident> = collections
+        .iter()
+        .map(|col| format_ident!("{}Rw", &col.name_class))
+        .collect();
+    let col_value_type: Vec<Ident> = collections.iter().map(|col| col.value_ty.clone()).collect();
+
+    /*let define_each_db_collection: Vec<proc_macro2::TokenStream> =
+    collections.iter().map(define_db_collection).collect();*/
+    let db_readable = format_ident!("{}Readable", db);
+    let impl_db_readable = impl_db_readable(
+        &db,
+        &db_ro,
+        &db_readable,
+        &col_name_class,
+        &col_field,
+        &col_event_type,
+        &col_key_type,
+        &col_value_type,
+    );
+    let db_writable = format_ident!("{}Writable", db);
+    let impl_db_writable = impl_db_writable(
+        &db,
+        &db_ro,
+        &db_readable,
+        &db_writable,
+        &col_name_class_rw,
+        &col_field,
+        &col_path,
+        &col_event_type,
+    );
+
+    let expanded = quote! {
+        pub use __inner::{#db, #db_batch, #db_ro, #db_readable, #db_writable};
+        #(
+            // Define each collection event type
+            #[derive(Debug, PartialEq)]
+            pub enum #col_event_type {
+                Upsert { key: #col_key_type, value: #col_value_type },
+                Remove { key: #col_key_type },
+            }
+            impl EventTrait for #col_event_type {
+                type K = #col_key_type;
+                type V = #col_value_type;
+
+                fn upsert(k: Self::K, v: Self::V) -> Self { Self::Upsert { key: k, value: v, } }
+                fn remove(k: Self::K) -> Self { Self::Remove { key: k } }
+            }
+        )*
+        // Mocks
+        #[cfg(feature = "mock")]
+        mockall::mock! {
+            pub #db_readable {}
+
+            trait #db_readable {
+                type Backend = kv_typed::backend::mock::MockBackend;
+                #(type #col_name_class = kv_typed::prelude::MockColRo<#col_event_type>;)*
+
+                #(fn #col_field(&self) -> kv_typed::prelude::MockColRo<#col_event_type>;)*
+            }
+        }
+        // Inner module used to hide internals types that must not be exposed on public api
+        mod __inner {
+            use super::*;
+            // DbCollections
+            #[derive(Clone, Debug)]
+            pub struct #db_cols_ro<BC: BackendCol> {
+                #(#col_field: ColRo<BC, #col_event_type>,)*
+            }
+            #[derive(Clone, Debug)]
+            pub struct #db_cols_rw<BC: BackendCol> {
+                #(#col_field: ColRw<BC, #col_event_type>,)*
+            }
+            impl<BC: BackendCol> #db_cols_rw<BC> {
+                fn to_ro(&self) -> #db_cols_ro<BC> {
+                    #db_cols_ro {
+                        #(#col_field: self.#col_field.to_ro().clone(),)*
+                    }
+                }
+            }
+            // Db
+            #[derive(Debug)]
+            pub struct #db<B: Backend> {
+                collections: #db_cols_rw<B::Col>,
+            }
+            impl<B: Backend> #db<B> {
+                pub const NAME: &'static str = #name;
+            }
+            impl<B: Backend> Clone for #db<B> {
+                fn clone(&self) -> Self {
+                    #db {
+                        collections: self.collections.clone(),
+                    }
+                }
+            }
+            #[cfg(feature = "explorer")]
+            use kv_typed::prelude::StringErr;
+            #[cfg(feature = "explorer")]
+            impl<B: Backend> kv_typed::explorer::DbExplorable for #db<B> {
+                fn explore<'a>(
+                    &self,
+                    collection_name: &str,
+                    action: kv_typed::explorer::ExplorerAction<'a>,
+                    stringify_json_value: fn(serde_json::Value) -> serde_json::Value,
+                ) -> KvResult<std::result::Result<kv_typed::explorer::ExplorerActionResponse, StringErr>> {
+                    #( if stringify!(#col_field) == collection_name {
+                        return action.exec(&self.collections.#col_field, stringify_json_value);
+                    } )*
+                    Ok(Err(StringErr(format!("collection '{}' not exist in database '{}'.", collection_name, stringify!(#db)))))
+                }
+                fn list_collections(&self) -> Vec<(&'static str, &'static str, &'static str)> {
+                    vec![
+                        #((stringify!(#col_field), stringify!(#col_key_type), stringify!(#col_value_type)),)*
+                    ]
+                }
+            }
+            pub struct #db_batch<B: Backend> {
+                #(#col_field: Batch<B::Col, ColRw<B::Col, #col_event_type>>,)*
+            }
+            impl<B: Backend> Default for #db_batch<B> {
+                fn default() -> Self {
+                    #db_batch {
+                        #(#col_field: Batch::default(),)*
+                    }
+                }
+            }
+            impl<B: Backend> #db_batch<B> {
+                #(pub fn #col_field(&mut self) -> &mut Batch<B::Col, ColRw<B::Col, #col_event_type>> { &mut self.#col_field })*
+            }
+            // DbRo
+            #[derive(Debug)]
+            pub struct #db_ro<B: Backend> {
+                collections: #db_cols_ro<B::Col>,
+            }
+            impl<B: Backend> #db_ro<B> {
+                pub const NAME: &'static str = #name;
+            }
+            impl<B: Backend> Clone for #db_ro<B> {
+                fn clone(&self) -> Self {
+                    #db_ro {
+                        collections: self.collections.clone(),
+                    }
+                }
+            }
+            // Read operations
+            #impl_db_readable
+            // Write operations
+            #impl_db_writable
+        }
+    };
+
+    //let tokens = TokenStream::from(expanded);
+    //eprintln!("TOKENS: {:#}", tokens);
+
+    TokenStream::from(expanded)
+}