Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Minimal radicle node implementation
Alexis Sellier committed 3 years ago
commit 789c50ee3589eed1d9aa57373a17ac8c3c2f1dfa
parent bad4dcf11e584ecf34de291a5295d7c1e2fca4d9
22 files changed +4031 -6
modified Cargo.lock
@@ -3,5 +3,1141 @@
version = 3

[[package]]
+
name = "android_system_properties"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
+
name = "anyhow"
+
version = "1.0.64"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7"
+

+
[[package]]
+
name = "arrayref"
+
version = "0.3.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+

+
[[package]]
+
name = "arrayvec"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+

+
[[package]]
+
name = "atty"
+
version = "0.2.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+
dependencies = [
+
 "hermit-abi",
+
 "libc",
+
 "winapi",
+
]
+

+
[[package]]
+
name = "autocfg"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+

+
[[package]]
+
name = "base-x"
+
version = "0.2.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
+

+
[[package]]
+
name = "bitflags"
+
version = "1.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+

+
[[package]]
+
name = "blake2b_simd"
+
version = "0.5.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
+
dependencies = [
+
 "arrayref",
+
 "arrayvec",
+
 "constant_time_eq",
+
]
+

+
[[package]]
+
name = "blake2s_simd"
+
version = "0.5.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9e461a7034e85b211a4acb57ee2e6730b32912b06c08cc242243c39fc21ae6a2"
+
dependencies = [
+
 "arrayref",
+
 "arrayvec",
+
 "constant_time_eq",
+
]
+

+
[[package]]
+
name = "block-buffer"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+
dependencies = [
+
 "block-padding",
+
 "generic-array",
+
]
+

+
[[package]]
+
name = "block-buffer"
+
version = "0.10.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+
dependencies = [
+
 "generic-array",
+
]
+

+
[[package]]
+
name = "block-padding"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
+

+
[[package]]
+
name = "bs58"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3"
+

+
[[package]]
+
name = "bumpalo"
+
version = "3.11.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
+

+
[[package]]
+
name = "byteorder"
+
version = "1.4.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+

+
[[package]]
+
name = "cc"
+
version = "1.0.73"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
dependencies = [
+
 "jobserver",
+
]
+

+
[[package]]
+
name = "cfg-if"
+
version = "1.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+

+
[[package]]
+
name = "chrono"
+
version = "0.4.22"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
+
dependencies = [
+
 "iana-time-zone",
+
 "js-sys",
+
 "num-integer",
+
 "num-traits",
+
 "time",
+
 "wasm-bindgen",
+
 "winapi",
+
]
+

+
[[package]]
+
name = "colored"
+
version = "1.9.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59"
+
dependencies = [
+
 "atty",
+
 "lazy_static",
+
 "winapi",
+
]
+

+
[[package]]
+
name = "constant_time_eq"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+

+
[[package]]
+
name = "core-foundation-sys"
+
version = "0.8.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+

+
[[package]]
+
name = "cpufeatures"
+
version = "0.2.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
+
name = "crossbeam-channel"
+
version = "0.5.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
+
dependencies = [
+
 "cfg-if",
+
 "crossbeam-utils",
+
]
+

+
[[package]]
+
name = "crossbeam-utils"
+
version = "0.8.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
+
dependencies = [
+
 "cfg-if",
+
 "once_cell",
+
]
+

+
[[package]]
+
name = "crypto-common"
+
version = "0.1.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+
dependencies = [
+
 "generic-array",
+
 "typenum",
+
]
+

+
[[package]]
+
name = "curve25519-dalek-ng"
+
version = "4.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8"
+
dependencies = [
+
 "byteorder",
+
 "digest 0.9.0",
+
 "rand_core",
+
 "subtle-ng",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "data-encoding"
+
version = "2.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
+

+
[[package]]
+
name = "data-encoding-macro"
+
version = "0.1.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "86927b7cd2fe88fa698b87404b287ab98d1a0063a34071d92e575b72d3029aca"
+
dependencies = [
+
 "data-encoding",
+
 "data-encoding-macro-internal",
+
]
+

+
[[package]]
+
name = "data-encoding-macro-internal"
+
version = "0.1.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a5bbed42daaa95e780b60a50546aa345b8413a1e46f9a40a12907d3598f038db"
+
dependencies = [
+
 "data-encoding",
+
 "syn",
+
]
+

+
[[package]]
+
name = "digest"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+
dependencies = [
+
 "generic-array",
+
]
+

+
[[package]]
+
name = "digest"
+
version = "0.10.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+
dependencies = [
+
 "block-buffer 0.10.3",
+
 "crypto-common",
+
]
+

+
[[package]]
+
name = "ed25519-consensus"
+
version = "2.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e1dd91246c940272326f665724138660a183577ffb77b384a5e10d67d2d5075a"
+
dependencies = [
+
 "curve25519-dalek-ng",
+
 "hex",
+
 "rand_core",
+
 "serde",
+
 "sha2 0.9.9",
+
 "thiserror",
+
 "zeroize",
+
]
+

+
[[package]]
+
name = "fastrand"
+
version = "1.8.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+
dependencies = [
+
 "instant",
+
]
+

+
[[package]]
+
name = "form_urlencoded"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+
dependencies = [
+
 "percent-encoding",
+
]
+

+
[[package]]
+
name = "generic-array"
+
version = "0.14.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+
dependencies = [
+
 "typenum",
+
 "version_check",
+
]
+

+
[[package]]
+
name = "getrandom"
+
version = "0.2.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+
dependencies = [
+
 "cfg-if",
+
 "libc",
+
 "wasi 0.11.0+wasi-snapshot-preview1",
+
]
+

+
[[package]]
+
name = "git-ref-format"
+
version = "0.1.0"
+
source = "git+https://github.com/radicle-dev/radicle-link?tag=cycle/2022-07-12#541a8161cb24c3b7b10d44f958cc5c5ed05cf443"
+
dependencies = [
+
 "git-ref-format-core",
+
 "git-ref-format-macro",
+
]
+

+
[[package]]
+
name = "git-ref-format-core"
+
version = "0.1.0"
+
source = "git+https://github.com/radicle-dev/radicle-link?tag=cycle/2022-07-12#541a8161cb24c3b7b10d44f958cc5c5ed05cf443"
+
dependencies = [
+
 "serde",
+
 "thiserror",
+
]
+

+
[[package]]
+
name = "git-ref-format-macro"
+
version = "0.1.0"
+
source = "git+https://github.com/radicle-dev/radicle-link?tag=cycle/2022-07-12#541a8161cb24c3b7b10d44f958cc5c5ed05cf443"
+
dependencies = [
+
 "git-ref-format-core",
+
 "proc-macro-error",
+
 "quote",
+
 "syn",
+
]
+

+
[[package]]
+
name = "git2"
+
version = "0.13.25"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6"
+
dependencies = [
+
 "bitflags",
+
 "libc",
+
 "libgit2-sys",
+
 "log",
+
 "openssl-probe",
+
 "openssl-sys",
+
 "url",
+
]
+

+
[[package]]
+
name = "hashbrown"
+
version = "0.12.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+

+
[[package]]
+
name = "hermit-abi"
+
version = "0.1.19"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
+
name = "hex"
+
version = "0.4.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+

+
[[package]]
+
name = "iana-time-zone"
+
version = "0.1.48"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "237a0714f28b1ee39ccec0770ccb544eb02c9ef2c82bb096230eefcffa6468b0"
+
dependencies = [
+
 "android_system_properties",
+
 "core-foundation-sys",
+
 "js-sys",
+
 "once_cell",
+
 "wasm-bindgen",
+
 "winapi",
+
]
+

+
[[package]]
+
name = "idna"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+
dependencies = [
+
 "unicode-bidi",
+
 "unicode-normalization",
+
]
+

+
[[package]]
+
name = "indexmap"
+
version = "1.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+
dependencies = [
+
 "autocfg",
+
 "hashbrown",
+
]
+

+
[[package]]
+
name = "instant"
+
version = "0.1.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+
dependencies = [
+
 "cfg-if",
+
]
+

+
[[package]]
+
name = "itoa"
+
version = "1.0.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+

+
[[package]]
+
name = "jobserver"
+
version = "0.1.24"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
+
name = "js-sys"
+
version = "0.3.60"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+
dependencies = [
+
 "wasm-bindgen",
+
]
+

+
[[package]]
+
name = "keccak"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838"
+

+
[[package]]
+
name = "lazy_static"
+
version = "1.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+

+
[[package]]
+
name = "libc"
+
version = "0.2.132"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
+

+
[[package]]
+
name = "libgit2-sys"
+
version = "0.12.26+1.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494"
+
dependencies = [
+
 "cc",
+
 "libc",
+
 "libssh2-sys",
+
 "libz-sys",
+
 "openssl-sys",
+
 "pkg-config",
+
]
+

+
[[package]]
+
name = "libssh2-sys"
+
version = "0.2.23"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca"
+
dependencies = [
+
 "cc",
+
 "libc",
+
 "libz-sys",
+
 "openssl-sys",
+
 "pkg-config",
+
 "vcpkg",
+
]
+

+
[[package]]
+
name = "libz-sys"
+
version = "1.1.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
+
dependencies = [
+
 "cc",
+
 "libc",
+
 "pkg-config",
+
 "vcpkg",
+
]
+

+
[[package]]
+
name = "log"
+
version = "0.4.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+
dependencies = [
+
 "cfg-if",
+
]
+

+
[[package]]
+
name = "multibase"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404"
+
dependencies = [
+
 "base-x",
+
 "data-encoding",
+
 "data-encoding-macro",
+
]
+

+
[[package]]
+
name = "multihash"
+
version = "0.11.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "567122ab6492f49b59def14ecc36e13e64dca4188196dd0cd41f9f3f979f3df6"
+
dependencies = [
+
 "blake2b_simd",
+
 "blake2s_simd",
+
 "digest 0.9.0",
+
 "sha-1",
+
 "sha2 0.9.9",
+
 "sha3",
+
 "unsigned-varint",
+
]
+

+
[[package]]
+
name = "nakamoto-net"
+
version = "0.3.0"
+
source = "git+https://github.com/cloudhead/nakamoto?branch=master#90cc3eac67aa5cfd5f42cf7cb1e2b155af3214fb"
+
dependencies = [
+
 "crossbeam-channel",
+
 "fastrand",
+
 "log",
+
 "thiserror",
+
]
+

+
[[package]]
+
name = "nakamoto-net-poll"
+
version = "0.3.0"
+
source = "git+https://github.com/cloudhead/nakamoto?branch=master#90cc3eac67aa5cfd5f42cf7cb1e2b155af3214fb"
+
dependencies = [
+
 "crossbeam-channel",
+
 "libc",
+
 "log",
+
 "nakamoto-net",
+
 "popol",
+
 "socket2",
+
]
+

+
[[package]]
+
name = "nonempty"
+
version = "0.8.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09f1f8e5676e1a1f2ee8b21f38238e1243c827531c9435624c7bfb305102cee4"
+
dependencies = [
+
 "serde",
+
]
+

+
[[package]]
+
name = "num-integer"
+
version = "0.1.45"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+
dependencies = [
+
 "autocfg",
+
 "num-traits",
+
]
+

+
[[package]]
+
name = "num-traits"
+
version = "0.2.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+
dependencies = [
+
 "autocfg",
+
]
+

+
[[package]]
+
name = "once_cell"
+
version = "1.14.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
+

+
[[package]]
+
name = "opaque-debug"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+

+
[[package]]
+
name = "openssl-probe"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+

+
[[package]]
+
name = "openssl-sys"
+
version = "0.9.75"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f"
+
dependencies = [
+
 "autocfg",
+
 "cc",
+
 "libc",
+
 "pkg-config",
+
 "vcpkg",
+
]
+

+
[[package]]
+
name = "percent-encoding"
+
version = "2.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+

+
[[package]]
+
name = "pkg-config"
+
version = "0.3.25"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+

+
[[package]]
+
name = "popol"
+
version = "0.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2dac9010e6fc2d991bbf3c362433214c28f7f1a82151b6b1de8dd69d27211495"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
+
name = "proc-macro-error"
+
version = "1.0.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+
dependencies = [
+
 "proc-macro-error-attr",
+
 "proc-macro2",
+
 "quote",
+
 "syn",
+
 "version_check",
+
]
+

+
[[package]]
+
name = "proc-macro-error-attr"
+
version = "1.0.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "version_check",
+
]
+

+
[[package]]
+
name = "proc-macro2"
+
version = "1.0.43"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+
dependencies = [
+
 "unicode-ident",
+
]
+

+
[[package]]
+
name = "quickcheck"
+
version = "1.0.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
+
dependencies = [
+
 "rand",
+
]
+

+
[[package]]
+
name = "quickcheck_macros"
+
version = "1.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn",
+
]
+

+
[[package]]
+
name = "quote"
+
version = "1.0.21"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+
dependencies = [
+
 "proc-macro2",
+
]
+

+
[[package]]
+
name = "radicle-git-ext"
+
version = "0.1.0"
+
source = "git+https://github.com/radicle-dev/radicle-link?tag=cycle/2022-07-12#541a8161cb24c3b7b10d44f958cc5c5ed05cf443"
+
dependencies = [
+
 "git-ref-format",
+
 "git2",
+
 "multihash",
+
 "percent-encoding",
+
 "radicle-std-ext",
+
 "serde",
+
 "thiserror",
+
]
+

+
[[package]]
name = "radicle-node"
+
version = "0.2.0"
+
dependencies = [
+
 "anyhow",
+
 "bs58",
+
 "chrono",
+
 "colored",
+
 "ed25519-consensus",
+
 "fastrand",
+
 "git-ref-format",
+
 "git2",
+
 "log",
+
 "multibase",
+
 "nakamoto-net",
+
 "nakamoto-net-poll",
+
 "nonempty",
+
 "once_cell",
+
 "quickcheck",
+
 "quickcheck_macros",
+
 "radicle-git-ext",
+
 "serde",
+
 "serde_json",
+
 "sha2 0.10.5",
+
 "siphasher",
+
 "tempfile",
+
 "thiserror",
+
 "toml",
+
]
+

+
[[package]]
+
name = "radicle-std-ext"
+
version = "0.1.0"
+
source = "git+https://github.com/radicle-dev/radicle-link?tag=cycle/2022-07-12#541a8161cb24c3b7b10d44f958cc5c5ed05cf443"
+

+
[[package]]
+
name = "rand"
+
version = "0.8.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+
dependencies = [
+
 "rand_core",
+
]
+

+
[[package]]
+
name = "rand_core"
+
version = "0.6.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+
dependencies = [
+
 "getrandom",
+
]
+

+
[[package]]
+
name = "redox_syscall"
+
version = "0.2.16"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+
dependencies = [
+
 "bitflags",
+
]
+

+
[[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 = "ryu"
+
version = "1.0.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+

+
[[package]]
+
name = "serde"
+
version = "1.0.144"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
+
dependencies = [
+
 "serde_derive",
+
]
+

+
[[package]]
+
name = "serde_derive"
+
version = "1.0.144"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn",
+
]
+

+
[[package]]
+
name = "serde_json"
+
version = "1.0.85"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
+
dependencies = [
+
 "indexmap",
+
 "itoa",
+
 "ryu",
+
 "serde",
+
]
+

+
[[package]]
+
name = "sha-1"
+
version = "0.9.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
+
dependencies = [
+
 "block-buffer 0.9.0",
+
 "cfg-if",
+
 "cpufeatures",
+
 "digest 0.9.0",
+
 "opaque-debug",
+
]
+

+
[[package]]
+
name = "sha2"
+
version = "0.9.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+
dependencies = [
+
 "block-buffer 0.9.0",
+
 "cfg-if",
+
 "cpufeatures",
+
 "digest 0.9.0",
+
 "opaque-debug",
+
]
+

+
[[package]]
+
name = "sha2"
+
version = "0.10.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cf9db03534dff993187064c4e0c05a5708d2a9728ace9a8959b77bedf415dac5"
+
dependencies = [
+
 "cfg-if",
+
 "cpufeatures",
+
 "digest 0.10.3",
+
]
+

+
[[package]]
+
name = "sha3"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809"
+
dependencies = [
+
 "block-buffer 0.9.0",
+
 "digest 0.9.0",
+
 "keccak",
+
 "opaque-debug",
+
]
+

+
[[package]]
+
name = "siphasher"
+
version = "0.3.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+

+
[[package]]
+
name = "socket2"
+
version = "0.4.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+
dependencies = [
+
 "libc",
+
 "winapi",
+
]
+

+
[[package]]
+
name = "subtle-ng"
+
version = "2.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142"
+

+
[[package]]
+
name = "syn"
+
version = "1.0.99"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "unicode-ident",
+
]
+

+
[[package]]
+
name = "tempfile"
+
version = "3.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+
dependencies = [
+
 "cfg-if",
+
 "fastrand",
+
 "libc",
+
 "redox_syscall",
+
 "remove_dir_all",
+
 "winapi",
+
]
+

+
[[package]]
+
name = "thiserror"
+
version = "1.0.34"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252"
+
dependencies = [
+
 "thiserror-impl",
+
]
+

+
[[package]]
+
name = "thiserror-impl"
+
version = "1.0.34"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn",
+
]
+

+
[[package]]
+
name = "time"
+
version = "0.1.44"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
+
dependencies = [
+
 "libc",
+
 "wasi 0.10.0+wasi-snapshot-preview1",
+
 "winapi",
+
]
+

+
[[package]]
+
name = "tinyvec"
+
version = "1.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+
dependencies = [
+
 "tinyvec_macros",
+
]
+

+
[[package]]
+
name = "tinyvec_macros"
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+

+
[[package]]
+
name = "toml"
+
version = "0.5.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+
dependencies = [
+
 "serde",
+
]
+

+
[[package]]
+
name = "typenum"
+
version = "1.15.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+

+
[[package]]
+
name = "unicode-bidi"
+
version = "0.3.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+

+
[[package]]
+
name = "unicode-ident"
+
version = "1.0.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
+

+
[[package]]
+
name = "unicode-normalization"
+
version = "0.1.21"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
+
dependencies = [
+
 "tinyvec",
+
]
+

+
[[package]]
+
name = "unsigned-varint"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f7fdeedbf205afadfe39ae559b75c3240f24e257d0ca27e85f85cb82aa19ac35"
+

+
[[package]]
+
name = "url"
+
version = "2.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+
dependencies = [
+
 "form_urlencoded",
+
 "idna",
+
 "percent-encoding",
+
]
+

+
[[package]]
+
name = "vcpkg"
+
version = "0.2.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+

+
[[package]]
+
name = "version_check"
+
version = "0.9.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+

+
[[package]]
+
name = "wasi"
+
version = "0.10.0+wasi-snapshot-preview1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+

+
[[package]]
+
name = "wasi"
+
version = "0.11.0+wasi-snapshot-preview1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+

+
[[package]]
+
name = "wasm-bindgen"
+
version = "0.2.83"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+
dependencies = [
+
 "cfg-if",
+
 "wasm-bindgen-macro",
+
]
+

+
[[package]]
+
name = "wasm-bindgen-backend"
+
version = "0.2.83"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+
dependencies = [
+
 "bumpalo",
+
 "log",
+
 "once_cell",
+
 "proc-macro2",
+
 "quote",
+
 "syn",
+
 "wasm-bindgen-shared",
+
]
+

+
[[package]]
+
name = "wasm-bindgen-macro"
+
version = "0.2.83"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+
dependencies = [
+
 "quote",
+
 "wasm-bindgen-macro-support",
+
]
+

+
[[package]]
+
name = "wasm-bindgen-macro-support"
+
version = "0.2.83"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn",
+
 "wasm-bindgen-backend",
+
 "wasm-bindgen-shared",
+
]
+

+
[[package]]
+
name = "wasm-bindgen-shared"
+
version = "0.2.83"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+

+
[[package]]
+
name = "winapi"
+
version = "0.3.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+
dependencies = [
+
 "winapi-i686-pc-windows-gnu",
+
 "winapi-x86_64-pc-windows-gnu",
+
]
+

+
[[package]]
+
name = "winapi-i686-pc-windows-gnu"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+

+
[[package]]
+
name = "winapi-x86_64-pc-windows-gnu"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+

+
[[package]]
+
name = "zeroize"
+
version = "1.5.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"
modified Cargo.toml
@@ -1,7 +1,20 @@
-
[package]
-
name = "radicle-node"
-
version = "0.1.0"
-
authors = ["Alexis Sellier <alexis@radicle.xyz>"]
-
edition = "2021"
+
[workspace]
+
members = ["node"]

-
[dependencies]
+
[patch.crates-io.radicle-git-ext]
+
git = "https://github.com/radicle-dev/radicle-link"
+
tag = "cycle/2022-07-12"
+

+
[patch.crates-io.git-ref-format]
+
git = "https://github.com/radicle-dev/radicle-link"
+
tag = "cycle/2022-07-12"
+

+
[patch.crates-io.nakamoto-net]
+
git = "https://github.com/cloudhead/nakamoto"
+
branch = "master"
+
version = "0.3.0"
+

+
[patch.crates-io.nakamoto-net-poll]
+
git = "https://github.com/cloudhead/nakamoto"
+
branch = "master"
+
version = "0.3.0"
added node/Cargo.toml
@@ -0,0 +1,34 @@
+
[package]
+
name = "radicle-node"
+
license = "MIT OR Apache-2.0"
+
version = "0.2.0"
+
authors = ["Alexis Sellier <alexis@radicle.xyz>"]
+
edition = "2021"
+

+
[dependencies]
+
anyhow = { version = "1" }
+
bs58 = { version = "0.4.0" }
+
ed25519-consensus = { version = "2.0.1" }
+
chrono = { version = "0.4.0" }
+
colored = { version = "1.9.0" }
+
fastrand = { version = "1.8.0" }
+
git-ref-format = { version = "0", features = ["serde", "macro"] }
+
git2 = { version = "0.13" }
+
multibase = { version = "0.9.1" }
+
log = { version = "0.4.17", features = ["std"] }
+
once_cell = { version = "1.13" }
+
sha2 = { version = "0.10.2" }
+
serde = { version = "1", features = ["derive"] }
+
serde_json = { version = "1", features = ["preserve_order"] }
+
siphasher = { version = "0.3.10" }
+
radicle-git-ext = { version = "0", features = ["serde"] }
+
nonempty = { version = "0.8.0", features = ["serialize"] }
+
nakamoto-net = { version = "0.3.0" }
+
nakamoto-net-poll = { version = "0.3.0" }
+
thiserror = { version = "1" }
+
toml = { version = "0.5.9" }
+

+
[dev-dependencies]
+
quickcheck = { version = "1", default-features = false }
+
quickcheck_macros = { version = "1", default-features = false }
+
tempfile = { version = "3.3.0" }
added node/src/address_book.rs
@@ -0,0 +1,468 @@
+
use std::io::Seek;
+
use std::ops::{Deref, DerefMut};
+
use std::path::Path;
+
use std::{fs, io, net};
+

+
use crate::collections::HashMap;
+
use crate::LocalTime;
+
use nonempty::NonEmpty;
+
use serde::{Deserialize, Serialize};
+

+
/// A map with the ability to randomly select values.
+
#[derive(Debug)]
+
pub struct AddressBook<K, V> {
+
    inner: HashMap<K, V>,
+
    rng: fastrand::Rng,
+
}
+

+
impl<K, V> AddressBook<K, V> {
+
    /// Create a new address book.
+
    pub fn new(rng: fastrand::Rng) -> Self {
+
        Self {
+
            inner: HashMap::with_hasher(rng.clone().into()),
+
            rng,
+
        }
+
    }
+

+
    /// Pick a random value in the book.
+
    pub fn sample(&self) -> Option<(&K, &V)> {
+
        self.sample_with(|_, _| true)
+
    }
+

+
    /// Pick a random value in the book matching a predicate.
+
    pub fn sample_with(&self, mut predicate: impl FnMut(&K, &V) -> bool) -> Option<(&K, &V)> {
+
        if let Some(pairs) = NonEmpty::from_vec(
+
            self.inner
+
                .iter()
+
                .filter(|(k, v)| predicate(*k, *v))
+
                .collect(),
+
        ) {
+
            let ix = self.rng.usize(..pairs.len());
+
            let pair = pairs[ix]; // Can't fail.
+

+
            Some(pair)
+
        } else {
+
            None
+
        }
+
    }
+

+
    /// Cycle through the keys at random. The random cycle repeats ad-infintum.
+
    pub fn cycle(&self) -> impl Iterator<Item = &K> {
+
        self.shuffled().map(|(k, _)| k).cycle()
+
    }
+

+
    /// Return a shuffled iterator over the keys.
+
    pub fn shuffled(&self) -> std::vec::IntoIter<(&K, &V)> {
+
        let mut keys = self.inner.iter().collect::<Vec<_>>();
+
        self.rng.shuffle(&mut keys);
+

+
        keys.into_iter()
+
    }
+
}
+

+
impl<K, V> Deref for AddressBook<K, V> {
+
    type Target = HashMap<K, V>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.inner
+
    }
+
}
+

+
impl<K, V> DerefMut for AddressBook<K, V> {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.inner
+
    }
+
}
+

+
/// A known address.
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct KnownAddress {
+
    /// Network address.
+
    pub addr: net::SocketAddr,
+
    /// Address of the peer who sent us this address.
+
    pub source: Source,
+
    /// Last time this address was used to successfully connect to a peer.
+
    #[serde(with = "local_time")]
+
    pub last_success: Option<LocalTime>,
+
    /// Last time this address was sampled.
+
    #[serde(with = "local_time")]
+
    pub last_sampled: Option<LocalTime>,
+
    /// Last time this address was tried.
+
    #[serde(with = "local_time")]
+
    pub last_attempt: Option<LocalTime>,
+
    /// Last time this peer was seen alive.
+
    #[serde(with = "local_time")]
+
    pub last_active: Option<LocalTime>,
+
}
+

+
impl KnownAddress {
+
    /// Create a new known address.
+
    pub fn new(addr: net::SocketAddr, source: Source, last_active: Option<LocalTime>) -> Self {
+
        Self {
+
            addr,
+
            source,
+
            last_success: None,
+
            last_attempt: None,
+
            last_sampled: None,
+
            last_active,
+
        }
+
    }
+
}
+

+
/// Address source. Specifies where an address originated from.
+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
pub enum Source {
+
    /// An address that was shared by another peer.
+
    Peer(net::SocketAddr),
+
    /// An address that came from a DNS seed.
+
    Dns,
+
    /// An address that came from some source external to the system, eg.
+
    /// specified by the user or added directly to the address manager.
+
    Imported,
+
}
+

+
impl std::fmt::Display for Source {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        match self {
+
            Self::Peer(addr) => write!(f, "{}", addr),
+
            Self::Dns => write!(f, "DNS"),
+
            Self::Imported => write!(f, "Imported"),
+
        }
+
    }
+
}
+

+
/// A file-backed address cache.
+
#[derive(Debug)]
+
pub struct Cache {
+
    addrs: std::collections::HashMap<net::IpAddr, KnownAddress>,
+
    file: fs::File,
+
}
+

+
impl Cache {
+
    /// Open an existing cache.
+
    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
+
        fs::OpenOptions::new()
+
            .read(true)
+
            .write(true)
+
            .open(path)
+
            .and_then(Self::from)
+
    }
+

+
    /// Create a new cache.
+
    pub fn create<P: AsRef<Path>>(path: P) -> io::Result<Self> {
+
        use std::collections::HashMap;
+

+
        let file = fs::OpenOptions::new()
+
            .create_new(true)
+
            .write(true)
+
            .open(path)?;
+

+
        Ok(Self {
+
            file,
+
            addrs: HashMap::new(),
+
        })
+
    }
+

+
    /// Create a new cache from a file.
+
    pub fn from(mut file: fs::File) -> io::Result<Self> {
+
        use std::collections::HashMap;
+

+
        let bytes = file.seek(io::SeekFrom::End(0))?;
+
        let addrs = if bytes == 0 {
+
            HashMap::new()
+
        } else {
+
            file.rewind()?;
+
            serde_json::from_reader(&file)?
+
        };
+

+
        Ok(Self { file, addrs })
+
    }
+
}
+

+
impl Store for Cache {
+
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress> {
+
        self.addrs.get_mut(ip)
+
    }
+

+
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress> {
+
        self.addrs.get(ip)
+
    }
+

+
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress> {
+
        self.addrs.remove(ip)
+
    }
+

+
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool {
+
        <std::collections::HashMap<_, _> as Store>::insert(&mut self.addrs, ip, ka)
+
    }
+

+
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a> {
+
        Box::new(self.addrs.iter())
+
    }
+

+
    fn clear(&mut self) {
+
        self.addrs.clear()
+
    }
+

+
    fn len(&self) -> usize {
+
        self.addrs.len()
+
    }
+

+
    fn flush<'a>(&mut self) -> io::Result<()> {
+
        use io::Write;
+

+
        let peers = serde_json::to_value(&self.addrs)?;
+
        let s = serde_json::to_string(&peers)?;
+

+
        self.file.set_len(0)?;
+
        self.file.seek(io::SeekFrom::Start(0))?;
+
        self.file.write_all(s.as_bytes())?;
+
        self.file.write_all(&[b'\n'])?;
+
        self.file.sync_data()?;
+

+
        Ok(())
+
    }
+
}
+

+
/// Address store.
+
///
+
/// Used to store peer addresses and metadata.
+
pub trait Store {
+
    /// Get a known peer address.
+
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress>;
+

+
    /// Get a known peer address mutably.
+
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress>;
+

+
    /// Insert a *new* address into the store. Returns `true` if the address was inserted,
+
    /// or `false` if it was already known.
+
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool;
+

+
    /// Remove an address from the store.
+
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress>;
+

+
    /// Return an iterator over the known addresses.
+
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a>;
+

+
    /// Returns the number of addresses.
+
    fn len(&self) -> usize;
+

+
    /// Returns true if there are no addresses.
+
    fn is_empty(&self) -> bool {
+
        self.len() == 0
+
    }
+

+
    /// Seed the peer store with addresses.
+
    /// Fails if *none* of the seeds could be resolved to addresses.
+
    fn seed<S: net::ToSocketAddrs>(
+
        &mut self,
+
        seeds: impl Iterator<Item = S>,
+
        source: Source,
+
    ) -> io::Result<()> {
+
        let mut error = None;
+
        let mut success = false;
+

+
        for seed in seeds {
+
            match seed.to_socket_addrs() {
+
                Ok(addrs) => {
+
                    success = true;
+
                    for addr in addrs {
+
                        self.insert(addr.ip(), KnownAddress::new(addr, source, None));
+
                    }
+
                }
+
                Err(err) => error = Some(err),
+
            }
+
        }
+

+
        if success {
+
            return Ok(());
+
        }
+
        if let Some(err) = error {
+
            return Err(io::Error::new(
+
                io::ErrorKind::Other,
+
                format!("seeds failed to resolve: {}", err),
+
            ));
+
        }
+
        Ok(())
+
    }
+

+
    /// Clears the store of all addresses.
+
    fn clear(&mut self);
+

+
    /// Flush data to permanent storage.
+
    fn flush(&mut self) -> io::Result<()>;
+
}
+

+
/// Implementation of [`Store`] for [`std::collections::HashMap`].
+
impl Store for std::collections::HashMap<net::IpAddr, KnownAddress> {
+
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress> {
+
        self.get_mut(ip)
+
    }
+

+
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress> {
+
        self.get(ip)
+
    }
+

+
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress> {
+
        self.remove(ip)
+
    }
+

+
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool {
+
        use std::collections::hash_map::Entry;
+

+
        match self.entry(ip) {
+
            Entry::Vacant(v) => {
+
                v.insert(ka);
+
            }
+
            Entry::Occupied(_) => return false,
+
        }
+
        true
+
    }
+

+
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a> {
+
        Box::new(self.iter())
+
    }
+

+
    fn clear(&mut self) {
+
        self.clear()
+
    }
+

+
    fn len(&self) -> usize {
+
        self.len()
+
    }
+

+
    fn flush(&mut self) -> std::io::Result<()> {
+
        Ok(())
+
    }
+
}
+

+
/// Implementation of [`Store`] for [`crate::collections::HashMap`].
+
impl Store for crate::collections::HashMap<net::IpAddr, KnownAddress> {
+
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress> {
+
        self.get_mut(ip)
+
    }
+

+
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress> {
+
        self.get(ip)
+
    }
+

+
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress> {
+
        self.remove(ip)
+
    }
+

+
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool {
+
        use std::collections::hash_map::Entry;
+

+
        match self.entry(ip) {
+
            Entry::Vacant(v) => {
+
                v.insert(ka);
+
            }
+
            Entry::Occupied(_) => return false,
+
        }
+
        true
+
    }
+

+
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a> {
+
        Box::new(self.iter())
+
    }
+

+
    fn clear(&mut self) {
+
        self.clear()
+
    }
+

+
    fn len(&self) -> usize {
+
        self.len()
+
    }
+

+
    fn flush(&mut self) -> std::io::Result<()> {
+
        Ok(())
+
    }
+
}
+

+
mod local_time {
+
    use super::LocalTime;
+
    use serde::{Deserialize, Deserializer, Serializer};
+

+
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<LocalTime>, D::Error>
+
    where
+
        D: Deserializer<'de>,
+
    {
+
        let value: Option<u64> = Deserialize::deserialize(deserializer)?;
+

+
        if let Some(value) = value {
+
            Ok(Some(LocalTime::from_secs(value)))
+
        } else {
+
            Ok(None)
+
        }
+
    }
+

+
    pub fn serialize<S>(value: &Option<LocalTime>, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        if let Some(local_time) = value {
+
            serializer.serialize_u64(local_time.as_secs())
+
        } else {
+
            serializer.serialize_none()
+
        }
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use super::*;
+

+
    #[test]
+
    fn test_empty() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let path = tmp.path().join("cache");
+

+
        Cache::create(&path).unwrap();
+
        let cache = Cache::open(&path).unwrap();
+

+
        assert!(cache.is_empty());
+
    }
+

+
    #[test]
+
    fn test_save_and_load() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let path = tmp.path().join("cache");
+
        let mut expected = Vec::new();
+

+
        {
+
            let mut cache = Cache::create(&path).unwrap();
+

+
            for i in 32..48 {
+
                let ip = net::IpAddr::from([127, 0, 0, i]);
+
                let addr = net::SocketAddr::from((ip, 8333));
+
                let ka = KnownAddress {
+
                    addr,
+
                    source: Source::Dns,
+
                    last_success: Some(LocalTime::from_secs(i as u64)),
+
                    last_sampled: Some(LocalTime::from_secs((i + 1) as u64)),
+
                    last_attempt: None,
+
                    last_active: None,
+
                };
+
                cache.insert(ip, ka);
+
            }
+
            cache.flush().unwrap();
+

+
            for (ip, ka) in cache.iter() {
+
                expected.push((*ip, ka.clone()));
+
            }
+
        }
+

+
        {
+
            let cache = Cache::open(&path).unwrap();
+
            let mut actual = cache
+
                .iter()
+
                .map(|(i, ka)| (*i, ka.clone()))
+
                .collect::<Vec<_>>();
+

+
            actual.sort_by_key(|(i, _)| *i);
+
            expected.sort_by_key(|(i, _)| *i);
+

+
            assert_eq!(actual, expected);
+
        }
+
    }
+
}
added node/src/address_manager.rs
@@ -0,0 +1,12 @@
+
use crate::address_book::Store;
+

+
#[derive(Debug)]
+
pub struct AddressManager<S> {
+
    store: S,
+
}
+

+
impl<S: Store> AddressManager<S> {
+
    pub fn new(store: S) -> Self {
+
        Self { store }
+
    }
+
}
added node/src/clock.rs
@@ -0,0 +1,37 @@
+
use std::cell::RefCell;
+
use std::rc::Rc;
+

+
use crate::{LocalDuration, LocalTime};
+

+
/// Clock with interior mutability.
+
#[derive(Debug, Clone)]
+
pub struct RefClock(Rc<RefCell<LocalTime>>);
+

+
impl std::ops::Deref for RefClock {
+
    type Target = Rc<RefCell<LocalTime>>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl RefClock {
+
    /// Elapse time.
+
    pub fn elapse(&self, duration: LocalDuration) {
+
        self.borrow_mut().elapse(duration)
+
    }
+

+
    pub fn local_time(&self) -> LocalTime {
+
        *self.borrow()
+
    }
+

+
    pub fn set(&mut self, time: LocalTime) {
+
        *self.borrow_mut() = time;
+
    }
+
}
+

+
impl From<LocalTime> for RefClock {
+
    fn from(other: LocalTime) -> Self {
+
        Self(Rc::new(RefCell::new(other)))
+
    }
+
}
added node/src/collections.rs
@@ -0,0 +1,38 @@
+
//! Useful collections for peer-to-peer networking.
+
use siphasher::sip::SipHasher13;
+

+
/// A `HashMap` which uses [`fastrand::Rng`] for its random state.
+
pub type HashMap<K, V> = std::collections::HashMap<K, V, RandomState>;
+

+
/// A `HashSet` which uses [`fastrand::Rng`] for its random state.
+
pub type HashSet<K> = std::collections::HashSet<K, RandomState>;
+

+
/// Random hasher state.
+
#[derive(Default, Clone)]
+
pub struct RandomState {
+
    key1: u64,
+
    key2: u64,
+
}
+

+
impl RandomState {
+
    fn new(rng: fastrand::Rng) -> Self {
+
        Self {
+
            key1: rng.u64(..),
+
            key2: rng.u64(..),
+
        }
+
    }
+
}
+

+
impl std::hash::BuildHasher for RandomState {
+
    type Hasher = SipHasher13;
+

+
    fn build_hasher(&self) -> Self::Hasher {
+
        SipHasher13::new_with_keys(self.key1, self.key2)
+
    }
+
}
+

+
impl From<fastrand::Rng> for RandomState {
+
    fn from(rng: fastrand::Rng) -> Self {
+
        Self::new(rng)
+
    }
+
}
added node/src/decoder.rs
@@ -0,0 +1,104 @@
+
use std::marker::PhantomData;
+

+
use crate::protocol::Envelope;
+
use serde::Deserialize;
+

+
/// Message stream decoder.
+
///
+
/// Used to for example turn a byte stream into network messages.
+
#[derive(Debug)]
+
pub struct Decoder<D = Envelope> {
+
    unparsed: Vec<u8>,
+
    item: PhantomData<D>,
+
}
+

+
impl<D> From<Vec<u8>> for Decoder<D> {
+
    fn from(unparsed: Vec<u8>) -> Self {
+
        Self {
+
            unparsed,
+
            item: PhantomData,
+
        }
+
    }
+
}
+

+
impl<'de, D: Deserialize<'de>> Decoder<D> {
+
    /// Create a new stream decoder.
+
    pub fn new(capacity: usize) -> Self {
+
        Self {
+
            unparsed: Vec::with_capacity(capacity),
+
            item: PhantomData,
+
        }
+
    }
+

+
    /// Input bytes into the decoder.
+
    pub fn input(&mut self, bytes: &[u8]) {
+
        self.unparsed.extend_from_slice(bytes);
+
    }
+

+
    /// Decode and return the next message. Returns [`None`] if nothing was decoded.
+
    pub fn decode_next(&mut self) -> Result<Option<D>, serde_json::Error> {
+
        let mut de = serde_json::Deserializer::from_reader(self.unparsed.as_slice()).into_iter();
+

+
        match de.next() {
+
            Some(Ok(msg)) => {
+
                self.unparsed.drain(..de.byte_offset());
+
                Ok(Some(msg))
+
            }
+
            Some(Err(err)) if err.is_eof() => Ok(None),
+

+
            result => result.transpose(),
+
        }
+
    }
+
}
+

+
impl<'de, D: Deserialize<'de>> Iterator for Decoder<D> {
+
    type Item = Result<D, serde_json::Error>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.decode_next().transpose()
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use super::*;
+
    use quickcheck_macros::quickcheck;
+

+
    const MSG_HELLO: &[u8] = b"{\"cmd\":\"hello\"}";
+
    const MSG_BYE: &[u8] = b"{\"cmd\":\"goodbye\"}";
+

+
    #[quickcheck]
+
    fn prop_decode_next(chunk_size: usize) {
+
        let mut bytes = vec![];
+
        let mut msgs = vec![];
+
        let mut decoder = Decoder::<serde_json::Value>::new(64);
+

+
        let chunk_size = 1 + chunk_size % MSG_HELLO.len() + MSG_BYE.len();
+

+
        bytes.extend_from_slice(MSG_HELLO);
+
        bytes.extend_from_slice(MSG_BYE);
+

+
        for chunk in bytes.as_slice().chunks(chunk_size) {
+
            decoder.input(chunk);
+

+
            while let Some(msg) = decoder.decode_next().unwrap() {
+
                msgs.push(msg);
+
            }
+
        }
+

+
        assert_eq!(decoder.unparsed.len(), 0);
+
        assert_eq!(msgs.len(), 2);
+
        assert_eq!(
+
            msgs[0],
+
            serde_json::json!({
+
                "cmd": "hello",
+
            })
+
        );
+
        assert_eq!(
+
            msgs[1],
+
            serde_json::json!({
+
                "cmd": "goodbye",
+
            })
+
        );
+
    }
+
}
added node/src/hash.rs
@@ -0,0 +1,79 @@
+
use std::fmt;
+

+
use serde::{Deserialize, Serialize};
+
use sha2::{
+
    digest::{generic_array::GenericArray, OutputSizeUser},
+
    Digest as _, Sha256,
+
};
+
use thiserror::Error;
+

+
#[derive(Debug, Clone, PartialEq, Eq, Error)]
+
pub enum ParseError {
+
    #[error("invalid string length")]
+
    InvalidLength,
+
    #[error(transparent)]
+
    ParseInt(#[from] std::num::ParseIntError),
+
}
+

+
/// A SHA-256 hash.
+
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+
pub struct Digest([u8; 32]);
+

+
impl Digest {
+
    pub fn new(bytes: impl AsRef<[u8]>) -> Self {
+
        Self::from(Sha256::digest(bytes))
+
    }
+

+
    pub fn encode(&self) -> String {
+
        self.to_string()
+
    }
+

+
    pub fn decode(s: &str) -> Result<Self, ParseError> {
+
        if s.len() != 64 {
+
            Err(ParseError::InvalidLength)
+
        } else {
+
            let mut bytes: [u8; 32] = Default::default();
+
            for (i, byte) in (0..s.len())
+
                .step_by(2)
+
                .map(|i| u8::from_str_radix(&s[i..i + 2], 16))
+
                .enumerate()
+
            {
+
                bytes[i] = byte?;
+
            }
+
            Ok(Self(bytes))
+
        }
+
    }
+
}
+

+
impl AsRef<[u8]> for Digest {
+
    fn as_ref(&self) -> &[u8] {
+
        &self.0
+
    }
+
}
+

+
impl fmt::Debug for Digest {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Hash({})", self.encode())
+
    }
+
}
+

+
impl fmt::Display for Digest {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        for byte in &self.0 {
+
            write!(f, "{:02x}", byte)?;
+
        }
+
        Ok(())
+
    }
+
}
+

+
impl From<[u8; 32]> for Digest {
+
    fn from(bytes: [u8; 32]) -> Self {
+
        Self(bytes)
+
    }
+
}
+

+
impl From<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>> for Digest {
+
    fn from(array: GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>) -> Self {
+
        Self(array.into())
+
    }
+
}
added node/src/identity.rs
@@ -0,0 +1,174 @@
+
use std::{fmt, io, ops::Deref, str::FromStr};
+

+
use ed25519_consensus::{VerificationKey, VerificationKeyBytes};
+
use nonempty::NonEmpty;
+
use radicle_git_ext::Oid;
+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
use crate::hash;
+

+
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+
pub struct ProjId(hash::Digest);
+

+
impl fmt::Display for ProjId {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "{}", self.encode())
+
    }
+
}
+

+
impl fmt::Debug for ProjId {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "ProjId({})", self.encode())
+
    }
+
}
+

+
impl ProjId {
+
    pub fn encode(&self) -> String {
+
        multibase::encode(multibase::Base::Base58Btc, &self.0.as_ref())
+
    }
+

+
    pub(crate) fn from_ref(s: &str) -> Result<ProjId, IdError> {
+
        if let Some(s) = s.split('/').nth(2) {
+
            let mut array: [u8; 32] = [0; 32];
+
            let bytes = bs58::decode(s).into(&mut array)?;
+

+
            // TODO: Multi-hash?
+

+
            assert_eq!(bytes, array.len());
+

+
            return Ok(Self(hash::Digest::from(array)));
+
        }
+
        Err(IdError::InvalidRef(s.to_owned()))
+
    }
+
}
+

+
impl From<hash::Digest> for ProjId {
+
    fn from(digest: hash::Digest) -> Self {
+
        Self(digest)
+
    }
+
}
+

+
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug, Clone)]
+
pub struct Did(UserId);
+

+
impl Did {
+
    fn encode(&self) -> String {
+
        format!("did:key:{}", self.0.encode())
+
    }
+
}
+

+
impl fmt::Display for Did {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "{}", self.encode())
+
    }
+
}
+

+
#[derive(Serialize, Deserialize, Eq, Debug, Clone)]
+
pub struct UserId(pub VerificationKey);
+

+
impl std::hash::Hash for UserId {
+
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+
        self.0.as_bytes().hash(state)
+
    }
+
}
+

+
impl PartialEq for UserId {
+
    fn eq(&self, other: &Self) -> bool {
+
        self.0 == other.0
+
    }
+
}
+

+
impl UserId {
+
    fn encode(&self) -> String {
+
        multibase::encode(multibase::Base::Base58Btc, &self.0)
+
    }
+
}
+

+
impl FromStr for UserId {
+
    type Err = IdError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        let mut array: [u8; 32] = [0; 32];
+
        let bytes = bs58::decode(s).into(&mut array)?;
+
        let key = VerificationKey::try_from(VerificationKeyBytes::from(array))?;
+

+
        assert_eq!(bytes, array.len());
+

+
        Ok(Self(key))
+
    }
+
}
+

+
impl Deref for UserId {
+
    type Target = VerificationKey;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
#[derive(Error, Debug)]
+
pub enum IdError {
+
    #[error("invalid ref '{0}'")]
+
    InvalidRef(String),
+
    #[error("invalid base58 string: {0}")]
+
    Base58(#[from] bs58::decode::Error),
+
    #[error("invalid key: {0}")]
+
    InvalidKey(#[from] ed25519_consensus::Error),
+
}
+

+
impl UserId {}
+

+
#[derive(Error, Debug)]
+
pub enum DocError {
+
    #[error("toml: {0}")]
+
    Toml(#[from] toml::ser::Error),
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
}
+

+
#[derive(Serialize, Deserialize)]
+
pub struct Delegate {
+
    pub name: String,
+
    pub id: Did,
+
}
+

+
#[derive(Serialize, Deserialize)]
+
pub struct Doc {
+
    pub name: String,
+
    pub description: String,
+
    pub version: u32,
+
    pub parent: Oid,
+
    pub delegate: NonEmpty<Delegate>,
+
}
+

+
impl Doc {
+
    pub fn write<W: io::Write>(&self, mut writer: W) -> Result<ProjId, DocError> {
+
        let buf = toml::to_string_pretty(self)?;
+
        let digest = hash::Digest::new(buf.as_bytes());
+
        let id = ProjId::from(digest);
+

+
        writer.write_all(buf.as_bytes())?;
+

+
        Ok(id)
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use super::*;
+
    use quickcheck_macros::quickcheck;
+
    use std::collections::HashSet;
+

+
    #[quickcheck]
+
    fn prop_user_id_equality(a: UserId, b: UserId) {
+
        assert_ne!(a, b);
+

+
        let mut hm = HashSet::new();
+

+
        assert!(hm.insert(a.clone()));
+
        assert!(hm.insert(b.clone()));
+
        assert!(!hm.insert(a));
+
        assert!(!hm.insert(b));
+
    }
+
}
added node/src/lib.rs
@@ -0,0 +1,19 @@
+
#![allow(dead_code)]
+
pub use nakamoto_net::{Io, Link, LocalDuration, LocalTime};
+

+
mod address_book;
+
mod address_manager;
+
mod clock;
+
mod collections;
+
mod decoder;
+
mod hash;
+
mod identity;
+
mod logger;
+
mod protocol;
+
mod storage;
+
#[cfg(test)]
+
mod test;
+

+
pub fn run() -> anyhow::Result<()> {
+
    Ok(())
+
}
added node/src/logger.rs
@@ -0,0 +1,61 @@
+
//! Logging module.
+
use std::io;
+

+
use chrono::prelude::*;
+
use colored::*;
+
use log::{Level, Log, Metadata, Record, SetLoggerError};
+

+
struct Logger {
+
    level: Level,
+
}
+

+
impl Log for Logger {
+
    fn enabled(&self, metadata: &Metadata) -> bool {
+
        metadata.level() <= self.level
+
    }
+

+
    fn log(&self, record: &Record) {
+
        if self.enabled(record.metadata()) {
+
            let module = record.module_path().unwrap_or_default();
+

+
            if record.level() == Level::Error {
+
                write(record, module, io::stderr());
+
            } else {
+
                write(record, module, io::stdout());
+
            }
+

+
            fn write(record: &log::Record, module: &str, mut stream: impl io::Write) {
+
                let message = format!("{} {} {}", record.level(), module.bold(), record.args());
+
                let message = match record.level() {
+
                    Level::Error => message.red(),
+
                    Level::Warn => message.yellow(),
+
                    Level::Info => message.normal(),
+
                    Level::Debug => message.dimmed(),
+
                    Level::Trace => message.white().dimmed(),
+
                };
+

+
                writeln!(
+
                    stream,
+
                    "{} {}",
+
                    Local::now()
+
                        .to_rfc3339_opts(SecondsFormat::Millis, true)
+
                        .white(),
+
                    message,
+
                )
+
                .expect("write shouldn't fail");
+
            }
+
        }
+
    }
+

+
    fn flush(&self) {}
+
}
+

+
/// Initialize a new logger.
+
pub fn init(level: Level) -> Result<(), SetLoggerError> {
+
    let logger = Logger { level };
+

+
    log::set_boxed_logger(Box::new(logger))?;
+
    log::set_max_level(level.to_level_filter());
+

+
    Ok(())
+
}
added node/src/main.rs
@@ -0,0 +1,3 @@
+
fn main() -> anyhow::Result<()> {
+
    radicle_node::run()
+
}
added node/src/protocol.rs
@@ -0,0 +1,806 @@
+
#![allow(dead_code)]
+
use std::ops::{Deref, DerefMut};
+
use std::{collections::VecDeque, fmt, io, net, net::IpAddr};
+

+
use fastrand::Rng;
+
use log::*;
+
use nakamoto::{LocalDuration, LocalTime};
+
use nakamoto_net as nakamoto;
+
use nakamoto_net::{Io, Link};
+
use serde::{Deserialize, Serialize};
+

+
use crate::address_book;
+
use crate::address_book::AddressBook;
+
use crate::address_manager::AddressManager;
+
use crate::clock::RefClock;
+
use crate::collections::{HashMap, HashSet};
+
use crate::decoder::Decoder;
+
use crate::identity::{ProjId, UserId};
+
use crate::storage;
+
use crate::storage::{Inventory, ReadStorage, Refs, WriteStorage};
+

+
/// Network peer identifier.
+
pub type PeerId = IpAddr;
+
/// Network routing table. Keeps track of where projects are hosted.
+
pub type Routing = HashMap<ProjId, HashSet<PeerId>>;
+

+
pub const NETWORK_MAGIC: u32 = 0x819b43d9;
+
pub const DEFAULT_PORT: u16 = 8776;
+
pub const PROTOCOL_VERSION: u32 = 1;
+
pub const TARGET_OUTBOUND_PEERS: usize = 8;
+
pub const IDLE_INTERVAL: LocalDuration = LocalDuration::from_secs(30);
+
pub const ANNOUNCE_INTERVAL: LocalDuration = LocalDuration::from_secs(30);
+
pub const SYNC_INTERVAL: LocalDuration = LocalDuration::from_secs(60);
+
pub const PRUNE_INTERVAL: LocalDuration = LocalDuration::from_mins(30);
+
pub const MAX_CONNECTION_ATTEMPTS: usize = 3;
+

+
/// Commands sent to the protocol by the operator.
+
#[derive(Debug)]
+
pub enum Command {
+
    Connect(net::SocketAddr),
+
}
+

+
/// Message envelope. All messages sent over the network are wrapped in this type.
+
#[derive(Debug, Serialize, Deserialize)]
+
pub struct Envelope {
+
    /// Network magic constant. Used to differentiate networks.
+
    pub magic: u32,
+
    /// The message payload.
+
    pub msg: Message,
+
}
+

+
/// Message payload.
+
/// These are the messages peers send to each other.
+
#[derive(Debug, Serialize, Deserialize, Clone)]
+
pub enum Message {
+
    /// Say hello to a peer. This is the first message sent to a peer after connection.
+
    Hello { version: u32 },
+
    /// Get node addresses from a peer.
+
    GetAddrs,
+
    /// Send node addresses to a peer. Sent in response to [`Message::GetAddrs`].
+
    Addrs { addrs: Vec<net::SocketAddr> },
+
    /// Get a peer's inventory.
+
    GetInventory { ids: Vec<ProjId> },
+
    /// Send our inventory to a peer. Sent in response to [`Message::GetInventory`].
+
    /// Nb. This should be the whole inventory, not a partial update.
+
    Inventory { seq: u64, inv: Inventory },
+
}
+

+
impl From<Message> for Envelope {
+
    fn from(msg: Message) -> Self {
+
        Self {
+
            magic: NETWORK_MAGIC,
+
            msg,
+
        }
+
    }
+
}
+

+
impl Message {
+
    pub fn hello() -> Self {
+
        Self::Hello {
+
            version: PROTOCOL_VERSION,
+
        }
+
    }
+

+
    pub fn inventory<S, T>(ctx: &mut Context<S, T>) -> Result<Self, storage::Error>
+
    where
+
        T: storage::ReadStorage,
+
    {
+
        ctx.seq += 1;
+

+
        let seq = ctx.seq;
+
        let inv = ctx.storage.inventory()?;
+

+
        Ok(Self::Inventory { seq, inv })
+
    }
+

+
    pub fn get_inventory(ids: impl Into<Vec<ProjId>>) -> Self {
+
        Self::GetInventory { ids: ids.into() }
+
    }
+
}
+

+
/// Project tracking policy.
+
#[derive(Debug)]
+
pub enum ProjectTracking {
+
    /// Track all projects we come across.
+
    All { blocked: HashSet<ProjId> },
+
    /// Track a static list of projects.
+
    Allowed(HashSet<ProjId>),
+
}
+

+
impl Default for ProjectTracking {
+
    fn default() -> Self {
+
        Self::All {
+
            blocked: HashSet::default(),
+
        }
+
    }
+
}
+

+
/// Project remote tracking policy.
+
#[derive(Debug, Default)]
+
pub enum RemoteTracking {
+
    /// Only track remotes of project delegates.
+
    #[default]
+
    DelegatesOnly,
+
    /// Track all remotes.
+
    All { blocked: HashSet<UserId> },
+
    /// Track a specific list of users as well as the project delegates.
+
    Allowed(HashSet<UserId>),
+
}
+

+
/// Protocol configuration.
+
#[derive(Debug, Default)]
+
pub struct Config {
+
    /// Peers to connect to on startup.
+
    /// Connections to these peers will be maintained.
+
    pub connect: Vec<net::SocketAddr>,
+
    /// Project tracking policy.
+
    pub project_tracking: ProjectTracking,
+
    /// Project remote tracking policy.
+
    pub remote_tracking: RemoteTracking,
+
}
+

+
impl Config {
+
    pub fn is_persistent(&self, addr: &net::SocketAddr) -> bool {
+
        self.connect.contains(addr)
+
    }
+

+
    /// Track a project. Returns whether the policy was updated.
+
    pub fn track(&mut self, proj: ProjId) -> bool {
+
        match &mut self.project_tracking {
+
            ProjectTracking::All { .. } => false,
+
            ProjectTracking::Allowed(projs) => projs.insert(proj),
+
        }
+
    }
+

+
    /// Untrack a project. Returns whether the policy was updated.
+
    pub fn untrack(&mut self, proj: ProjId) -> bool {
+
        match &mut self.project_tracking {
+
            ProjectTracking::All { blocked } => blocked.insert(proj),
+
            ProjectTracking::Allowed(projs) => projs.remove(&proj),
+
        }
+
    }
+
}
+

+
#[derive(Debug)]
+
pub struct Protocol<S, T> {
+
    /// Peers currently or recently connected.
+
    peers: Peers,
+
    /// Protocol state that isn't peer-specific.
+
    context: Context<S, T>,
+
    /// Whether our local inventory no long represents what we have announced to the network.
+
    out_of_sync: bool,
+
    /// Last time the protocol was idle.
+
    last_idle: LocalTime,
+
    /// Last time the protocol synced.
+
    last_sync: LocalTime,
+
    /// Last time the protocol routing table was pruned.
+
    last_prune: LocalTime,
+
    /// Last time the protocol announced its inventory.
+
    last_announce: LocalTime,
+
    /// Time when the protocol was initialized.
+
    start_time: LocalTime,
+
}
+

+
impl<T: ReadStorage + WriteStorage, S: address_book::Store> Protocol<S, T> {
+
    pub fn new(config: Config, clock: RefClock, storage: T, addresses: S, rng: Rng) -> Self {
+
        let addrmgr = AddressManager::new(addresses);
+

+
        Self {
+
            context: Context::new(config, clock, storage, addrmgr, rng.clone()),
+
            peers: Peers::new(rng),
+
            out_of_sync: false,
+
            last_idle: LocalTime::default(),
+
            last_sync: LocalTime::default(),
+
            last_prune: LocalTime::default(),
+
            last_announce: LocalTime::default(),
+
            start_time: LocalTime::default(),
+
        }
+
    }
+

+
    pub fn disconnect(&mut self, peer: &PeerId) {
+
        if let Some(addr) = self.peers.get(peer).map(|p| p.addr) {
+
            self.outbox()
+
                .push_back(Io::Disconnect(addr, DisconnectReason::User));
+
        }
+
    }
+

+
    pub fn providers(&self, proj: &ProjId) -> Box<dyn Iterator<Item = &Peer> + '_> {
+
        if let Some(peers) = self.routing.get(proj) {
+
            Box::new(peers.iter().filter_map(|id| self.peers.get(id)))
+
        } else {
+
            Box::new(std::iter::empty())
+
        }
+
    }
+

+
    pub fn tracked(&self) -> Result<Vec<ProjId>, storage::Error> {
+
        let tracked = match &self.config.project_tracking {
+
            ProjectTracking::All { blocked } => self
+
                .storage
+
                .inventory()?
+
                .into_iter()
+
                .map(|(id, _)| id)
+
                .filter(|id| !blocked.contains(id))
+
                .collect(),
+

+
            ProjectTracking::Allowed(projs) => projs.iter().cloned().collect(),
+
        };
+

+
        Ok(tracked)
+
    }
+

+
    /// Track a project.
+
    /// Returns whether or not the tracking policy was updated.
+
    pub fn track(&mut self, proj: ProjId) -> bool {
+
        self.out_of_sync = self.config.track(proj);
+
        self.out_of_sync
+
    }
+

+
    /// Untrack a project.
+
    /// Returns whether or not the tracking policy was updated.
+
    /// Note that when untracking, we don't announce anything to the network. This is because by
+
    /// simply not announcing it anymore, it will eventually be pruned by nodes.
+
    pub fn untrack(&mut self, proj: ProjId) -> bool {
+
        self.config.untrack(proj)
+
    }
+

+
    /// Find the closest `n` peers by proximity in tracking graphs.
+
    /// Returns a sorted list from the closest peer to the furthest.
+
    /// Peers with more trackings in common score score higher.
+
    #[allow(unused)]
+
    pub fn closest_peers(&self, n: usize) -> Vec<PeerId> {
+
        todo!()
+
    }
+

+
    /// Get the connected peers.
+
    pub fn peers(&self) -> &Peers {
+
        &self.peers
+
    }
+

+
    /// Get the current inventory.
+
    pub fn inventory(&self) -> Result<Inventory, storage::Error> {
+
        self.context.storage.inventory()
+
    }
+

+
    /// Get the storage instance.
+
    pub fn storage(&self) -> &T {
+
        &self.context.storage
+
    }
+

+
    /// Get the local protocol time.
+
    pub fn local_time(&self) -> LocalTime {
+
        self.context.clock.local_time()
+
    }
+

+
    /// Get protocol configuration.
+
    pub fn config(&self) -> &Config {
+
        &self.context.config
+
    }
+

+
    /// Get reference to routing table.
+
    pub fn routing(&self) -> &Routing {
+
        &self.context.routing
+
    }
+

+
    /// Get I/O outbox.
+
    pub fn outbox(&mut self) -> &mut VecDeque<Io<(), DisconnectReason>> {
+
        &mut self.context.io
+
    }
+

+
    pub fn lookup(&self, proj: &ProjId) -> Lookup {
+
        Lookup {
+
            local: self.context.storage.get(proj).unwrap(),
+
            remote: self
+
                .context
+
                .routing
+
                .get(proj)
+
                .map_or(vec![], |r| r.iter().copied().collect()),
+
        }
+
    }
+

+
    ////////////////////////////////////////////////////////////////////////////
+
    // Periodic tasks
+
    ////////////////////////////////////////////////////////////////////////////
+

+
    /// Announce our inventory to all connected peers.
+
    fn announce_inventory(&mut self) -> Result<(), storage::Error> {
+
        let inv = Message::inventory(&mut self.context)?;
+

+
        for addr in self.peers.negotiated().map(|(_, p)| p.addr) {
+
            self.context.write(addr, inv.clone());
+
        }
+
        Ok(())
+
    }
+

+
    fn get_inventories(&mut self) -> Result<(), storage::Error> {
+
        let mut msgs = Vec::new();
+
        for proj in self.tracked()? {
+
            for peer in self.providers(&proj) {
+
                if peer.is_negotiated() {
+
                    msgs.push((
+
                        peer.addr,
+
                        Message::GetInventory {
+
                            ids: vec![proj.clone()],
+
                        },
+
                    ));
+
                }
+
            }
+
        }
+
        for (remote, msg) in msgs {
+
            self.write(remote, msg);
+
        }
+

+
        Ok(())
+
    }
+

+
    fn prune_routing_entries(&mut self) {
+
        // TODO
+
    }
+

+
    fn maintain_connections(&mut self) {
+
        // TODO: Connect to all potential providers.
+
        if self.peers.len() < TARGET_OUTBOUND_PEERS {
+
            let delta = TARGET_OUTBOUND_PEERS - self.peers.len();
+

+
            for _ in 0..delta {
+
                // TODO: Connect to random peer.
+
            }
+
        }
+
    }
+
}
+

+
impl<S, T> nakamoto::Protocol for Protocol<S, T>
+
where
+
    T: ReadStorage + WriteStorage,
+
    S: address_book::Store,
+
{
+
    type Event = ();
+
    type Command = Command;
+
    type DisconnectReason = DisconnectReason;
+

+
    fn initialize(&mut self, time: LocalTime) {
+
        trace!("Init {}", time.as_secs());
+

+
        self.start_time = time;
+

+
        // Connect to configured peers.
+
        let addrs = self.context.config.connect.clone();
+
        for addr in addrs {
+
            self.context.connect(addr);
+
        }
+
    }
+

+
    fn tick(&mut self, now: nakamoto::LocalTime) {
+
        trace!("Tick +{}", now - self.start_time);
+

+
        self.context.clock.set(now);
+
    }
+

+
    fn wake(&mut self) {
+
        let now = self.context.clock.local_time();
+

+
        trace!("Wake +{}", now - self.start_time);
+

+
        if now - self.last_idle >= IDLE_INTERVAL {
+
            debug!("Running 'idle' task...");
+

+
            self.maintain_connections();
+
            self.context.io.push_back(Io::Wakeup(IDLE_INTERVAL));
+
            self.last_idle = now;
+
        }
+
        if now - self.last_sync >= SYNC_INTERVAL {
+
            debug!("Running 'sync' task...");
+

+
            self.get_inventories().unwrap();
+
            self.context.io.push_back(Io::Wakeup(SYNC_INTERVAL));
+
            self.last_sync = now;
+
        }
+
        if now - self.last_announce >= ANNOUNCE_INTERVAL {
+
            if self.out_of_sync {
+
                self.announce_inventory().unwrap();
+
            }
+
            self.context.io.push_back(Io::Wakeup(ANNOUNCE_INTERVAL));
+
            self.last_announce = now;
+
        }
+
        if now - self.last_prune >= PRUNE_INTERVAL {
+
            debug!("Running 'prune' task...");
+

+
            self.prune_routing_entries();
+
            self.context.io.push_back(Io::Wakeup(PRUNE_INTERVAL));
+
            self.last_prune = now;
+
        }
+
    }
+

+
    fn command(&mut self, cmd: Self::Command) {
+
        debug!("Command {:?}", cmd);
+

+
        match cmd {
+
            Command::Connect(addr) => self.context.connect(addr),
+
        }
+
    }
+

+
    fn attempted(&mut self, addr: &std::net::SocketAddr) {
+
        let id = addr.ip();
+
        let persistent = self.context.config.is_persistent(addr);
+
        let mut peer = self
+
            .peers
+
            .entry(id)
+
            .or_insert_with(|| Peer::new(*addr, Link::Outbound, persistent));
+

+
        peer.attempts += 1;
+
    }
+

+
    fn connected(
+
        &mut self,
+
        addr: std::net::SocketAddr,
+
        _local_addr: &std::net::SocketAddr,
+
        link: Link,
+
    ) {
+
        let id = addr.ip();
+

+
        debug!("Connected to {} ({:?})", id, link);
+

+
        // For outbound connections, we are the first to say "Hello".
+
        // For inbound connections, we wait for the remote to say "Hello" first.
+
        // TODO: How should we deal with multiple peers connecting from the same IP address?
+
        if link.is_outbound() {
+
            let since = self.local_time();
+

+
            if let Some(peer) = self.peers.get_mut(&id) {
+
                self.context
+
                    .write_all(peer.addr, [Message::hello(), Message::get_inventory([])]);
+

+
                peer.state = PeerState::Negotiated { since };
+
                peer.attempts = 0;
+
            }
+
        } else {
+
            self.peers.insert(
+
                id,
+
                Peer::new(
+
                    addr,
+
                    Link::Inbound,
+
                    self.context.config.is_persistent(&addr),
+
                ),
+
            );
+
        }
+
    }
+

+
    fn disconnected(
+
        &mut self,
+
        addr: &std::net::SocketAddr,
+
        reason: nakamoto::DisconnectReason<Self::DisconnectReason>,
+
    ) {
+
        let since = self.local_time();
+
        let id = addr.ip();
+

+
        debug!("Disconnected from {} ({})", id, reason);
+

+
        if let Some(peer) = self.peers.get_mut(&id) {
+
            peer.state = PeerState::Disconnected { since };
+

+
            // Attempt to re-connect to persistent peers.
+
            if self.context.config.is_persistent(addr) && peer.attempts < MAX_CONNECTION_ATTEMPTS {
+
                if reason.is_dial_err() {
+
                    return;
+
                }
+
                if let nakamoto::DisconnectReason::Protocol(r) = reason {
+
                    if !r.is_transient() {
+
                        return;
+
                    }
+
                }
+
                // TODO: Eventually we want a delay before attempting a reconnection,
+
                // with exponential back-off.
+
                debug!("Reconnecting to {} (attempts={})...", id, peer.attempts);
+

+
                // TODO: Try to reconnect only if the peer was attempted. A disconnect without
+
                // even a successful attempt means that we're unlikely to be able to reconnect.
+

+
                self.context.connect(*addr);
+
            } else {
+
                // TODO: Non-persistent peers should be removed from the
+
                // map here or at some later point.
+
            }
+
        }
+
    }
+

+
    fn received_bytes(&mut self, addr: &std::net::SocketAddr, bytes: &[u8]) {
+
        let peer = addr.ip();
+

+
        let (peer, msgs) = if let Some(peer) = self.peers.get_mut(&peer) {
+
            let decoder = &mut peer.inbox;
+
            decoder.input(bytes);
+

+
            let mut msgs = Vec::with_capacity(1);
+
            loop {
+
                match decoder.decode_next() {
+
                    Ok(Some(msg)) => msgs.push(msg),
+
                    Ok(None) => break,
+

+
                    Err(_err) => {
+
                        // TODO: Disconnect peer.
+
                        return;
+
                    }
+
                }
+
            }
+
            (peer, msgs)
+
        } else {
+
            return;
+
        };
+

+
        for msg in msgs {
+
            peer.received(msg, &mut self.context);
+
        }
+
    }
+
}
+

+
impl<S, T> Deref for Protocol<S, T> {
+
    type Target = Context<S, T>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.context
+
    }
+
}
+

+
impl<S, T> DerefMut for Protocol<S, T> {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.context
+
    }
+
}
+

+
#[derive(Debug, Clone)]
+
pub enum DisconnectReason {
+
    User,
+
}
+

+
impl DisconnectReason {
+
    fn is_transient(&self) -> bool {
+
        match self {
+
            Self::User => false,
+
        }
+
    }
+
}
+

+
impl From<DisconnectReason> for nakamoto_net::DisconnectReason<DisconnectReason> {
+
    fn from(reason: DisconnectReason) -> Self {
+
        nakamoto_net::DisconnectReason::Protocol(reason)
+
    }
+
}
+

+
impl fmt::Display for DisconnectReason {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        match self {
+
            Self::User => write!(f, "user"),
+
        }
+
    }
+
}
+

+
impl<S, T> Iterator for Protocol<S, T> {
+
    type Item = Io<(), DisconnectReason>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.context.io.pop_front()
+
    }
+
}
+

+
/// Result of a project lookup.
+
#[derive(Debug)]
+
pub struct Lookup {
+
    /// Whether the project was found locally or not.
+
    pub local: Option<Refs>,
+
    /// A list of remote peers on which the project is known to exist.
+
    pub remote: Vec<PeerId>,
+
}
+

+
/// Global protocol state used across peers.
+
#[derive(Debug)]
+
pub struct Context<S, T> {
+
    /// Protocol configuration.
+
    config: Config,
+
    /// Tracks the location of projects.
+
    routing: Routing,
+
    /// Outgoing I/O queue.
+
    io: VecDeque<Io<(), DisconnectReason>>,
+
    /// Clock. Tells the time.
+
    clock: RefClock,
+
    /// Sequence number used for inventory messages.
+
    seq: u64,
+
    /// Project storage.
+
    storage: T,
+
    /// Peer address manager.
+
    addrmgr: AddressManager<S>,
+
    /// Source of entropy.
+
    rng: Rng,
+
}
+

+
impl<S, T> Context<S, T>
+
where
+
    T: storage::ReadStorage,
+
{
+
    fn new(
+
        config: Config,
+
        clock: RefClock,
+
        storage: T,
+
        addrmgr: AddressManager<S>,
+
        rng: Rng,
+
    ) -> Self {
+
        Self {
+
            config,
+
            clock,
+
            routing: HashMap::with_hasher(rng.clone().into()),
+
            io: VecDeque::new(),
+
            seq: 0,
+
            storage,
+
            addrmgr,
+
            rng,
+
        }
+
    }
+

+
    /// Process a peer inventory announcement by updating our routing table.
+
    fn process_inventory(&mut self, from: PeerId, inventory: Inventory) {
+
        for (proj_id, _refs) in inventory {
+
            let inventory = self
+
                .routing
+
                .entry(proj_id)
+
                .or_insert_with(|| HashSet::with_hasher(self.rng.clone().into()));
+

+
            // TODO: If we're tracking this project, check the refs to see if we need to
+
            // fetch updates from this peer.
+

+
            inventory.insert(from);
+
        }
+
    }
+
}
+

+
impl<S, T> Context<S, T> {
+
    /// Connect to a peer.
+
    fn connect(&mut self, addr: net::SocketAddr) {
+
        // TODO: Make sure we don't try to connect more than once to the same address.
+
        self.io.push_back(Io::Connect(addr));
+
    }
+

+
    fn write_all(&mut self, remote: net::SocketAddr, msgs: impl IntoIterator<Item = Message>) {
+
        let mut buf = io::Cursor::new(Vec::new());
+

+
        for msg in msgs {
+
            debug!("Write {:?} to {}", &msg, remote.ip());
+

+
            serde_json::to_writer(&mut buf, &Envelope::from(msg)).unwrap();
+
        }
+
        self.io.push_back(Io::Write(remote, buf.into_inner()));
+
    }
+

+
    fn write(&mut self, remote: net::SocketAddr, msg: Message) {
+
        debug!("Write {:?} to {}", &msg, remote.ip());
+

+
        let bytes = serde_json::to_vec(&Envelope::from(msg)).unwrap();
+

+
        self.io.push_back(Io::Write(remote, bytes));
+
    }
+
}
+

+
#[derive(Debug)]
+
pub struct Peers(AddressBook<PeerId, Peer>);
+

+
impl Peers {
+
    pub fn new(rng: Rng) -> Self {
+
        Self(AddressBook::new(rng))
+
    }
+

+
    /// Iterator over fully negotiated peers.
+
    pub fn negotiated(&self) -> impl Iterator<Item = (&IpAddr, &Peer)> + Clone {
+
        self.0.iter().filter(move |(_, p)| p.is_negotiated())
+
    }
+
}
+

+
impl Deref for Peers {
+
    type Target = AddressBook<PeerId, Peer>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl DerefMut for Peers {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.0
+
    }
+
}
+

+
#[derive(Debug, Default)]
+
enum PeerState {
+
    /// Initial peer state. For outgoing peers this
+
    /// means we've attempted a connection. For incoming
+
    /// peers, this means they've successfully connected
+
    /// to us.
+
    #[default]
+
    Initial,
+
    /// State after successful handshake.
+
    Negotiated { since: LocalTime },
+
    /// When a peer is disconnected.
+
    Disconnected { since: LocalTime },
+
}
+

+
#[derive(Debug)]
+
pub struct Peer {
+
    /// Peer address.
+
    addr: net::SocketAddr,
+
    /// Inbox for incoming messages from peer.
+
    inbox: Decoder,
+
    /// Peer connection state.
+
    state: PeerState,
+
    /// Connection direction.
+
    link: Link,
+
    /// Whether we should attempt to re-connect
+
    /// to this peer upon disconnection.
+
    persistent: bool,
+
    /// Connection attempts. For persistent peers, Tracks
+
    /// how many times we've attempted to connect. We reset this to zero
+
    /// upon successful connection.
+
    attempts: usize,
+
}
+

+
impl Peer {
+
    fn new(addr: net::SocketAddr, link: Link, persistent: bool) -> Self {
+
        Self {
+
            addr,
+
            inbox: Decoder::new(256),
+
            state: PeerState::default(),
+
            link,
+
            persistent,
+
            attempts: 0,
+
        }
+
    }
+

+
    fn id(&self) -> PeerId {
+
        self.addr.ip()
+
    }
+

+
    fn is_negotiated(&self) -> bool {
+
        matches!(self.state, PeerState::Negotiated { .. })
+
    }
+

+
    fn received<S, T>(&mut self, envelope: Envelope, ctx: &mut Context<S, T>)
+
    where
+
        T: storage::ReadStorage,
+
    {
+
        if envelope.magic != NETWORK_MAGIC {
+
            // TODO: Disconnect
+
            return;
+
        }
+
        debug!("Received {:?} from {}", &envelope.msg, self.id());
+

+
        match envelope.msg {
+
            Message::Hello { .. } => {
+
                if let PeerState::Initial = self.state {
+
                    // TODO: Check version.
+
                    // Nb. This is a very primitive handshake. Eventually we should have anyhow
+
                    // extra "acknowledgment" message sent when the `Hello` is well received.
+
                    if self.link.is_inbound() {
+
                        ctx.write_all(self.addr, [Message::hello(), Message::get_inventory([])]);
+
                    }
+
                    self.state = PeerState::Negotiated {
+
                        since: ctx.clock.local_time(),
+
                    };
+
                } else {
+
                    // TODO: Handle misbehavior.
+
                }
+
            }
+
            Message::GetInventory { .. } => {
+
                // TODO: Handle partial inventory requests.
+
                let inventory = Message::inventory(ctx).unwrap();
+
                ctx.write(self.addr, inventory);
+
            }
+
            Message::Inventory { inv, .. } => {
+
                ctx.process_inventory(self.id(), inv);
+
            }
+
            Message::GetAddrs => {
+
                // TODO: Send peer addresses.
+
                todo!();
+
            }
+
            Message::Addrs { .. } => {
+
                // TODO: Update address book.
+
                todo!();
+
            }
+
        }
+
    }
+
}
added node/src/storage.rs
@@ -0,0 +1,194 @@
+
use std::marker::PhantomData;
+
use std::ops::{Deref, DerefMut};
+
use std::path::Path;
+
use std::{fmt, fs, io, net};
+

+
use git_ref_format::refspec;
+
use once_cell::sync::Lazy;
+
use radicle_git_ext as git_ext;
+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
pub use radicle_git_ext::Oid;
+

+
use crate::collections::HashMap;
+
use crate::identity;
+
use crate::identity::{IdError, ProjId, UserId};
+

+
pub static RAD_ID_GLOB: Lazy<refspec::PatternString> =
+
    Lazy::new(|| refspec::pattern!("refs/namespaces/*/refs/rad/id"));
+
pub static IDENTITY_PATH: Lazy<&Path> = Lazy::new(|| Path::new(".rad/identity.toml"));
+

+
pub type BranchName = String;
+
pub type Inventory = Vec<(ProjId, Refs)>;
+

+
/// Storage error.
+
#[derive(Error, Debug)]
+
pub enum Error {
+
    #[error("invalid git reference")]
+
    InvalidRef,
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("id: {0}")]
+
    ProjId(#[from] IdError),
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("doc: {0}")]
+
    Doc(#[from] identity::DocError),
+
    #[error("invalid repository head")]
+
    InvalidHead,
+
}
+

+
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
+
pub struct Refs {
+
    heads: HashMap<BranchName, Oid>,
+
}
+

+
impl From<HashMap<BranchName, Oid>> for Refs {
+
    fn from(heads: HashMap<BranchName, Oid>) -> Self {
+
        Self { heads }
+
    }
+
}
+

+
pub type RemoteId = UserId;
+
pub type RefName = String;
+

+
/// Verified (used as type witness).
+
pub struct Verified;
+
/// Unverified (used as type witness).
+
pub struct Unverified;
+

+
/// Project remotes. Tracks the git state of a project.
+
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
+
pub struct Remotes<V>(HashMap<RemoteId, Remote<V>>);
+

+
/// A project remote.
+
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
+
pub struct Remote<V> {
+
    /// Git references published under this remote, and their hashes.
+
    refs: HashMap<RefName, Oid>,
+
    /// Whether this remote is of a project delegate.
+
    delegate: bool,
+
    /// Whether the remote is verified or not, ie. whether its signed refs were checked.
+
    verified: PhantomData<V>,
+
}
+

+
pub trait ReadStorage {
+
    fn get(&self, proj: &ProjId) -> Result<Option<Refs>, Error>;
+
    fn inventory(&self) -> Result<Inventory, Error>;
+
}
+

+
pub trait WriteStorage {
+
    /// Fetch a project from a remote peer.
+
    fn fetch(&mut self, proj: &ProjId, remote: &net::SocketAddr) -> Result<(), Error>;
+
}
+

+
impl<T, S> ReadStorage for T
+
where
+
    T: Deref<Target = S>,
+
    S: ReadStorage,
+
{
+
    fn inventory(&self) -> Result<Inventory, Error> {
+
        self.deref().inventory()
+
    }
+

+
    fn get(&self, proj: &ProjId) -> Result<Option<Refs>, Error> {
+
        self.deref().get(proj)
+
    }
+
}
+

+
impl<T, S> WriteStorage for T
+
where
+
    T: DerefMut<Target = S>,
+
    S: WriteStorage,
+
{
+
    fn fetch(&mut self, proj: &ProjId, remote: &net::SocketAddr) -> Result<(), Error> {
+
        self.deref_mut().fetch(proj, remote)
+
    }
+
}
+

+
pub struct Storage {
+
    backend: git2::Repository,
+
}
+

+
impl fmt::Debug for Storage {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Storage(..)")
+
    }
+
}
+

+
impl ReadStorage for Storage {
+
    fn get(&self, _id: &ProjId) -> Result<Option<Refs>, Error> {
+
        todo!()
+
    }
+

+
    fn inventory(&self) -> Result<Inventory, Error> {
+
        let glob: String = RAD_ID_GLOB.clone().into();
+
        let refs = self.backend.references_glob(glob.as_str())?;
+
        let mut projs = Vec::new();
+

+
        for r in refs {
+
            let r = r?;
+
            let name = r.name().ok_or(Error::InvalidRef)?;
+
            let id = ProjId::from_ref(name)?;
+

+
            projs.push((id, Refs::default()));
+
        }
+
        Ok(projs)
+
    }
+
}
+

+
impl WriteStorage for Storage {
+
    fn fetch(&mut self, _id: &ProjId, _remote: &net::SocketAddr) -> Result<(), Error> {
+
        todo!()
+
    }
+
}
+

+
impl Storage {
+
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
+
        let path = path.as_ref();
+
        let backend = match git2::Repository::open_bare(path) {
+
            Err(e) if git_ext::is_not_found_err(&e) => {
+
                let backend = git2::Repository::init_opts(
+
                    path,
+
                    git2::RepositoryInitOptions::new()
+
                        .bare(true)
+
                        .no_reinit(true)
+
                        .external_template(false),
+
                )?;
+

+
                Ok(backend)
+
            }
+
            Ok(repo) => Ok(repo),
+
            Err(e) => Err(e),
+
        }?;
+

+
        Ok(Self { backend })
+
    }
+

+
    pub fn create(
+
        &self,
+
        repo: &git2::Repository,
+
        identity: impl Into<identity::Doc>,
+
    ) -> Result<(ProjId, git2::Reference), Error> {
+
        let doc = identity.into();
+
        let file = fs::OpenOptions::new()
+
            .create_new(true)
+
            .write(true)
+
            .open(*IDENTITY_PATH)?;
+
        let id = doc.write(file)?;
+
        let ref_name = RAD_ID_GLOB.replace('*', &id.encode());
+
        let oid = repo.head()?.target().ok_or(Error::InvalidHead)?;
+
        let reference = self.backend.reference(&ref_name, oid, false, "")?;
+

+
        // TODO: Push project to monorepo.
+

+
        Ok((id, reference))
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    #[test]
+
    fn test_storage() {}
+
}
added node/src/test.rs
@@ -0,0 +1,6 @@
+
mod arbitrary;
+
mod assert;
+
mod logger;
+
mod peer;
+
mod storage;
+
mod tests;
added node/src/test/arbitrary.rs
@@ -0,0 +1,58 @@
+
use crate::collections::HashMap;
+
use crate::hash;
+
use crate::identity::{ProjId, UserId};
+
use crate::storage;
+

+
impl quickcheck::Arbitrary for storage::Refs {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
+
        let mut refs: HashMap<storage::BranchName, storage::Oid> = HashMap::with_hasher(rng.into());
+
        let mut bytes: [u8; 20] = [0; 20];
+
        let names = &["master", "dev", "feature/1", "feature/2", "feature/3"];
+

+
        for _ in 0..g.size().min(2) {
+
            if let Some(name) = g.choose(names) {
+
                for byte in &mut bytes {
+
                    *byte = u8::arbitrary(g);
+
                }
+
                let oid = storage::Oid::try_from(&bytes[..]).unwrap();
+
                refs.insert(name.to_string(), oid);
+
            }
+
        }
+
        storage::Refs::from(refs)
+
    }
+
}
+

+
impl quickcheck::Arbitrary for ProjId {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let digest = hash::Digest::arbitrary(g);
+
        ProjId::from(digest)
+
    }
+
}
+

+
impl quickcheck::Arbitrary for hash::Digest {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let mut bytes: [u8; 32] = [0; 32];
+

+
        for byte in &mut bytes {
+
            *byte = u8::arbitrary(g);
+
        }
+
        hash::Digest::from(bytes)
+
    }
+
}
+

+
impl quickcheck::Arbitrary for UserId {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        use ed25519_consensus::SigningKey;
+

+
        let mut bytes: [u8; 32] = [0; 32];
+

+
        for byte in &mut bytes {
+
            *byte = u8::arbitrary(g);
+
        }
+
        let sk = SigningKey::from(bytes);
+
        let vk = sk.verification_key();
+

+
        UserId(vk)
+
    }
+
}
added node/src/test/assert.rs
@@ -0,0 +1,296 @@
+
// Copyright (c) 2016 Murarth
+
//
+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+
// and associated documentation files (the "Software"), to deal in the Software without
+
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
+
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+
// Software is furnished to do so, subject to the following conditions:
+
//
+
// The above copyright notice and this permission notice shall be included in all copies or
+
// substantial portions of the Software.
+
//
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+

+
//! Provides a macro, `assert_matches!`, which tests whether a value
+
//! matches a given pattern, causing a panic if the match fails.
+
//!
+
//! See the macro [`assert_matches!`] documentation for more information.
+
//!
+
//! Also provides a debug-only counterpart, [`debug_assert_matches!`].
+
//!
+
//! See the macro [`debug_assert_matches!`] documentation for more information
+
//! about this macro.
+
//!
+
//! [`assert_matches!`]: macro.assert_matches.html
+
//! [`debug_assert_matches!`]: macro.debug_assert_matches.html
+

+
#![deny(missing_docs)]
+

+
/// Asserts that an expression matches a given pattern.
+
///
+
/// A guard expression may be supplied to add further restrictions to the
+
/// expected value of the expression.
+
///
+
/// A `match` arm may be supplied to perform additional assertions or to yield
+
/// a value from the macro invocation.
+
///
+
#[macro_export]
+
macro_rules! assert_matches {
+
    ( $e:expr , $($pat:pat_param)|+ ) => {
+
        match $e {
+
            $($pat)|+ => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr ) => {
+
        match $e {
+
            $($pat)|+ if $cond => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+ if $cond))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ => $arm:expr ) => {
+
        match $e {
+
            $($pat)|+ => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr => $arm:expr ) => {
+
        match $e {
+
            $($pat)|+ if $cond => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+ if $cond))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+), format_args!($($arg)*))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ if $cond => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+ if $cond), format_args!($($arg)*))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ => $arm:expr , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+), format_args!($($arg)*))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr => $arm:expr , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ if $cond => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+ if $cond), format_args!($($arg)*))
+
        }
+
    };
+
}
+

+
/// Asserts that an expression matches a given pattern.
+
///
+
/// Unlike [`assert_matches!`], `debug_assert_matches!` statements are only enabled
+
/// in non-optimized builds by default. An optimized build will omit all
+
/// `debug_assert_matches!` statements unless `-C debug-assertions` is passed
+
/// to the compiler.
+
///
+
/// See the macro [`assert_matches!`] documentation for more information.
+
///
+
/// [`assert_matches!`]: macro.assert_matches.html
+
#[macro_export(local_inner_macros)]
+
macro_rules! debug_assert_matches {
+
    ( $($tt:tt)* ) => { {
+
        if _assert_matches_cfg!(debug_assertions) {
+
            assert_matches!($($tt)*);
+
        }
+
    } }
+
}
+

+
#[doc(hidden)]
+
#[macro_export]
+
macro_rules! _assert_matches_cfg {
+
    ( $($tt:tt)* ) => { cfg!($($tt)*) }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use std::panic::{catch_unwind, UnwindSafe};
+

+
    #[derive(Debug)]
+
    enum Foo {
+
        A(i32),
+
        B(&'static str),
+
        C(&'static str),
+
    }
+

+
    #[test]
+
    fn test_assert_succeed() {
+
        let a = Foo::A(123);
+

+
        assert_matches!(a, Foo::A(_));
+
        assert_matches!(a, Foo::A(123));
+
        assert_matches!(a, Foo::A(i) if i == 123);
+
        assert_matches!(a, Foo::A(42) | Foo::A(123));
+

+
        let b = Foo::B("foo");
+

+
        assert_matches!(b, Foo::B(_));
+
        assert_matches!(b, Foo::B("foo"));
+
        assert_matches!(b, Foo::B(s) if s == "foo");
+
        assert_matches!(b, Foo::B(s) => assert_eq!(s, "foo"));
+
        assert_matches!(b, Foo::B(s) => { assert_eq!(s, "foo") });
+
        assert_matches!(b, Foo::B(s) if s == "foo" => assert_eq!(s, "foo"));
+
        assert_matches!(b, Foo::B(s) if s == "foo" => { assert_eq!(s, "foo") });
+

+
        let c = Foo::C("foo");
+

+
        assert_matches!(c, Foo::B(_) | Foo::C(_));
+
        assert_matches!(c, Foo::B("foo") | Foo::C("foo"));
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo");
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) => assert_eq!(s, "foo"));
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) => { assert_eq!(s, "foo") });
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => assert_eq!(s, "foo"));
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => { assert_eq!(s, "foo") });
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn test_assert_panic_0() {
+
        let a = Foo::A(123);
+

+
        assert_matches!(a, Foo::B(_));
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn test_assert_panic_1() {
+
        let b = Foo::B("foo");
+

+
        assert_matches!(b, Foo::B("bar"));
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn test_assert_panic_2() {
+
        let b = Foo::B("foo");
+

+
        assert_matches!(b, Foo::B(s) if s == "bar");
+
    }
+

+
    #[test]
+
    fn test_assert_no_move() {
+
        let b = &mut Foo::A(0);
+
        assert_matches!(*b, Foo::A(0));
+
    }
+

+
    #[test]
+
    fn assert_with_message() {
+
        let a = Foo::A(0);
+

+
        assert_matches!(a, Foo::A(_), "o noes");
+
        assert_matches!(a, Foo::A(n) if n == 0, "o noes");
+
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes");
+
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes");
+
        assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes");
+
        assert_matches!(a, Foo::A(n) if n == 0 => { assert_eq!(n, 0); assert!(n < 1) }, "o noes");
+
        assert_matches!(a, Foo::A(_), "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(n) if n == 0, "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(_), "o noes {value:?}", value = a);
+
        assert_matches!(a, Foo::A(n) if n == 0, "o noes {value:?}", value=a);
+
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {value:?}", value=a);
+
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {value:?}", value=a);
+
        assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes {value:?}", value=a);
+
    }
+

+
    fn panic_message<F>(f: F) -> String
+
    where
+
        F: FnOnce() + UnwindSafe,
+
    {
+
        let err = catch_unwind(f).expect_err("function did not panic");
+

+
        *err.downcast::<String>()
+
            .expect("function panicked with non-String value")
+
    }
+

+
    #[test]
+
    fn test_panic_message() {
+
        let a = Foo::A(1);
+

+
        // expr, pat
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_));
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#
+
        );
+

+
        // expr, pat if cond
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#
+
        );
+

+
        // expr, pat => arm
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_) => {});
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#
+
        );
+

+
        // expr, pat if cond => arm
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo" => {});
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#
+
        );
+

+
        // expr, pat, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_), "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#
+
        );
+

+
        // expr, pat if cond, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo", "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#
+
        );
+

+
        // expr, pat => arm, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_) => {}, "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#
+
        );
+

+
        // expr, pat if cond => arm, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo" => {}, "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#
+
        );
+
    }
+
}
added node/src/test/logger.rs
@@ -0,0 +1,43 @@
+
use log::*;
+

+
struct Logger {
+
    level: Level,
+
}
+

+
impl Log for Logger {
+
    fn enabled(&self, metadata: &Metadata) -> bool {
+
        metadata.level() <= self.level
+
    }
+

+
    fn log(&self, record: &Record) {
+
        use colored::Colorize;
+

+
        match record.target() {
+
            "test" => {
+
                println!(
+
                    "{} {}",
+
                    "test:".yellow(),
+
                    record.args().to_string().yellow()
+
                )
+
            }
+
            "sim" => {
+
                println!("{}  {}", "sim:".bold(), record.args().to_string().bold())
+
            }
+
            target => {
+
                if self.enabled(record.metadata()) {
+
                    let s = format!("{:<8} {}", format!("{}:", target), record.args());
+
                    println!("{}", s.dimmed());
+
                }
+
            }
+
        }
+
    }
+

+
    fn flush(&self) {}
+
}
+

+
pub fn init(level: Level) {
+
    let logger = Logger { level };
+

+
    log::set_boxed_logger(Box::new(logger)).ok();
+
    log::set_max_level(level.to_level_filter());
+
}
added node/src/test/peer.rs
@@ -0,0 +1,181 @@
+
use std::net;
+
use std::ops::{Deref, DerefMut};
+

+
use log::*;
+
use nakamoto_net::simulator;
+
use nakamoto_net::Protocol as _;
+

+
use crate::address_book::{KnownAddress, Source};
+
use crate::clock::RefClock;
+
use crate::collections::HashMap;
+
use crate::decoder::Decoder;
+
use crate::protocol::*;
+
use crate::storage::{ReadStorage, WriteStorage};
+
use crate::*;
+

+
/// Protocol instantiation used for testing.
+
pub type Protocol<S> = crate::protocol::Protocol<HashMap<net::IpAddr, KnownAddress>, S>;
+

+
#[derive(Debug)]
+
pub struct Peer<S> {
+
    pub name: &'static str,
+
    pub protocol: Protocol<S>,
+
    pub ip: net::IpAddr,
+
    pub rng: fastrand::Rng,
+
    pub local_time: LocalTime,
+
    pub local_addr: net::SocketAddr,
+

+
    initialized: bool,
+
}
+

+
impl<S> simulator::Peer<Protocol<S>> for Peer<S>
+
where
+
    S: ReadStorage + WriteStorage + 'static,
+
{
+
    fn init(&mut self) {
+
        self.initialize()
+
    }
+

+
    fn addr(&self) -> net::SocketAddr {
+
        net::SocketAddr::new(self.ip, DEFAULT_PORT)
+
    }
+
}
+

+
impl<S> Deref for Peer<S> {
+
    type Target = Protocol<S>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.protocol
+
    }
+
}
+

+
impl<S> DerefMut for Peer<S> {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.protocol
+
    }
+
}
+

+
impl<S> Peer<S>
+
where
+
    S: ReadStorage + WriteStorage,
+
{
+
    pub fn new(name: &'static str, ip: impl Into<net::IpAddr>, storage: S) -> Self {
+
        Self::config(
+
            name,
+
            Config::default(),
+
            ip,
+
            vec![],
+
            storage,
+
            fastrand::Rng::new(),
+
        )
+
    }
+

+
    pub fn config(
+
        name: &'static str,
+
        config: Config,
+
        ip: impl Into<net::IpAddr>,
+
        addrs: Vec<(net::SocketAddr, Source)>,
+
        storage: S,
+
        rng: fastrand::Rng,
+
    ) -> Self {
+
        let addrs = addrs
+
            .into_iter()
+
            .map(|(addr, src)| (addr.ip(), KnownAddress::new(addr, src, None)))
+
            .collect();
+
        let local_time = LocalTime::now();
+
        let clock = RefClock::from(local_time);
+
        let protocol = Protocol::new(config, clock, storage, addrs, rng.clone());
+
        let ip = ip.into();
+
        let local_addr = net::SocketAddr::new(ip, rng.u16(..));
+

+
        Self {
+
            name,
+
            protocol,
+
            ip,
+
            local_addr,
+
            rng,
+
            local_time,
+
            initialized: false,
+
        }
+
    }
+

+
    pub fn initialize(&mut self) {
+
        if !self.initialized {
+
            info!("{}: Initializing: address = {}", self.name, self.ip);
+

+
            self.initialized = true;
+
            self.protocol.initialize(LocalTime::now());
+
        }
+
    }
+

+
    pub fn receive(&mut self, peer: &net::SocketAddr, msg: Message) {
+
        let bytes = serde_json::to_vec(&Envelope {
+
            magic: NETWORK_MAGIC,
+
            msg,
+
        })
+
        .unwrap();
+

+
        self.protocol.received_bytes(peer, &bytes);
+
    }
+

+
    pub fn connect_from(&mut self, remote: &net::SocketAddr) {
+
        let local = net::SocketAddr::new(self.ip, self.rng.u16(..));
+

+
        self.initialize();
+
        self.protocol.connected(*remote, &local, Link::Inbound);
+
        self.receive(remote, Message::hello());
+
        self.receive(
+
            remote,
+
            Message::Inventory {
+
                seq: 0,
+
                inv: vec![],
+
            },
+
        );
+

+
        let mut msgs = self.messages(remote);
+
        msgs.find(|m| matches!(m, Message::Hello { .. }))
+
            .expect("`hello` is sent");
+
        msgs.find(|m| matches!(m, Message::GetInventory { .. }))
+
            .expect("`get-inventory` is sent");
+
    }
+

+
    pub fn connect_to(&mut self, remote: &net::SocketAddr) {
+
        self.initialize();
+
        self.protocol.attempted(remote);
+
        self.protocol
+
            .connected(*remote, &self.local_addr, Link::Outbound);
+

+
        let mut msgs = self.messages(remote);
+
        msgs.find(|m| matches!(m, Message::Hello { .. }))
+
            .expect("`hello` is sent");
+
        msgs.find(|m| matches!(m, Message::GetInventory { .. }))
+
            .expect("`get-inventory` is sent");
+

+
        self.receive(remote, Message::hello());
+
    }
+

+
    /// Get outgoing messages sent from this peer to the remote address.
+
    pub fn messages(&mut self, remote: &net::SocketAddr) -> impl Iterator<Item = Message> {
+
        let mut stream = Decoder::<Envelope>::new(2048);
+
        let mut msgs = Vec::new();
+

+
        for o in self.protocol.outbox().iter() {
+
            match o {
+
                Io::Write(a, bytes) if a == remote => {
+
                    stream.input(bytes);
+
                }
+
                _ => {}
+
            }
+
        }
+

+
        while let Some(envelope) = stream.decode_next().unwrap() {
+
            msgs.push(envelope.msg);
+
        }
+
        msgs.into_iter()
+
    }
+

+
    /// Get a draining iterator over the peers's I/O outbox.
+
    pub fn outbox(&mut self) -> impl Iterator<Item = Io<(), DisconnectReason>> + '_ {
+
        self.protocol.outbox().drain(..)
+
    }
+
}
added node/src/test/storage.rs
@@ -0,0 +1,39 @@
+
use std::net;
+

+
use crate::identity::ProjId;
+
use crate::storage::{Error, Inventory, ReadStorage, Refs, WriteStorage};
+

+
pub struct MockStorage {
+
    pub inventory: Inventory,
+
}
+

+
impl MockStorage {
+
    pub fn new(inventory: Inventory) -> Self {
+
        Self { inventory }
+
    }
+

+
    pub fn empty() -> Self {
+
        Self {
+
            inventory: Vec::new(),
+
        }
+
    }
+
}
+

+
impl ReadStorage for MockStorage {
+
    fn get(&self, proj: &ProjId) -> Result<Option<Refs>, Error> {
+
        if let Some((_, refs)) = self.inventory.iter().find(|(id, _)| id == proj) {
+
            return Ok(Some(refs.clone()));
+
        }
+
        Ok(None)
+
    }
+

+
    fn inventory(&self) -> Result<Inventory, Error> {
+
        Ok(self.inventory.clone())
+
    }
+
}
+

+
impl WriteStorage for MockStorage {
+
    fn fetch(&mut self, _proj: &ProjId, _remote: &net::SocketAddr) -> Result<(), Error> {
+
        todo!()
+
    }
+
}
added node/src/test/tests.rs
@@ -0,0 +1,224 @@
+
use std::io;
+
use std::sync::Arc;
+

+
use nakamoto_net as nakamoto;
+
use nakamoto_net::simulator;
+
use nakamoto_net::simulator::{Peer as _, Simulation};
+
use nakamoto_net::Protocol as _;
+
use quickcheck_macros::quickcheck;
+

+
use crate::collections::{HashMap, HashSet};
+
use crate::protocol::*;
+
use crate::storage::{Inventory, ReadStorage};
+
#[allow(unused)]
+
use crate::test::logger;
+
use crate::test::peer::Peer;
+
use crate::test::storage::MockStorage;
+
use crate::*;
+

+
// NOTE
+
//
+
// If you wish to see the logs for a running test, simply add the following line to your test:
+
//
+
//      logger::init(log::Level::Debug);
+
//
+
// You may then run the test with eg. `cargo test -- --nocapture` to always show output.
+

+
#[test]
+
fn test_outbound_connection() {
+
    let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
+
    let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
+
    let eve = Peer::new("eve", [7, 7, 7, 7], MockStorage::empty());
+

+
    alice.connect_to(&bob.addr());
+
    alice.connect_to(&eve.addr());
+

+
    let peers = alice
+
        .protocol
+
        .peers()
+
        .negotiated()
+
        .map(|(ip, _)| *ip)
+
        .collect::<Vec<_>>();
+

+
    assert!(peers.contains(&eve.ip));
+
    assert!(peers.contains(&bob.ip));
+
}
+

+
#[test]
+
fn test_inbound_connection() {
+
    let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
+
    let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
+
    let eve = Peer::new("eve", [7, 7, 7, 7], MockStorage::empty());
+

+
    alice.connect_from(&bob.addr());
+
    alice.connect_from(&eve.addr());
+

+
    let peers = alice
+
        .protocol
+
        .peers()
+
        .negotiated()
+
        .map(|(ip, _)| *ip)
+
        .collect::<Vec<_>>();
+

+
    assert!(peers.contains(&eve.ip));
+
    assert!(peers.contains(&bob.ip));
+
}
+

+
#[test]
+
fn test_persistent_peer_connect() {
+
    let rng = fastrand::Rng::new();
+
    let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
+
    let eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
+
    let config = Config {
+
        connect: vec![bob.addr(), eve.addr()],
+
        ..Config::default()
+
    };
+
    let mut alice = Peer::config(
+
        "alice",
+
        config,
+
        [7, 7, 7, 7],
+
        vec![],
+
        MockStorage::empty(),
+
        rng,
+
    );
+

+
    alice.initialize();
+

+
    let mut outbox = alice.outbox();
+
    assert_matches!(outbox.next(), Some(Io::Connect(a)) if a == bob.addr());
+
    assert_matches!(outbox.next(), Some(Io::Connect(a)) if a == eve.addr());
+
    assert_matches!(outbox.next(), None);
+
}
+

+
#[test]
+
fn test_persistent_peer_reconnect() {
+
    let mut bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
+
    let mut eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
+
    let mut alice = Peer::config(
+
        "alice",
+
        Config {
+
            connect: vec![bob.addr(), eve.addr()],
+
            ..Config::default()
+
        },
+
        [7, 7, 7, 7],
+
        vec![],
+
        MockStorage::empty(),
+
        fastrand::Rng::new(),
+
    );
+

+
    let mut sim = Simulation::new(
+
        LocalTime::now(),
+
        alice.rng.clone(),
+
        simulator::Options::default(),
+
    )
+
    .initialize([&mut alice, &mut bob, &mut eve]);
+

+
    sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
+

+
    let ips = alice
+
        .peers()
+
        .negotiated()
+
        .map(|(ip, _)| *ip)
+
        .collect::<Vec<_>>();
+
    assert!(ips.contains(&bob.ip));
+
    assert!(ips.contains(&eve.ip));
+

+
    // ... Negotiated ...
+
    //
+
    // Now let's disconnect a peer.
+

+
    // A transient error such as this will cause Alice to attempt a reconnection.
+
    let error = Arc::new(io::Error::from(io::ErrorKind::ConnectionReset));
+

+
    // A non-transient disconnect, such as one requested by the user will not trigger
+
    // a reconnection.
+
    alice.disconnected(
+
        &eve.addr(),
+
        nakamoto::DisconnectReason::DialError(error.clone()),
+
    );
+
    assert_matches!(alice.outbox().next(), None);
+

+
    for _ in 0..MAX_CONNECTION_ATTEMPTS {
+
        alice.disconnected(
+
            &bob.addr(),
+
            nakamoto::DisconnectReason::ConnectionError(error.clone()),
+
        );
+
        assert_matches!(alice.outbox().next(), Some(Io::Connect(a)) if a == bob.addr());
+
        assert_matches!(alice.outbox().next(), None);
+

+
        alice.attempted(&bob.addr());
+
    }
+

+
    // After the max connection attempts, a disconnect doesn't trigger a reconnect.
+
    alice.disconnected(
+
        &bob.addr(),
+
        nakamoto::DisconnectReason::ConnectionError(error),
+
    );
+
    assert_matches!(alice.outbox().next(), None);
+
}
+

+
#[quickcheck]
+
fn prop_inventory_exchange_dense(alice_inv: Inventory, bob_inv: Inventory, eve_inv: Inventory) {
+
    let rng = fastrand::Rng::new();
+
    let alice = Peer::new("alice", [7, 7, 7, 7], MockStorage::new(alice_inv.clone()));
+
    let mut bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::new(bob_inv.clone()));
+
    let mut eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::new(eve_inv.clone()));
+
    let mut routing = Routing::with_hasher(rng.clone().into());
+

+
    for (inv, peer) in &[
+
        (alice_inv, alice.addr().ip()),
+
        (bob_inv, bob.addr().ip()),
+
        (eve_inv, eve.addr().ip()),
+
    ] {
+
        for (proj, _) in inv {
+
            routing
+
                .entry(proj.clone())
+
                .or_insert_with(|| HashSet::with_hasher(rng.clone().into()))
+
                .insert(*peer);
+
        }
+
    }
+

+
    // Fully-connected.
+
    bob.command(Command::Connect(alice.addr()));
+
    bob.command(Command::Connect(eve.addr()));
+
    eve.command(Command::Connect(alice.addr()));
+
    eve.command(Command::Connect(bob.addr()));
+

+
    let mut peers: HashMap<_, _> = [(alice.ip, alice), (bob.ip, bob), (eve.ip, eve)]
+
        .into_iter()
+
        .collect();
+
    let mut simulator = Simulation::new(LocalTime::now(), rng, simulator::Options::default())
+
        .initialize(peers.values_mut());
+

+
    simulator.run_while(peers.values_mut(), |s| !s.is_settled());
+

+
    for (proj_id, remotes) in &routing {
+
        for peer in peers.values() {
+
            let lookup = peer.lookup(proj_id);
+

+
            if lookup.local.is_some() {
+
                peer.storage()
+
                    .get(proj_id)
+
                    .expect("There are no errors querying storage")
+
                    .expect("The project is available locally");
+
            } else {
+
                for remote in &lookup.remote {
+
                    peers[remote]
+
                        .storage()
+
                        .get(proj_id)
+
                        .expect("There are no errors querying storage")
+
                        .expect("The project is available remotely");
+
                }
+
                assert!(
+
                    !lookup.remote.is_empty(),
+
                    "There are remote locations for the project"
+
                );
+
                assert_eq!(
+
                    &lookup.remote.into_iter().collect::<HashSet<_>>(),
+
                    remotes,
+
                    "The remotes match the global routing table"
+
                );
+
            }
+
        }
+
    }
+
}