diff --git a/Cargo.lock b/Cargo.lock index c0a9ae4d..a2ff1aee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,1141 @@ version = 3 [[package]] -name = "radicle-node" +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" diff --git a/Cargo.toml b/Cargo.toml index 44a96d2f..7b9c4986 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,20 @@ -[package] -name = "radicle-node" -version = "0.1.0" -authors = ["Alexis Sellier "] -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" diff --git a/node/Cargo.toml b/node/Cargo.toml new file mode 100644 index 00000000..48a190f0 --- /dev/null +++ b/node/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "radicle-node" +license = "MIT OR Apache-2.0" +version = "0.2.0" +authors = ["Alexis Sellier "] +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" } diff --git a/node/src/address_book.rs b/node/src/address_book.rs new file mode 100644 index 00000000..7fe0a96d --- /dev/null +++ b/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 { + inner: HashMap, + rng: fastrand::Rng, +} + +impl AddressBook { + /// 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 { + 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::>(); + self.rng.shuffle(&mut keys); + + keys.into_iter() + } +} + +impl Deref for AddressBook { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for AddressBook { + 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, + /// Last time this address was sampled. + #[serde(with = "local_time")] + pub last_sampled: Option, + /// Last time this address was tried. + #[serde(with = "local_time")] + pub last_attempt: Option, + /// Last time this peer was seen alive. + #[serde(with = "local_time")] + pub last_active: Option, +} + +impl KnownAddress { + /// Create a new known address. + pub fn new(addr: net::SocketAddr, source: Source, last_active: Option) -> 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, + file: fs::File, +} + +impl Cache { + /// Open an existing cache. + pub fn open>(path: P) -> io::Result { + fs::OpenOptions::new() + .read(true) + .write(true) + .open(path) + .and_then(Self::from) + } + + /// Create a new cache. + pub fn create>(path: P) -> io::Result { + 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 { + 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 { + self.addrs.remove(ip) + } + + fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool { + as Store>::insert(&mut self.addrs, ip, ka) + } + + fn iter<'a>(&'a self) -> Box + '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; + + /// Return an iterator over the known addresses. + fn iter<'a>(&'a self) -> Box + '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( + &mut self, + seeds: impl Iterator, + 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 { + 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 { + 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 + '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 { + 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 { + 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 + '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, D::Error> + where + D: Deserializer<'de>, + { + let value: Option = Deserialize::deserialize(deserializer)?; + + if let Some(value) = value { + Ok(Some(LocalTime::from_secs(value))) + } else { + Ok(None) + } + } + + pub fn serialize(value: &Option, serializer: S) -> Result + 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::>(); + + actual.sort_by_key(|(i, _)| *i); + expected.sort_by_key(|(i, _)| *i); + + assert_eq!(actual, expected); + } + } +} diff --git a/node/src/address_manager.rs b/node/src/address_manager.rs new file mode 100644 index 00000000..aa458663 --- /dev/null +++ b/node/src/address_manager.rs @@ -0,0 +1,12 @@ +use crate::address_book::Store; + +#[derive(Debug)] +pub struct AddressManager { + store: S, +} + +impl AddressManager { + pub fn new(store: S) -> Self { + Self { store } + } +} diff --git a/node/src/clock.rs b/node/src/clock.rs new file mode 100644 index 00000000..12181975 --- /dev/null +++ b/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>); + +impl std::ops::Deref for RefClock { + type Target = Rc>; + + 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 for RefClock { + fn from(other: LocalTime) -> Self { + Self(Rc::new(RefCell::new(other))) + } +} diff --git a/node/src/collections.rs b/node/src/collections.rs new file mode 100644 index 00000000..b464f388 --- /dev/null +++ b/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 = std::collections::HashMap; + +/// A `HashSet` which uses [`fastrand::Rng`] for its random state. +pub type HashSet = std::collections::HashSet; + +/// 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 for RandomState { + fn from(rng: fastrand::Rng) -> Self { + Self::new(rng) + } +} diff --git a/node/src/decoder.rs b/node/src/decoder.rs new file mode 100644 index 00000000..61ff8e1b --- /dev/null +++ b/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 { + unparsed: Vec, + item: PhantomData, +} + +impl From> for Decoder { + fn from(unparsed: Vec) -> Self { + Self { + unparsed, + item: PhantomData, + } + } +} + +impl<'de, D: Deserialize<'de>> Decoder { + /// 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, 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 { + type Item = Result; + + fn next(&mut self) -> Option { + 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::::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", + }) + ); + } +} diff --git a/node/src/hash.rs b/node/src/hash.rs new file mode 100644 index 00000000..7a903b55 --- /dev/null +++ b/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 { + 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::OutputSize>> for Digest { + fn from(array: GenericArray::OutputSize>) -> Self { + Self(array.into()) + } +} diff --git a/node/src/identity.rs b/node/src/identity.rs new file mode 100644 index 00000000..9127d1b0 --- /dev/null +++ b/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 { + 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 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(&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 { + 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, +} + +impl Doc { + pub fn write(&self, mut writer: W) -> Result { + 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)); + } +} diff --git a/node/src/lib.rs b/node/src/lib.rs new file mode 100644 index 00000000..caf2afcc --- /dev/null +++ b/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(()) +} diff --git a/node/src/logger.rs b/node/src/logger.rs new file mode 100644 index 00000000..8a60eecf --- /dev/null +++ b/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(()) +} diff --git a/node/src/main.rs b/node/src/main.rs new file mode 100644 index 00000000..c3ea5d54 --- /dev/null +++ b/node/src/main.rs @@ -0,0 +1,3 @@ +fn main() -> anyhow::Result<()> { + radicle_node::run() +} diff --git a/node/src/protocol.rs b/node/src/protocol.rs new file mode 100644 index 00000000..ad170bf2 --- /dev/null +++ b/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>; + +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 }, + /// Get a peer's inventory. + GetInventory { ids: Vec }, + /// 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 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(ctx: &mut Context) -> Result + 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>) -> Self { + Self::GetInventory { ids: ids.into() } + } +} + +/// Project tracking policy. +#[derive(Debug)] +pub enum ProjectTracking { + /// Track all projects we come across. + All { blocked: HashSet }, + /// Track a static list of projects. + Allowed(HashSet), +} + +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 }, + /// Track a specific list of users as well as the project delegates. + Allowed(HashSet), +} + +/// Protocol configuration. +#[derive(Debug, Default)] +pub struct Config { + /// Peers to connect to on startup. + /// Connections to these peers will be maintained. + pub connect: Vec, + /// 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 { + /// Peers currently or recently connected. + peers: Peers, + /// Protocol state that isn't peer-specific. + context: Context, + /// 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 Protocol { + 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 + '_> { + 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, 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 { + todo!() + } + + /// Get the connected peers. + pub fn peers(&self) -> &Peers { + &self.peers + } + + /// Get the current inventory. + pub fn inventory(&self) -> Result { + 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> { + &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 nakamoto::Protocol for Protocol +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, + ) { + 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 Deref for Protocol { + type Target = Context; + + fn deref(&self) -> &Self::Target { + &self.context + } +} + +impl DerefMut for Protocol { + 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 for nakamoto_net::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 Iterator for Protocol { + type Item = Io<(), DisconnectReason>; + + fn next(&mut self) -> Option { + 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, + /// A list of remote peers on which the project is known to exist. + pub remote: Vec, +} + +/// Global protocol state used across peers. +#[derive(Debug)] +pub struct Context { + /// Protocol configuration. + config: Config, + /// Tracks the location of projects. + routing: Routing, + /// Outgoing I/O queue. + io: VecDeque>, + /// Clock. Tells the time. + clock: RefClock, + /// Sequence number used for inventory messages. + seq: u64, + /// Project storage. + storage: T, + /// Peer address manager. + addrmgr: AddressManager, + /// Source of entropy. + rng: Rng, +} + +impl Context +where + T: storage::ReadStorage, +{ + fn new( + config: Config, + clock: RefClock, + storage: T, + addrmgr: AddressManager, + 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 Context { + /// 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) { + 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); + +impl Peers { + pub fn new(rng: Rng) -> Self { + Self(AddressBook::new(rng)) + } + + /// Iterator over fully negotiated peers. + pub fn negotiated(&self) -> impl Iterator + Clone { + self.0.iter().filter(move |(_, p)| p.is_negotiated()) + } +} + +impl Deref for Peers { + type Target = AddressBook; + + 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(&mut self, envelope: Envelope, ctx: &mut Context) + 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!(); + } + } + } +} diff --git a/node/src/storage.rs b/node/src/storage.rs new file mode 100644 index 00000000..a2223e6f --- /dev/null +++ b/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 = + 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, +} + +impl From> for Refs { + fn from(heads: HashMap) -> 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(HashMap>); + +/// A project remote. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +pub struct Remote { + /// Git references published under this remote, and their hashes. + refs: HashMap, + /// 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, +} + +pub trait ReadStorage { + fn get(&self, proj: &ProjId) -> Result, Error>; + fn inventory(&self) -> Result; +} + +pub trait WriteStorage { + /// Fetch a project from a remote peer. + fn fetch(&mut self, proj: &ProjId, remote: &net::SocketAddr) -> Result<(), Error>; +} + +impl ReadStorage for T +where + T: Deref, + S: ReadStorage, +{ + fn inventory(&self) -> Result { + self.deref().inventory() + } + + fn get(&self, proj: &ProjId) -> Result, Error> { + self.deref().get(proj) + } +} + +impl WriteStorage for T +where + T: DerefMut, + 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, Error> { + todo!() + } + + fn inventory(&self) -> Result { + 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>(path: P) -> Result { + 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, + ) -> 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() {} +} diff --git a/node/src/test.rs b/node/src/test.rs new file mode 100644 index 00000000..f60b8250 --- /dev/null +++ b/node/src/test.rs @@ -0,0 +1,6 @@ +mod arbitrary; +mod assert; +mod logger; +mod peer; +mod storage; +mod tests; diff --git a/node/src/test/arbitrary.rs b/node/src/test/arbitrary.rs new file mode 100644 index 00000000..07e49348 --- /dev/null +++ b/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 = 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) + } +} diff --git a/node/src/test/assert.rs b/node/src/test/assert.rs new file mode 100644 index 00000000..4f6ed872 --- /dev/null +++ b/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) -> String + where + F: FnOnce() + UnwindSafe, + { + let err = catch_unwind(f).expect_err("function did not panic"); + + *err.downcast::() + .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"# + ); + } +} diff --git a/node/src/test/logger.rs b/node/src/test/logger.rs new file mode 100644 index 00000000..4dee6143 --- /dev/null +++ b/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()); +} diff --git a/node/src/test/peer.rs b/node/src/test/peer.rs new file mode 100644 index 00000000..92b15db4 --- /dev/null +++ b/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 = crate::protocol::Protocol, S>; + +#[derive(Debug)] +pub struct Peer { + pub name: &'static str, + pub protocol: Protocol, + pub ip: net::IpAddr, + pub rng: fastrand::Rng, + pub local_time: LocalTime, + pub local_addr: net::SocketAddr, + + initialized: bool, +} + +impl simulator::Peer> for Peer +where + S: ReadStorage + WriteStorage + 'static, +{ + fn init(&mut self) { + self.initialize() + } + + fn addr(&self) -> net::SocketAddr { + net::SocketAddr::new(self.ip, DEFAULT_PORT) + } +} + +impl Deref for Peer { + type Target = Protocol; + + fn deref(&self) -> &Self::Target { + &self.protocol + } +} + +impl DerefMut for Peer { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.protocol + } +} + +impl Peer +where + S: ReadStorage + WriteStorage, +{ + pub fn new(name: &'static str, ip: impl Into, 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, + 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 { + let mut stream = Decoder::::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> + '_ { + self.protocol.outbox().drain(..) + } +} diff --git a/node/src/test/storage.rs b/node/src/test/storage.rs new file mode 100644 index 00000000..e1254183 --- /dev/null +++ b/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, Error> { + if let Some((_, refs)) = self.inventory.iter().find(|(id, _)| id == proj) { + return Ok(Some(refs.clone())); + } + Ok(None) + } + + fn inventory(&self) -> Result { + Ok(self.inventory.clone()) + } +} + +impl WriteStorage for MockStorage { + fn fetch(&mut self, _proj: &ProjId, _remote: &net::SocketAddr) -> Result<(), Error> { + todo!() + } +} diff --git a/node/src/test/tests.rs b/node/src/test/tests.rs new file mode 100644 index 00000000..78927579 --- /dev/null +++ b/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::>(); + + 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::>(); + + 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::>(); + 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::>(), + remotes, + "The remotes match the global routing table" + ); + } + } + } +}