diff --git a/Cargo.lock b/Cargo.lock index b8d31f952..3347dc7b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,7 +43,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", "once_cell", "version_check", ] @@ -59,9 +59,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "arrayref" @@ -121,9 +121,9 @@ dependencies = [ [[package]] name = "async-global-executor" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da5b41ee986eed3f524c380e6d64965aea573882a8907682ad100f7859305ca" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" dependencies = [ "async-channel", "async-executor", @@ -136,16 +136,16 @@ dependencies = [ [[package]] name = "async-io" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7" +checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7" dependencies = [ + "async-lock", "autocfg", "concurrent-queue", "futures-lite", "libc", "log", - "once_cell", "parking", "polling", "slab", @@ -156,11 +156,12 @@ dependencies = [ [[package]] name = "async-lock" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" dependencies = [ "event-listener", + "futures-lite", ] [[package]] @@ -294,9 +295,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" +checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43" dependencies = [ "async-trait", "axum-core", @@ -327,9 +328,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b" +checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" dependencies = [ "async-trait", "bytes", @@ -380,9 +381,9 @@ checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64ct" @@ -522,9 +523,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" [[package]] name = "cfg-if" @@ -582,9 +583,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.17" +version = "4.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06badb543e734a2d6568e19a40af66ed5364360b9226184926f89d229b4b4267" +checksum = "335867764ed2de42325fafe6d18b8af74ba97ee0c590fa016f157535b42ab04b" dependencies = [ "atty", "bitflags", @@ -597,9 +598,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.13" +version = "4.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f169caba89a7d512b5418b09864543eeb4d497416c917d7137863bd2076ad" +checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" dependencies = [ "heck", "proc-macro-error", @@ -919,16 +920,16 @@ dependencies = [ [[package]] name = "ed25519-zebra" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403ef3e961ab98f0ba902771d29f842058578bb1ce7e3c59dad5a6a93e784c69" +checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ "curve25519-dalek 3.2.0", + "hashbrown", "hex", "rand_core 0.6.4", "serde", "sha2 0.9.9", - "thiserror", "zeroize", ] @@ -1043,9 +1044,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" dependencies = [ "futures-channel", "futures-core", @@ -1058,9 +1059,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" dependencies = [ "futures-core", "futures-sink", @@ -1068,15 +1069,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-executor" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" dependencies = [ "futures-core", "futures-task", @@ -1086,9 +1087,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" [[package]] name = "futures-lite" @@ -1107,9 +1108,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", @@ -1118,15 +1119,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" [[package]] name = "futures-task" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" [[package]] name = "futures-timer" @@ -1136,9 +1137,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ "futures-channel", "futures-core", @@ -1238,9 +1239,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "js-sys", @@ -1297,9 +1298,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" dependencies = [ "bytes", "fnv", @@ -1446,9 +1447,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.20" +version = "0.14.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +checksum = "abfba89e19b959ca163c7752ba59d737c1ceea53a5d31a149c805446fc958064" dependencies = [ "bytes", "futures-channel", @@ -1643,9 +1644,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.135" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "libipld-cbor" @@ -1700,7 +1701,7 @@ dependencies = [ "bytes", "futures", "futures-timer", - "getrandom 0.2.7", + "getrandom 0.2.8", "instant", "lazy_static", "libp2p-core", @@ -2076,14 +2077,14 @@ checksum = "e6c5f1f52e39b728e73af4b454f1b29173d4544607bd395dafe1918fd149db67" [[package]] name = "mio" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.36.1", + "windows-sys", ] [[package]] @@ -2377,7 +2378,7 @@ dependencies = [ "fastcdc", "futures", "fvm_ipld_amt", - "getrandom 0.2.7", + "getrandom 0.2.8", "libipld-cbor", "libipld-core", "log", @@ -2455,14 +2456,20 @@ name = "noosphere-ns" version = "0.1.0-alpha.1" dependencies = [ "anyhow", + "cid", "futures", + "libipld-cbor", "libp2p", "noosphere-core", + "noosphere-storage", "rand 0.8.5", + "serde", + "serde_json", "test-log", "tokio", "tracing", "tracing-subscriber", + "ucan", "ucan-key-support", ] @@ -2610,9 +2617,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ "hermit-abi", "libc", @@ -2620,9 +2627,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "opaque-debug" @@ -2632,9 +2639,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "os_str_bytes" -version = "6.3.0" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" [[package]] name = "overload" @@ -2693,7 +2700,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] @@ -2816,9 +2823,9 @@ dependencies = [ [[package]] name = "polling" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011" +checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2" dependencies = [ "autocfg", "cfg-if", @@ -3082,7 +3089,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", ] [[package]] @@ -3359,9 +3366,9 @@ dependencies = [ [[package]] name = "scoped-tls" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" @@ -3402,9 +3409,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] @@ -3429,9 +3436,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", @@ -3452,9 +3459,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" +checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" dependencies = [ "itoa", "ryu", @@ -3769,9 +3776,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" dependencies = [ "proc-macro2", "quote", @@ -4215,7 +4222,7 @@ dependencies = [ "base64", "bs58", "cid", - "getrandom 0.2.7", + "getrandom 0.2.8", "instant", "libipld-core", "libipld-json", @@ -4604,19 +4611,6 @@ dependencies = [ "windows_x86_64_msvc 0.34.0", ] -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", -] - [[package]] name = "windows-sys" version = "0.42.0" @@ -4644,12 +4638,6 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - [[package]] name = "windows_aarch64_msvc" version = "0.42.0" @@ -4662,12 +4650,6 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - [[package]] name = "windows_i686_gnu" version = "0.42.0" @@ -4680,12 +4662,6 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - [[package]] name = "windows_i686_msvc" version = "0.42.0" @@ -4698,12 +4674,6 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - [[package]] name = "windows_x86_64_gnu" version = "0.42.0" @@ -4722,12 +4692,6 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - [[package]] name = "windows_x86_64_msvc" version = "0.42.0" diff --git a/rust/noosphere-core/src/authority/capability.rs b/rust/noosphere-core/src/authority/capability.rs index 576dbef1a..91f599023 100644 --- a/rust/noosphere-core/src/authority/capability.rs +++ b/rust/noosphere-core/src/authority/capability.rs @@ -42,7 +42,7 @@ impl TryFrom for SphereAction { } } -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SphereReference { pub did: String, } diff --git a/rust/noosphere-core/src/data/authority.rs b/rust/noosphere-core/src/data/authority.rs index fa4b0be55..f697ee607 100644 --- a/rust/noosphere-core/src/data/authority.rs +++ b/rust/noosphere-core/src/data/authority.rs @@ -59,7 +59,7 @@ impl DelegationIpld { } } -/// See https://github.com/ucan-wg/spec#66-revocation +/// See /// TODO(ucan-wg/spec#112): Verify the form of this #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Hash)] pub struct RevocationIpld { diff --git a/rust/noosphere-ns/Cargo.toml b/rust/noosphere-ns/Cargo.toml index d7ab99fe9..21f47bb55 100644 --- a/rust/noosphere-ns/Cargo.toml +++ b/rust/noosphere-ns/Cargo.toml @@ -22,17 +22,23 @@ homepage = "https://github.com/subconsciousnetwork/noosphere" readme = "README.md" [dependencies] -anyhow = "^1" -tracing = "0.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -noosphere-core = { version = "0.1.0-alpha.1", path = "../noosphere-core" } -ucan-key-support = { version = "0.7.0-alpha.1" } +anyhow = "^1" +tracing = "0.1" +cid = "~0.8" +serde = "^1" +serde_json = "^1" futures = "0.3.1" +ucan = { version = "0.7.0-alpha.1" } +ucan-key-support = { version = "0.7.0-alpha.1" } tokio = { version = "1.15", features = ["io-util", "io-std", "sync", "macros", "rt", "rt-multi-thread"] } -tracing-subscriber = { version = "~0.3", features = ["env-filter"] } libp2p = { version = "0.49.0", default-features = false, features = [ "identify", "dns", "tcp", "tokio", "noise", "mplex", "yamux", "kad" ] } +noosphere-storage = { version = "0.1.0-alpha.1", path = "../noosphere-storage" } +noosphere-core = { version = "0.1.0-alpha.1", path = "../noosphere-core" } [dev-dependencies] rand = { version = "0.8.5" } test-log = { version = "0.2.11", default-features = false, features = ["trace"] } +tracing-subscriber = { version = "~0.3", features = ["env-filter"] } +libipld-cbor = "~0.14" diff --git a/rust/noosphere-ns/src/builder.rs b/rust/noosphere-ns/src/builder.rs new file mode 100644 index 000000000..58f5dc962 --- /dev/null +++ b/rust/noosphere-ns/src/builder.rs @@ -0,0 +1,202 @@ +use crate::{dht::DHTConfig, name_system::NameSystem}; +use anyhow::{anyhow, Result}; +use libp2p::{self, Multiaddr}; +use noosphere_storage::{db::SphereDb, interface::Store}; +use std::net::Ipv4Addr; +use ucan_key_support::ed25519::Ed25519KeyMaterial; + +/// [NameSystemBuilder] is the primary external interface for +/// creating a new [NameSystem]. `key_material` and `store` +/// must be provided. +/// +/// # Examples +/// +/// ``` +/// use noosphere_core::authority::generate_ed25519_key; +/// use noosphere_storage::{db::SphereDb, memory::{MemoryStore, MemoryStorageProvider}}; +/// use noosphere_ns::{NameSystem, NameSystemBuilder}; +/// use tokio; +/// +/// #[tokio::main] +/// async fn main() { +/// let key_material = generate_ed25519_key(); +/// let store = SphereDb::new(&MemoryStorageProvider::default()).await.unwrap(); +/// +/// let ns = NameSystemBuilder::default() +/// .key_material(&key_material) +/// .store(&store) +/// .listening_port(30000) +/// .build().expect("valid config"); +/// +/// assert!(NameSystemBuilder::::default().build().is_err(), "key_material and store must be provided."); +/// } +/// ``` +pub struct NameSystemBuilder +where + S: Store, +{ + bootstrap_peers: Option>, + dht_config: DHTConfig, + key_material: Option, + store: Option>, + propagation_interval: u64, +} + +impl NameSystemBuilder +where + S: Store, +{ + /// If bootstrap peers are provided, how often, + /// in seconds, should the bootstrap process execute + /// to keep routing tables fresh. + pub fn bootstrap_interval(mut self, interval: u64) -> Self { + self.dht_config.bootstrap_interval = interval; + self + } + + /// Peer addresses to query to update routing tables + /// during bootstrap. A standalone bootstrap node would + /// have this field empty. + pub fn bootstrap_peers(mut self, peers: &Vec) -> Self { + self.bootstrap_peers = Some(peers.to_owned()); + self + } + + /// Public/private keypair for DHT node. + pub fn key_material(mut self, key_material: &Ed25519KeyMaterial) -> Self { + self.key_material = Some(key_material.to_owned()); + self + } + + /// Port to listen for incoming TCP connections. If not specified, + /// an open port is automatically chosen. + pub fn listening_port(mut self, port: u16) -> Self { + let mut address = Multiaddr::empty(); + address.push(libp2p::multiaddr::Protocol::Ip4(Ipv4Addr::new( + 127, 0, 0, 1, + ))); + address.push(libp2p::multiaddr::Protocol::Tcp(port)); + self.dht_config.listening_address = Some(address); + self + } + + /// How frequently, in seconds, the DHT attempts to + /// dial peers found in its kbucket. Outside of tests, + /// should not be lower than 5 seconds. + pub fn peer_dialing_interval(mut self, interval: u64) -> Self { + self.dht_config.peer_dialing_interval = interval; + self + } + + /// How long, in seconds, until a network query times out. + pub fn query_timeout(mut self, timeout: u32) -> Self { + self.dht_config.query_timeout = timeout; + self + } + + /// The Noosphere Store to use for reading and writing sphere data. + pub fn store(mut self, store: &SphereDb) -> Self { + self.store = Some(store.to_owned()); + self + } + + /// Default interval for hosted records to be propagated to the network. + pub fn propagation_interval(mut self, propagation_interval: u64) -> Self { + self.propagation_interval = propagation_interval; + self + } + + /// Build a [NameSystem] based off of the provided configuration. + pub fn build(mut self) -> Result> { + let key_material = self + .key_material + .take() + .ok_or_else(|| anyhow!("key_material required."))?; + let store = self + .store + .take() + .ok_or_else(|| anyhow!("store required."))?; + Ok(NameSystem::new( + key_material, + store, + self.bootstrap_peers.take(), + self.dht_config, + self.propagation_interval, + )) + } +} + +impl Default for NameSystemBuilder +where + S: Store, +{ + fn default() -> Self { + Self { + bootstrap_peers: None, + dht_config: DHTConfig::default(), + key_material: None, + store: None, + propagation_interval: 60 * 60 * 24, // 1 day + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use noosphere_core::authority::generate_ed25519_key; + use noosphere_storage::{ + db::SphereDb, + memory::{MemoryStorageProvider, MemoryStore}, + }; + + #[tokio::test] + async fn test_name_system_builder() -> Result<(), anyhow::Error> { + let key_material = generate_ed25519_key(); + let store = SphereDb::new(&MemoryStorageProvider::default()) + .await + .unwrap(); + let bootstrap_peers: Vec = vec![ + "/ip4/127.0.0.50/tcp/33333/p2p/12D3KooWH8WgH9mgbMXrKX4veokUznvEn6Ycwg4qaGNi83nLkoUK" + .parse()?, + "/ip4/127.0.0.50/tcp/33334/p2p/12D3KooWMWo6tNGRx1G4TNqvr4SnHyVXSReC3tdX6zoJothXxV2c" + .parse()?, + ]; + + let ns = NameSystemBuilder::default() + .listening_port(30000) + .key_material(&key_material) + .store(&store) + .bootstrap_peers(&bootstrap_peers) + .bootstrap_interval(33) + .peer_dialing_interval(11) + .query_timeout(22) + .propagation_interval(3600) + .build()?; + + assert_eq!(ns.key_material.0.as_ref(), key_material.0.as_ref()); + assert_eq!(ns._propagation_interval, 3600); + assert_eq!(ns.bootstrap_peers.as_ref().unwrap().len(), 2); + assert_eq!(ns.bootstrap_peers.as_ref().unwrap()[0], bootstrap_peers[0],); + assert_eq!(ns.bootstrap_peers.as_ref().unwrap()[1], bootstrap_peers[1]); + assert_eq!( + ns.dht_config.listening_address.as_ref().unwrap(), + &"/ip4/127.0.0.1/tcp/30000".parse()? + ); + assert_eq!(ns.dht_config.bootstrap_interval, 33); + assert_eq!(ns.dht_config.peer_dialing_interval, 11); + assert_eq!(ns.dht_config.query_timeout, 22); + + if NameSystemBuilder::default().store(&store).build().is_ok() { + panic!("key_material required."); + } + if NameSystemBuilder::::default() + .key_material(&key_material) + .build() + .is_ok() + { + panic!("store required."); + } + Ok(()) + } +} diff --git a/rust/noosphere-ns/src/dht/channel.rs b/rust/noosphere-ns/src/dht/channel.rs index 05e1c516e..df12ce288 100644 --- a/rust/noosphere-ns/src/dht/channel.rs +++ b/rust/noosphere-ns/src/dht/channel.rs @@ -2,7 +2,6 @@ use core::{fmt, result::Result}; use tokio; use tokio::sync::{mpsc, mpsc::error::SendError, oneshot, oneshot::error::RecvError}; - impl std::error::Error for ChannelError {} impl fmt::Display for ChannelError { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -77,6 +76,7 @@ impl MessageClient { rx.await.map_err(|e| e.into()) } + #[allow(clippy::type_complexity)] fn send_request_impl( &self, request: Q, diff --git a/rust/noosphere-ns/src/dht/config.rs b/rust/noosphere-ns/src/dht/config.rs index a59484abc..6a7daef9f 100644 --- a/rust/noosphere-ns/src/dht/config.rs +++ b/rust/noosphere-ns/src/dht/config.rs @@ -1,9 +1,15 @@ +use libp2p::Multiaddr; + #[derive(Clone, Debug)] pub struct DHTConfig { /// If bootstrap peers are provided, how often, /// in seconds, should the bootstrap process execute /// to keep routing tables fresh. pub bootstrap_interval: u64, + /// The local network interface and TCP port to listen + /// for incoming DHT connections. If `None`, can run + /// a limited set of queries on the network. + pub listening_address: Option, /// How frequently, in seconds, the DHT attempts to /// dial peers found in its kbucket. Outside of tests, /// should not be lower than 5 seconds. @@ -18,6 +24,7 @@ impl Default for DHTConfig { fn default() -> Self { Self { bootstrap_interval: 5 * 60, + listening_address: None, peer_dialing_interval: 5, query_timeout: 5 * 60, } diff --git a/rust/noosphere-ns/src/dht/node.rs b/rust/noosphere-ns/src/dht/node.rs index e28139ceb..173274b50 100644 --- a/rust/noosphere-ns/src/dht/node.rs +++ b/rust/noosphere-ns/src/dht/node.rs @@ -6,7 +6,8 @@ use crate::dht::{ utils::key_material_to_libp2p_keypair, DHTConfig, }; -use std::{net::SocketAddr, time::Duration}; +use libp2p; +use std::time::Duration; use tokio; use ucan_key_support::ed25519::Ed25519KeyMaterial; @@ -36,43 +37,33 @@ pub struct DHTNode { thread_handle: Option>>, keypair: libp2p::identity::Keypair, peer_id: libp2p::PeerId, - p2p_address: libp2p::Multiaddr, + p2p_address: Option, bootstrap_peers: Option>, } impl DHTNode { /// Creates a new [DHTNode]. - /// `listening_address` is a [std::net::SocketAddr] that is used to listen for incoming - /// connections. /// `bootstrap_peers` is a collection of [String]s in [libp2p::Multiaddr] form of initial /// peers to connect to during bootstrapping. This collection would be empty in the /// standalone bootstrap node scenario. + /// `config` is a [DHTConfig] of various configurations for the node. pub fn new( key_material: &Ed25519KeyMaterial, - listening_address: &SocketAddr, - bootstrap_peers: Option<&Vec>, + bootstrap_peers: Option<&Vec>, config: &DHTConfig, ) -> Result { let keypair = key_material_to_libp2p_keypair(key_material)?; let peer_id = libp2p::PeerId::from(keypair.public()); - let p2p_address = { - let mut multiaddr: libp2p::Multiaddr = listening_address.ip().into(); - multiaddr.push(libp2p::multiaddr::Protocol::Tcp(listening_address.port())); - multiaddr.push(libp2p::multiaddr::Protocol::P2p(peer_id.into())); - multiaddr - }; - - let peers: Option> = if let Some(peers) = bootstrap_peers { - Some( - peers - .iter() - .map(|p| p.parse()) - .collect::, libp2p::multiaddr::Error>>() - .map_err(|e| DHTError::Error(e.to_string()))?, - ) - } else { - None - }; + let peers: Option> = bootstrap_peers.map(|peers| peers.to_vec()); + + let p2p_address: Option = + if let Some(listening_address) = config.listening_address.as_ref() { + let mut p2p_address = listening_address.to_owned(); + p2p_address.push(libp2p::multiaddr::Protocol::P2p(peer_id.into())); + Some(p2p_address) + } else { + None + }; Ok(DHTNode { keypair, @@ -114,13 +105,9 @@ impl DHTNode { } /// Adds additional bootstrap peers. Can only be executed before calling [DHTNode::run]. - pub fn add_peers(&mut self, new_peers: &Vec) -> Result<(), DHTError> { + pub fn add_peers(&mut self, new_peers: &[libp2p::Multiaddr]) -> Result<(), DHTError> { self.ensure_state(DHTStatus::Initialized)?; - let mut new_peers_list: Vec = new_peers - .iter() - .map(|p| p.parse()) - .collect::, libp2p::multiaddr::Error>>() - .map_err(|e| DHTError::Error(e.to_string()))?; + let mut new_peers_list: Vec = new_peers.to_vec(); if let Some(ref mut peers) = self.bootstrap_peers { peers.append(&mut new_peers_list); @@ -142,8 +129,8 @@ impl DHTNode { } /// Returns the listening address of this node. - pub fn p2p_address(&self) -> &libp2p::Multiaddr { - &self.p2p_address + pub fn p2p_address(&self) -> Option<&libp2p::Multiaddr> { + self.p2p_address.as_ref() } pub fn status(&self) -> DHTStatus { @@ -199,10 +186,10 @@ impl DHTNode { /// Return value may be `Ok(None)` if query finished without finding /// any matching values. /// Fails if node is not in an active state. - pub async fn get_record(&self, name: Vec) -> Result>, DHTError> { + pub async fn get_record(&self, name: Vec) -> Result<(Vec, Option>), DHTError> { let request = DHTRequest::GetRecord { name }; let response = self.send_request(request).await?; - ensure_response!(response, DHTResponse::GetRecord { value, .. } => Ok(Some(value))) + ensure_response!(response, DHTResponse::GetRecord { name, value, .. } => Ok((name, value))) } /// Instructs the node to tell its peers that it is providing diff --git a/rust/noosphere-ns/src/dht/processor.rs b/rust/noosphere-ns/src/dht/processor.rs index 9e30627ff..c4c2db605 100644 --- a/rust/noosphere-ns/src/dht/processor.rs +++ b/rust/noosphere-ns/src/dht/processor.rs @@ -27,8 +27,8 @@ use tokio; /// should only interface with a [DHTProcessor] via [DHTNode]. pub struct DHTProcessor { config: DHTConfig, - p2p_address: libp2p::Multiaddr, peer_id: PeerId, + p2p_address: Option, processor: DHTMessageProcessor, swarm: DHTSwarm, requests: HashMap, @@ -59,7 +59,7 @@ impl DHTProcessor { pub(crate) fn spawn( keypair: &libp2p::identity::Keypair, peer_id: &PeerId, - p2p_address: &libp2p::Multiaddr, + p2p_address: &Option, bootstrap_peers: &Option>, config: &DHTConfig, processor: DHTMessageProcessor, @@ -136,7 +136,7 @@ impl DHTProcessor { _ = bootstrap_tick.tick() => self.execute_bootstrap()?, _ = peer_dialing_tick.tick() => self.dial_next_peer(), } - }; + } Ok(()) } @@ -169,9 +169,7 @@ impl DHTProcessor { } */ DHTRequest::Bootstrap => { - message.respond( - self.execute_bootstrap().map(|_| DHTResponse::Success), - ); + message.respond(self.execute_bootstrap().map(|_| DHTResponse::Success)); } DHTRequest::GetNetworkInfo => { let info = self.swarm.network_info(); @@ -263,14 +261,24 @@ impl DHTProcessor { if let Some(message) = self.requests.remove(&id) { message.respond(Ok(DHTResponse::GetRecord { name: key.to_vec(), - value, + value: Some(value), })); } } } QueryResult::GetRecord(Err(e)) => { if let Some(message) = self.requests.remove(&id) { - message.respond(Err(DHTError::from(e))); + match e { + kad::GetRecordError::NotFound { key, .. } => { + // Not finding a record is not an `Err` response, + // but simply a successful query with a `None` result. + message.respond(Ok(DHTResponse::GetRecord { + name: key.to_vec(), + value: None, + })) + } + e => message.respond(Err(DHTError::from(e))), + }; } } QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { @@ -350,9 +358,12 @@ impl DHTProcessor { } => {} kad::InboundRequest::PutRecord { source, record, .. } => match record { Some(rec) => { - if self.swarm.behaviour_mut().kad.store_mut().put(rec.clone()).is_err() + if let Err(e) = self.swarm.behaviour_mut().kad.store_mut().put(rec.clone()) { - warn!("InboundRequest::PutRecord failed: {:?} {:?}", rec, source); + warn!( + "InboundRequest::PutRecord failed: {:?} {:?}, {}", + rec, source, e + ); } } None => warn!("InboundRequest::PutRecord failed; empty record"), @@ -377,22 +388,19 @@ impl DHTProcessor { } fn process_identify_event(&mut self, event: IdentifyEvent) { - match event { - IdentifyEvent::Received { peer_id, info } => { - if info - .protocols - .iter() - .any(|p| p.as_bytes() == kad::protocol::DEFAULT_PROTO_NAME) - { - for addr in &info.listen_addrs { - self.swarm - .behaviour_mut() - .kad - .add_address(&peer_id, addr.clone()); - } + if let IdentifyEvent::Received { peer_id, info } = event { + if info + .protocols + .iter() + .any(|p| p.as_bytes() == kad::protocol::DEFAULT_PROTO_NAME) + { + for addr in &info.listen_addrs { + self.swarm + .behaviour_mut() + .kad + .add_address(&peer_id, addr.clone()); } } - _ => {} } } @@ -433,11 +441,17 @@ impl DHTProcessor { } fn start_listening(&mut self) -> Result<(), DHTError> { - let addr = self.p2p_address.clone(); - dht_event_trace(self, &format!("Start listening on {}", addr)); - self.swarm - .listen_on(addr).map(|_| ()) - .map_err(DHTError::from) + match self.p2p_address.as_ref() { + Some(p2p_address) => { + let addr = p2p_address.to_owned(); + dht_event_trace(self, &format!("Start listening on {}", addr)); + self.swarm + .listen_on(addr) + .map(|_| ()) + .map_err(DHTError::from) + } + None => Ok(()), + } } fn execute_bootstrap(&mut self) -> Result<(), DHTError> { @@ -456,7 +470,6 @@ impl fmt::Debug for DHTProcessor { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("DHTNode") .field("peer_id", &self.peer_id) - .field("p2p_address", &self.p2p_address) .field("config", &self.config) .finish() } @@ -476,9 +489,7 @@ fn dht_event_trace(processor: &DHTProcessor, data: &T) { let peer_id_b58 = processor.peer_id.to_base58(); trace!( "\nFrom ..{:#?}..\n{:#?}", - peer_id_b58 - .get(8..14) - .unwrap_or("INVALID PEER ID"), + peer_id_b58.get(8..14).unwrap_or("INVALID PEER ID"), data ); } diff --git a/rust/noosphere-ns/src/dht/swarm.rs b/rust/noosphere-ns/src/dht/swarm.rs index dfb7148fb..861446bcf 100644 --- a/rust/noosphere-ns/src/dht/swarm.rs +++ b/rust/noosphere-ns/src/dht/swarm.rs @@ -7,7 +7,7 @@ use libp2p::{ dns, identify::{Behaviour as Identify, Config as IdentifyConfig, Event as IdentifyEvent}, identity::Keypair, - kad::{self, Kademlia, KademliaConfig, KademliaEvent}, + kad::{self, Kademlia, KademliaConfig, KademliaEvent, KademliaStoreInserts}, mplex, noise, swarm::SwarmBuilder, swarm::{self, ConnectionHandler, IntoConnectionHandler, SwarmEvent}, @@ -52,6 +52,11 @@ impl DHTBehaviour { let kad = { let mut cfg = KademliaConfig::default(); cfg.set_query_timeout(Duration::from_secs(config.query_timeout.into())); + // By default, all records from peers are automatically stored. + // `FilterBoth` means it's the Kademlia behaviour handler's responsibility + // to determine whether or not Provider records and KV records ("both") get stored, + // where we implement logic to validate/prune incoming records. + cfg.set_record_filtering(KademliaStoreInserts::FilterBoth); // TODO(#99): Use SphereFS storage let store = kad::record::store::MemoryStore::new(local_peer_id.to_owned()); @@ -60,7 +65,7 @@ impl DHTBehaviour { let identify = { let config = IdentifyConfig::new("ipfs/1.0.0".into(), keypair.public()) - .with_agent_version(format!("noosphere-p2p/{}", env!("CARGO_PKG_VERSION"))); + .with_agent_version(format!("noosphere-ns/{}", env!("CARGO_PKG_VERSION"))); Identify::new(config) }; diff --git a/rust/noosphere-ns/src/dht/types.rs b/rust/noosphere-ns/src/dht/types.rs index 483c85d9a..81ac17917 100644 --- a/rust/noosphere-ns/src/dht/types.rs +++ b/rust/noosphere-ns/src/dht/types.rs @@ -71,7 +71,7 @@ pub enum DHTResponse { GetNetworkInfo(DHTNetworkInfo), GetRecord { name: Vec, - value: Vec, + value: Option>, }, SetRecord { name: Vec, @@ -96,7 +96,11 @@ impl fmt::Display for DHTResponse { fmt, "DHTResponse::GetRecord {{ name={:?}, value={:?} }}", str::from_utf8(name), - str::from_utf8(value) + if value.is_some() { + str::from_utf8(value.as_ref().unwrap()) + } else { + Ok("None") + } ), DHTResponse::SetRecord { name } => write!( fmt, diff --git a/rust/noosphere-ns/src/lib.rs b/rust/noosphere-ns/src/lib.rs index 3d81109a3..cdd589c04 100644 --- a/rust/noosphere-ns/src/lib.rs +++ b/rust/noosphere-ns/src/lib.rs @@ -1,5 +1,15 @@ +#![cfg(not(target_arch = "wasm32"))] + #[macro_use] extern crate tracing; -#[cfg(not(target_arch = "wasm32"))] +mod builder; pub mod dht; +mod name_system; +mod records; +pub mod utils; + +pub use builder::NameSystemBuilder; +pub use libp2p::multiaddr; +pub use name_system::NameSystem; +pub use records::NSRecord; diff --git a/rust/noosphere-ns/src/name_system.rs b/rust/noosphere-ns/src/name_system.rs new file mode 100644 index 000000000..59b8331ab --- /dev/null +++ b/rust/noosphere-ns/src/name_system.rs @@ -0,0 +1,256 @@ +use crate::{ + dht::{DHTConfig, DHTNode}, + records::NSRecord, +}; +use anyhow::{anyhow, Result}; +use futures::future::try_join_all; +use libp2p::Multiaddr; +use noosphere_core::authority::SUPPORTED_KEYS; +use noosphere_storage::{db::SphereDb, interface::Store}; +use std::collections::HashMap; +use ucan::crypto::did::DidParser; +use ucan_key_support::ed25519::Ed25519KeyMaterial; + +/// The [NameSystem] is responsible for both propagating and resolving Sphere DIDs +/// into an authorized UCAN publish token, resolving into a [cid::Cid] address for +/// a sphere's content. These records are propagated and resolved via the +/// Noosphere NS distributed network, built on [libp2p](https://libp2p.io)'s +/// [Kademlia DHT specification](https://github.com/libp2p/specs/blob/master/kad-dht/README.md). +/// +/// Hosted records can be set via [NameSystem::set_record], propagating the +/// record immediately, and repropagated every `propagation_interval` seconds. Records +/// can be resolved via [NameSystem::get_record]. +/// +/// New [NameSystem] instances can be created via [crate::NameSystemBuilder]. +pub struct NameSystem +where + S: Store, +{ + /// Bootstrap peers for the DHT network. + pub(crate) bootstrap_peers: Option>, + pub(crate) dht: Option, + pub(crate) dht_config: DHTConfig, + /// Key of the NameSystem's sphere. + pub(crate) key_material: Ed25519KeyMaterial, + /// In seconds, the interval that hosted records are propagated on the network. + pub(crate) _propagation_interval: u64, + pub(crate) store: SphereDb, + /// Map of sphere DIDs to [NSRecord] hosted/propagated by this name system. + hosted_records: HashMap, + /// Map of resolved sphere DIDs to resolved [NSRecord]. + resolved_records: HashMap, + /// Cached DidParser. + did_parser: DidParser, +} + +impl NameSystem +where + S: Store, +{ + /// Internal instantiation function invoked by [crate::NameSystemBuilder]. + pub(crate) fn new( + key_material: Ed25519KeyMaterial, + store: SphereDb, + bootstrap_peers: Option>, + dht_config: DHTConfig, + _propagation_interval: u64, + ) -> Self { + NameSystem { + key_material, + store, + bootstrap_peers, + dht_config, + _propagation_interval, + dht: None, + hosted_records: HashMap::new(), + resolved_records: HashMap::new(), + did_parser: DidParser::new(SUPPORTED_KEYS), + } + } + + /// Initializes and attempts to connect to the network. + pub async fn connect(&mut self) -> Result<()> { + let mut dht = DHTNode::new( + &self.key_material, + self.bootstrap_peers.as_ref(), + &self.dht_config, + )?; + dht.run().map_err(|e| anyhow!(e.to_string()))?; + dht.bootstrap().await.map_err(|e| anyhow!(e.to_string()))?; + dht.wait_for_peers(1) + .await + .map_err(|e| anyhow!(e.to_string()))?; + self.dht = Some(dht); + Ok(()) + } + + /// Disconnect and deallocate connections to the network. + pub fn disconnect(&mut self) -> Result<()> { + if let Some(mut dht) = self.dht.take() { + dht.terminate()?; + } + Ok(()) + } + + /// Propagates all hosted records on nearby peers in the DHT network. + /// Automatically called every `crate::NameSystemBuilder::propagation_interval` seconds (TBD), + /// but can be manually called to republish records to the network. + /// + /// Can fail if NameSystem is not connected or if no peers can be found. + pub async fn propagate_records(&self) -> Result<()> { + let _ = self.require_dht()?; + + if self.hosted_records.is_empty() { + return Ok(()); + } + + let pending_tasks: Vec<_> = self + .hosted_records + .iter() + .map(|(identity, record)| self.dht_set_record(identity, record)) + .collect(); + try_join_all(pending_tasks).await?; + Ok(()) + } + + /// Propagates the corresponding managed sphere's [NSRecord] on nearby peers + /// in the DHT network. + /// + /// Can fail if NameSystem is not connected or if no peers can be found. + pub async fn set_record(&mut self, mut record: NSRecord) -> Result<()> { + let _ = self.require_dht()?; + + record.validate(&self.store, &mut self.did_parser).await?; + let identity = record.identity(); + + self.dht_set_record(identity, &record).await?; + self.hosted_records.insert(identity.to_owned(), record); + Ok(()) + } + + /// Returns an [NSRecord] for the provided identity if found. + /// + /// Reads from local cache if a valid token is found; otherwise, + /// queries the network for a valid record. + /// + /// Can fail if network errors occur. + pub async fn get_record(&mut self, identity: &str) -> Result> { + if let Some(record) = self.resolved_records.get(identity) { + if !record.is_expired() { + return Ok(Some(record.clone())); + } else { + self.resolved_records.remove(identity); + } + } + // No non-expired record found locally, query the network. + match self.dht_get_record(identity).await? { + (_, Some(record)) => { + self.resolved_records + .insert(identity.to_owned(), record.clone()); + Ok(Some(record)) + } + (_, None) => Ok(None), + } + } + + /// Clears out the internal cache of resolved records. + pub fn flush_records(&mut self) { + self.resolved_records.drain(); + } + + /// Clears out the internal cache of resolved records + /// for the matched identity. Returned value indicates whether + /// a record was successfully removed. + pub fn flush_records_for_identity(&mut self, identity: &String) -> bool { + self.resolved_records.remove(identity).is_some() + } + + /// Access the record cache of the name system. + pub fn get_cache(&self) -> &HashMap { + &self.resolved_records + } + + /// Access the record cache as mutable of the name system. + pub fn get_cache_mut(&mut self) -> &mut HashMap { + &mut self.resolved_records + } + + pub fn p2p_address(&self) -> Option<&Multiaddr> { + if let Some(dht) = &self.dht { + dht.p2p_address() + } else { + None + } + } + + /// Queries the DHT for a record for the given sphere identity. + /// If no record is found, no error is returned. + /// + /// Returns an error if not connected to the DHT network. + async fn dht_get_record(&self, identity: &str) -> Result<(String, Option)> { + let dht = self.require_dht()?; + + match dht.get_record(identity.to_owned().into_bytes()).await { + Ok((_, result)) => match result { + Some(value) => { + // Validation/correctness and filtering through + // the most recent values can be performed here + let record = NSRecord::try_from(value)?; + info!( + "NameSystem: GetRecord: {} {}", + identity, + record + .address() + .map_or_else(|| String::from("None"), |cid| cid.to_string()) + ); + Ok((identity.to_owned(), Some(record))) + } + None => { + warn!("NameSystem: GetRecord: No record found for {}.", identity); + Ok((identity.to_owned(), None)) + } + }, + Err(e) => { + warn!("NameSystem: GetRecord: Failure for {} {:?}.", identity, e); + Err(anyhow!(e.to_string())) + } + } + } + + /// Propagates and serializes the record on peers in the DHT network. + /// + /// Can fail if record is invalid, NameSystem is not connected or + /// if no peers can be found. + async fn dht_set_record(&self, identity: &str, record: &NSRecord) -> Result<()> { + let dht = self.require_dht()?; + + match dht + .set_record(String::from(identity).into_bytes(), record.try_into()?) + .await + { + Ok(_) => { + info!("NameSystem: SetRecord: {}", identity); + Ok(()) + } + Err(e) => { + warn!("NameSystem: SetRecord: Failure for {} {:?}.", identity, e); + Err(anyhow!(e.to_string())) + } + } + } + + fn require_dht(&self) -> Result<&DHTNode> { + self.dht.as_ref().ok_or_else(|| anyhow!("not connected")) + } +} + +impl Drop for NameSystem +where + S: Store, +{ + fn drop(&mut self) { + if let Err(e) = self.disconnect() { + error!("{}", e.to_string()); + } + } +} diff --git a/rust/noosphere-ns/src/records.rs b/rust/noosphere-ns/src/records.rs new file mode 100644 index 000000000..8a4e13510 --- /dev/null +++ b/rust/noosphere-ns/src/records.rs @@ -0,0 +1,461 @@ +use crate::utils::generate_capability; +use anyhow::{anyhow, Error}; +use cid::Cid; +use noosphere_core::authority::SPHERE_SEMANTICS; +use noosphere_storage::{db::SphereDb, interface::Store}; +use serde_json::Value; +use std::{convert::TryFrom, str, str::FromStr}; +use ucan::{chain::ProofChain, crypto::did::DidParser, Ucan}; + +/// An [NSRecord] is the internal representation of a mapping from a +/// sphere's identity (DID key) to a sphere's revision as a +/// content address ([cid::Cid]). The record wraps a [ucan::Ucan] token, +/// providing de/serialization for transmitting in the NS network, +/// and validates data ensuring the sphere's owner authorized the publishing +/// of a new content address. +/// +/// When transmitting through the distributed NS network, the record is +/// represented as the base64 encoded Ucan token. +/// +/// # Ucan Semantics +/// +/// An [NSRecord] is a small interface over a [ucan::Ucan] token, +/// with the following semantics: +/// +/// ```json +/// { +/// // The identity (DID) of the Principal that signed the token +/// "iss": "did:key:z6MkoE19WHXJzpLqkxbGP7uXdJX38sWZNUWwyjcuCmjhPpUP", +/// // The identity (DID) of the sphere this record maps. +/// "aud": "did:key:z6MkkVfktAC5rVNRmmTjkKPapT3bAyVkYH8ZVCF1UBNUfazp", +/// // Attenuation must contain a capability with a resource "sphere:{AUD}" +/// // and action "sphere/publish". +/// "att": [{ +/// "with": "sphere:did:key:z6MkkVfktAC5rVNRmmTjkKPapT3bAyVkYH8ZVCF1UBNUfazp", +/// "can": "sphere/publish" +/// }], +/// // Additional Ucan proofs needed to validate. +/// "prf": [], +/// // Facts contain a single entry with an "address" field containing +/// // the content address of a sphere revision (CID) associated with +/// // the sphere this record maps to. +/// "fct": [{ +/// "address": "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72" +/// }] +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct NSRecord { + /// The wrapped Ucan token describing this record. + pub(crate) token: Ucan, + /// The resolved sphere revision this record maps to. + pub(crate) address: Option, +} + +impl NSRecord { + /// Creates a new [NSRecord]. + pub fn new(token: Ucan) -> Self { + // Cache the revision address if "fct" contains an entry matching + // the following object without any authority validation: + // `{ "address": "{VALID_CID}" }` + let mut address = None; + for ref fact in token.facts() { + if let Value::Object(map) = fact { + if let Some(Value::String(addr)) = map.get(&String::from("address")) { + if let Ok(cid) = Cid::from_str(addr) { + address = Some(cid); + break; + } + } + } + } + + Self { token, address } + } + + /// Validates the underlying [ucan::Ucan] token, ensuring that + /// the sphere's owner authorized the publishing of a new + /// content address. Returns an `Err` if validation fails. + pub async fn validate( + &mut self, + store: &SphereDb, + did_parser: &mut DidParser, + ) -> Result<(), Error> { + if self.is_expired() { + return Err(anyhow!("Token is expired.")); + } + + let identity = self.identity(); + + let desired_capability = generate_capability(identity); + let proof = ProofChain::from_ucan(self.token.clone(), did_parser, store).await?; + + let mut has_capability = false; + for capability_info in proof.reduce_capabilities(&SPHERE_SEMANTICS) { + let capability = capability_info.capability; + if capability_info.originators.contains(identity) + && capability.enables(&desired_capability) + { + has_capability = true; + break; + } + } + + if !has_capability { + return Err(anyhow!("Token is not authorized to publish this sphere.")); + } + + if self.address.is_none() { + return Err(anyhow!( + "Missing a valid fact entry with record sphere revision. {} {:?}", + identity, + self.token.facts() + )); + } + + self.token.check_signature(did_parser).await?; + Ok(()) + } + + /// The DID key of the sphere that this record maps. + pub fn identity(&self) -> &str { + self.token.audience() + } + + /// The sphere revision ([cid::Cid]) that the sphere's identity maps to. + pub fn address(&self) -> Option<&Cid> { + self.address.as_ref() + } + + /// Returns true if the UCAN token is past its expiration. + pub fn is_expired(&self) -> bool { + self.token.is_expired() + } +} + +impl From for NSRecord { + fn from(ucan: Ucan) -> Self { + Self::new(ucan) + } +} + +/// Deserialize an encoded UCAN token byte vec into a [NSRecord]. +impl TryFrom> for NSRecord { + type Error = anyhow::Error; + + fn try_from(bytes: Vec) -> Result { + NSRecord::try_from(&bytes[..]) + } +} + +/// Serialize a [NSRecord] into an encoded UCAN token byte vec. +impl TryFrom for Vec { + type Error = anyhow::Error; + + fn try_from(record: NSRecord) -> Result { + Vec::try_from(&record) + } +} + +/// Deserialize an encoded UCAN token byte vec reference into a [NSRecord]. +impl TryFrom<&[u8]> for NSRecord { + type Error = anyhow::Error; + + fn try_from(bytes: &[u8]) -> Result { + NSRecord::try_from(str::from_utf8(bytes)?) + } +} + +/// Serialize a [NSRecord] reference into an encoded UCAN token byte vec. +impl TryFrom<&NSRecord> for Vec { + type Error = anyhow::Error; + + fn try_from(record: &NSRecord) -> Result { + Ok(Vec::from(record.token.encode()?)) + } +} + +/// Deserialize an encoded UCAN token string reference into a [NSRecord]. +impl<'a> TryFrom<&'a str> for NSRecord { + type Error = anyhow::Error; + + fn try_from(ucan_token: &str) -> Result { + NSRecord::from_str(ucan_token) + } +} + +/// Deserialize an encoded UCAN token string into a [NSRecord]. +impl TryFrom for NSRecord { + type Error = anyhow::Error; + + fn try_from(ucan_token: String) -> Result { + NSRecord::from_str(ucan_token.as_str()) + } +} + +/// Deserialize an encoded UCAN token string reference into a [NSRecord]. +impl FromStr for NSRecord { + type Err = anyhow::Error; + + fn from_str(ucan_token: &str) -> Result { + // Wait for next release of `ucan` which includes traits and + // removes `try_from_token_string`: + // https://github.com/ucan-wg/rs-ucan/commit/75e9afdb9da60c3d5d8c65b6704e412f0ef8189b + Ok(NSRecord::new(Ucan::try_from_token_string(ucan_token)?)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use noosphere_core::authority::{generate_ed25519_key, SUPPORTED_KEYS}; + use noosphere_storage::{ + db::SphereDb, + memory::{MemoryStorageProvider, MemoryStore}, + }; + use serde_json::json; + use std::str::FromStr; + + use ucan::{ + builder::UcanBuilder, crypto::did::DidParser, crypto::KeyMaterial, store::UcanJwtStore, + }; + + async fn expect_failure( + message: &str, + store: &SphereDb, + did_parser: &mut DidParser, + ucan: Ucan, + ) { + assert!( + NSRecord::new(ucan) + .validate(store, did_parser) + .await + .is_err(), + "{}", + message + ); + } + + #[tokio::test] + async fn test_nsrecord_self_signed() -> Result<(), Error> { + let sphere_key = generate_ed25519_key(); + let sphere_identity = sphere_key.get_did().await?; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let capability = generate_capability(&sphere_identity); + let cid_address = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72"; + let fact = json!({ "address": cid_address }); + let store = SphereDb::new(&MemoryStorageProvider::default()) + .await + .unwrap(); + + let mut record = NSRecord::new( + UcanBuilder::default() + .issued_by(&sphere_key) + .for_audience(&sphere_identity) + .with_lifetime(1000) + .claiming_capability(&capability) + .with_fact(fact) + .build()? + .sign() + .await?, + ); + + assert_eq!(record.identity(), &sphere_identity); + assert_eq!(record.address(), Some(&Cid::from_str(cid_address).unwrap())); + record.validate(&store, &mut did_parser).await?; + Ok(()) + } + + #[tokio::test] + async fn test_nsrecord_delegated() -> Result<(), Error> { + let owner_key = generate_ed25519_key(); + let owner_identity = owner_key.get_did().await?; + let sphere_key = generate_ed25519_key(); + let sphere_identity = sphere_key.get_did().await?; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let capability = generate_capability(&sphere_identity); + let cid_address = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72"; + let fact = json!({ "address": cid_address }); + let mut store = SphereDb::new(&MemoryStorageProvider::default()) + .await + .unwrap(); + + // First verify that `owner` cannot publish for `sphere` + // without delegation. + let mut record = NSRecord::new( + UcanBuilder::default() + .issued_by(&owner_key) + .for_audience(&sphere_identity) + .with_lifetime(1000) + .claiming_capability(&capability) + .with_fact(fact.clone()) + .build()? + .sign() + .await?, + ); + + assert_eq!(record.identity(), &sphere_identity); + assert_eq!(record.address(), Some(&Cid::from_str(cid_address).unwrap())); + if record.validate(&store, &mut did_parser).await.is_ok() { + panic!("Owner should not have authorization to publish record") + } + + // Delegate `sphere_key`'s publishing authority to `owner_key` + let delegate_capability = generate_capability(&sphere_identity); + let delegate_ucan = UcanBuilder::default() + .issued_by(&sphere_key) + .for_audience(&owner_identity) + .with_lifetime(1000) + .claiming_capability(&delegate_capability) + .build()? + .sign() + .await?; + let _ = store.write_token(&delegate_ucan.encode()?).await?; + + // Attempt `owner` publishing `sphere` with the proper authorization + let mut record = NSRecord::new( + UcanBuilder::default() + .issued_by(&owner_key) + .for_audience(&sphere_identity) + .with_lifetime(1000) + .claiming_capability(&capability) + .witnessed_by(&delegate_ucan) + .with_fact(fact.clone()) + .build()? + .sign() + .await?, + ); + assert_eq!(record.identity(), &sphere_identity); + assert_eq!(record.address(), Some(&Cid::from_str(cid_address).unwrap())); + record.validate(&store, &mut did_parser).await?; + + Ok(()) + } + + #[tokio::test] + async fn test_nsrecord_failures() -> Result<(), Error> { + let sphere_key = generate_ed25519_key(); + let sphere_identity = sphere_key.get_did().await?; + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let cid_address = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72"; + let store = SphereDb::new(&MemoryStorageProvider::default()) + .await + .unwrap(); + + let sphere_capability = generate_capability(&sphere_identity); + expect_failure( + "fails when expect `fact` is missing", + &store, + &mut did_parser, + UcanBuilder::default() + .issued_by(&sphere_key) + .for_audience(&sphere_identity) + .with_lifetime(1000) + .claiming_capability(&sphere_capability) + .with_fact(json!({ "invalid_fact": cid_address })) + .build()? + .sign() + .await?, + ) + .await; + + let capability = generate_capability(&generate_ed25519_key().get_did().await?); + expect_failure( + "fails when capability resource does not match sphere identity", + &store, + &mut did_parser, + UcanBuilder::default() + .issued_by(&sphere_key) + .for_audience(&sphere_identity) + .with_lifetime(1000) + .claiming_capability(&capability) + .with_fact(json!({ "address": cid_address })) + .build()? + .sign() + .await?, + ) + .await; + + let non_auth_key = generate_ed25519_key(); + expect_failure( + "fails when a non-authorized key signs the record", + &store, + &mut did_parser, + UcanBuilder::default() + .issued_by(&non_auth_key) + .for_audience(&sphere_identity) + .with_lifetime(1000) + .claiming_capability(&sphere_capability) + .with_fact(json!({ "address": cid_address })) + .build()? + .sign() + .await?, + ) + .await; + + Ok(()) + } + + #[tokio::test] + async fn test_nsrecord_convert() -> Result<(), Error> { + let sphere_key = generate_ed25519_key(); + let sphere_identity = sphere_key.get_did().await?; + let capability = generate_capability(&sphere_identity); + let cid_address = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72"; + let fact = json!({ "address": cid_address }); + + let ucan = UcanBuilder::default() + .issued_by(&sphere_key) + .for_audience(&sphere_identity) + .with_lifetime(1000) + .claiming_capability(&capability) + .with_fact(fact) + .build()? + .sign() + .await?; + + let base = NSRecord::new(ucan.clone()); + let encoded = ucan.encode()?; + let bytes = Vec::from(encoded.clone()); + + // NSRecord::try_from::>() + let record = NSRecord::try_from(bytes.clone())?; + assert_eq!(base.identity(), record.identity(), "try_from::>()"); + assert_eq!(base.address(), record.address(), "try_from::>()"); + + // NSRecord::try_into::>() + let rec_bytes: Vec = base.clone().try_into()?; + assert_eq!(bytes, rec_bytes, "try_into::>()"); + + // NSRecord::try_from::<&[u8]>() + let record = NSRecord::try_from(&bytes[..])?; + assert_eq!(base.identity(), record.identity(), "try_from::<&[u8]>()"); + assert_eq!(base.address(), record.address(), "try_from::<&[u8]>()"); + + // &NSRecord::try_into::>() + let rec_bytes: Vec = (&base).try_into()?; + assert_eq!(bytes, rec_bytes, "&NSRecord::try_into::>()"); + + // NSRecord::from::() + let record = NSRecord::from(ucan); + assert_eq!(base.identity(), record.identity(), "from::()"); + assert_eq!(base.address(), record.address(), "from::()"); + + // NSRecord::try_from::<&str>() + let record = NSRecord::try_from(encoded.as_str())?; + assert_eq!(base.identity(), record.identity(), "try_from::<&str>()"); + assert_eq!(base.address(), record.address(), "try_from::<&str>()"); + + // NSRecord::try_from::() + let record = NSRecord::try_from(encoded.clone())?; + assert_eq!(base.identity(), record.identity(), "try_from::()"); + assert_eq!(base.address(), record.address(), "try_from::()"); + + // NSRecord::from_str() + let record = NSRecord::from_str(encoded.as_str())?; + assert_eq!(base.identity(), record.identity(), "from_str()"); + assert_eq!(base.address(), record.address(), "from_str()"); + + Ok(()) + } +} diff --git a/rust/noosphere-ns/src/utils.rs b/rust/noosphere-ns/src/utils.rs new file mode 100644 index 000000000..82f5ce15b --- /dev/null +++ b/rust/noosphere-ns/src/utils.rs @@ -0,0 +1,49 @@ +use noosphere_core::authority::{SphereAction, SphereReference}; +use serde_json; +use ucan::capability::{Capability, Resource, With}; + +/// Generates a [Capability] struct representing permission to +/// publish a sphere. +/// +/// ``` +/// use noosphere_ns::utils::generate_capability; +/// use noosphere_core::authority::{SphereAction, SphereReference}; +/// use ucan::capability::{Capability, Resource, With}; +/// +/// let identity = "did:key:z6MkoE19WHXJzpLqkxbGP7uXdJX38sWZNUWwyjcuCmjhPpUP"; +/// let expected_capability = Capability { +/// with: With::Resource { +/// kind: Resource::Scoped(SphereReference { +/// did: identity.to_owned(), +/// }), +/// }, +/// can: SphereAction::Publish, +/// }; +/// assert_eq!(generate_capability(identity), expected_capability); +/// ``` +pub fn generate_capability(sphere_did: &str) -> Capability { + Capability { + with: With::Resource { + kind: Resource::Scoped(SphereReference { + did: sphere_did.to_owned(), + }), + }, + can: SphereAction::Publish, + } +} + +/// Generates a UCAN `"fct"` struct for the NS network, representing +/// the resolved sphere's revision as a [cid::Cid]. +/// +/// ``` +/// use noosphere_ns::utils::generate_fact; +/// use noosphere_storage::encoding::derive_cid; +/// use libipld_cbor::DagCborCodec; +/// use serde_json::json; +/// +/// let address = "bafy2bzaced25m65oooyocdin7uyehm7u6eak3iauyxbxxvoos6atwe7vvmv46"; +/// assert_eq!(generate_fact(address), json!({ "address": address })); +/// ``` +pub fn generate_fact(address: &str) -> serde_json::Value { + serde_json::json!({ "address": address }) +} diff --git a/rust/noosphere-ns/tests/integration_test.rs b/rust/noosphere-ns/tests/dht_test.rs similarity index 87% rename from rust/noosphere-ns/tests/integration_test.rs rename to rust/noosphere-ns/tests/dht_test.rs index bbacaa4c1..744a27f71 100644 --- a/rust/noosphere-ns/tests/integration_test.rs +++ b/rust/noosphere-ns/tests/dht_test.rs @@ -3,19 +3,15 @@ use noosphere_ns::dht::{DHTError, DHTNetworkInfo, DHTNode, DHTStatus}; use std::str; -mod utils; +pub mod utils; use noosphere_core::authority::generate_ed25519_key; -use utils::{create_test_config, generate_listening_addr, initialize_network, swarm_command}; + +use utils::{create_test_config, initialize_network, swarm_command}; /// Testing a detached DHTNode as a server with no peers. #[test_log::test(tokio::test)] async fn test_dhtnode_base_case() -> Result<(), DHTError> { - let mut node = DHTNode::new( - &generate_ed25519_key(), - &generate_listening_addr(), - None, - &create_test_config(), - )?; + let mut node = DHTNode::new(&generate_ed25519_key(), None, &create_test_config())?; assert_eq!(node.status(), DHTStatus::Initialized, "DHT is initialized"); node.run()?; assert_eq!(node.status(), DHTStatus::Active, "DHT is active"); @@ -30,11 +26,10 @@ async fn test_dhtnode_base_case() -> Result<(), DHTError> { num_pending: 0, } ); - assert_eq!( - node.bootstrap().await?, - (), - "bootstrap() should succeed, even without peers to bootstrap." - ); + + if node.bootstrap().await.is_err() { + panic!("bootstrap() should succeed, even without peers to bootstrap."); + } node.terminate()?; assert_eq!(node.status(), DHTStatus::Terminated, "DHT is terminated"); @@ -85,7 +80,11 @@ async fn test_dhtnode_simple() -> Result<(), DHTError> { let result = client_b .get_record(String::from("foo").into_bytes()) .await?; - assert_eq!(str::from_utf8(result.as_ref().unwrap()).unwrap(), "bar"); + assert_eq!(str::from_utf8(&result.0).expect("parseable"), "foo"); + assert_eq!( + str::from_utf8(result.1.as_ref().unwrap()).expect("parseable"), + "bar" + ); Ok(()) } diff --git a/rust/noosphere-ns/tests/ns_test.rs b/rust/noosphere-ns/tests/ns_test.rs new file mode 100644 index 000000000..2cac6a357 --- /dev/null +++ b/rust/noosphere-ns/tests/ns_test.rs @@ -0,0 +1,215 @@ +#![cfg(not(target_arch = "wasm32"))] +#![cfg(test)] +pub mod utils; +use anyhow::{anyhow, Result}; +use futures::future::try_join_all; +use libipld_cbor::DagCborCodec; +use noosphere_core::{authority::generate_ed25519_key, view::SPHERE_LIFETIME}; +use noosphere_ns::{ + dht::DHTNode, + utils::{generate_capability, generate_fact}, + NameSystem, NameSystemBuilder, +}; +use noosphere_storage::{ + db::SphereDb, encoding::derive_cid, memory::MemoryStorageProvider, memory::MemoryStore, +}; + +use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore, time::now, Ucan}; +use ucan_key_support::ed25519::Ed25519KeyMaterial; +use utils::create_bootstrap_nodes; + +/// Data related to an owner sphere and a NameSystem running +/// on its behalf in it's corresponding gateway. +struct NSData { + pub ns: NameSystem, + pub owner_key: Ed25519KeyMaterial, + pub owner_id: String, + pub sphere_id: String, + pub delegation: Ucan, +} + +/// Generates a DHT network bootstrap node with `ns_count` +/// NameSystems connected, each with a corresponding owner sphere. +async fn generate_name_systems_network( + ns_count: usize, +) -> Result<(DHTNode, SphereDb, Vec)> { + let bootstrap_node = create_bootstrap_nodes(1) + .map_err(|e| anyhow!(e.to_string()))? + .pop() + .unwrap(); + let bootstrap_addresses = vec![bootstrap_node.p2p_address().unwrap().to_owned()]; + + let mut store = SphereDb::new(&MemoryStorageProvider::default()).await?; + let mut name_systems: Vec = vec![]; + + for _ in 0..ns_count { + let owner_key = generate_ed25519_key(); + let owner_id = owner_key.get_did().await?; + let sphere_key = generate_ed25519_key(); + let sphere_id = sphere_key.get_did().await?; + + // Delegate `sphere_key`'s publishing authority to `owner_key` + let delegate_capability = generate_capability(&sphere_id); + let delegation = UcanBuilder::default() + .issued_by(&sphere_key) + .for_audience(&owner_id) + .with_lifetime(SPHERE_LIFETIME) + .claiming_capability(&delegate_capability) + .build()? + .sign() + .await?; + let _ = &store.write_token(&delegation.encode()?).await?; + + let ns_key = generate_ed25519_key(); + let ns: NameSystem = NameSystemBuilder::default() + .key_material(&ns_key) + .store(&store) + .propagation_interval(3600) + .peer_dialing_interval(1) + .bootstrap_peers(&bootstrap_addresses) + .build()?; + name_systems.push(NSData { + ns, + owner_key, + owner_id, + sphere_id, + delegation, + }); + } + + let futures: Vec<_> = name_systems + .iter_mut() + .map(|data| data.ns.connect()) + .collect(); + try_join_all(futures).await?; + + Ok((bootstrap_node, store, name_systems)) +} + +#[test_log::test(tokio::test)] +async fn test_name_system_peer_propagation() -> Result<()> { + // Create two NameSystems, where `ns_1` is publishing for `sphere_1` + // and `ns_2` is publishing for `sphere_2`. + let (_bootstrap_node, _store, mut ns_data) = generate_name_systems_network(2).await?; + + let sphere_1_cid_1 = derive_cid::(b"00000000"); + let sphere_1_cid_2 = derive_cid::(b"11111111"); + let sphere_2_cid_1 = derive_cid::(b"99999999"); + let sphere_2_cid_2 = derive_cid::(b"88888888"); + + let [mut ns_1, mut ns_2] = [ns_data.remove(0), ns_data.remove(0)]; + + // Test propagating records from ns_1 to ns_2 + ns_1.ns + .set_record( + UcanBuilder::default() + .issued_by(&ns_1.owner_key) + .for_audience(&ns_1.sphere_id) + .with_lifetime(SPHERE_LIFETIME - 1000) + .claiming_capability(&generate_capability(&ns_1.sphere_id)) + .with_fact(generate_fact(&sphere_1_cid_1.to_string())) + .witnessed_by(&ns_1.delegation) + .build()? + .sign() + .await? + .into(), + ) + .await?; + + // `None` for a record that cannot be found + assert!( + ns_2.ns.get_record("unknown").await?.is_none(), + "no record found" + ); + + // Baseline fetching record from the network. + assert_eq!( + ns_2.ns + .get_record(&ns_1.sphere_id) + .await? + .expect("to be some") + .address() + .unwrap(), + &sphere_1_cid_1, + "first record found" + ); + + // Flush records by identity and fetch latest value from network. + ns_1.ns + .set_record( + UcanBuilder::default() + .issued_by(&ns_1.owner_key) + .for_audience(&ns_1.sphere_id) + .with_lifetime(SPHERE_LIFETIME - 1000) + .claiming_capability(&generate_capability(&ns_1.sphere_id)) + .with_fact(generate_fact(&sphere_1_cid_2.to_string())) + .witnessed_by(&ns_1.delegation) + .build()? + .sign() + .await? + .into(), + ) + .await?; + assert!(!ns_2 + .ns + .flush_records_for_identity(&generate_ed25519_key().get_did().await?)); + assert!(ns_2.ns.flush_records_for_identity(&ns_1.sphere_id)); + assert_eq!( + ns_2.ns + .get_record(&ns_1.sphere_id) + .await? + .expect("to be some") + .address() + .unwrap(), + &sphere_1_cid_2, + "latest record is found from network after flushing record" + ); + + // Store an expired record in ns_1's cache + ns_1.ns.get_cache_mut().insert( + ns_2.owner_id.clone(), + UcanBuilder::default() + .issued_by(&ns_2.owner_key) + .for_audience(&ns_2.sphere_id) + .with_expiration(now() - 1000) // already expired + .claiming_capability(&generate_capability(&ns_2.sphere_id)) + .with_fact(generate_fact(&sphere_2_cid_1.to_string())) + .witnessed_by(&ns_2.delegation) + .build()? + .sign() + .await? + .into(), + ); + + // Publish an updated record for sphere_2 + ns_2.ns + .set_record( + UcanBuilder::default() + .issued_by(&ns_2.owner_key) + .for_audience(&ns_2.sphere_id) + .with_lifetime(SPHERE_LIFETIME - 1000) + .claiming_capability(&generate_capability(&ns_2.sphere_id)) + .with_fact(generate_fact(&sphere_2_cid_2.to_string())) + .witnessed_by(&ns_2.delegation) + .build()? + .sign() + .await? + .into(), + ) + .await?; + + // Fetch sphere 2's record, which should check the network + // rather than using the cached, expired record. + assert_eq!( + ns_1.ns + .get_record(&ns_2.sphere_id) + .await? + .expect("to be some") + .address() + .unwrap(), + &sphere_2_cid_2, + "non-cached record found for sphere_2" + ); + + Ok(()) +} diff --git a/rust/noosphere-ns/tests/utils/mod.rs b/rust/noosphere-ns/tests/utils/mod.rs index 26239276d..78a97982a 100644 --- a/rust/noosphere-ns/tests/utils/mod.rs +++ b/rust/noosphere-ns/tests/utils/mod.rs @@ -1,25 +1,26 @@ #![cfg(test)] use futures::future::try_join_all; - +use libp2p::{self, Multiaddr}; use noosphere_core::authority::generate_ed25519_key; use noosphere_ns::dht::{DHTConfig, DHTError, DHTNode}; use rand::{thread_rng, Rng}; use std::future::Future; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::time::Duration; -pub fn generate_listening_addr() -> SocketAddr { - SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - thread_rng().gen_range(49152..65535), +fn generate_listening_addr() -> Multiaddr { + format!( + "/ip4/127.0.0.1/tcp/{}", + thread_rng().gen_range(49152..65535) ) + .parse() + .expect("parseable") } pub async fn wait_ms(ms: u64) { tokio::time::sleep(Duration::from_millis(ms)).await; } -pub async fn await_or_timeout( +async fn await_or_timeout( timeout_ms: u64, future: impl Future, message: String, @@ -31,13 +32,15 @@ pub async fn await_or_timeout( } pub fn create_test_config() -> DHTConfig { - let mut config = DHTConfig::default(); - config.peer_dialing_interval = 1; - config + DHTConfig { + listening_address: Some(generate_listening_addr()), + peer_dialing_interval: 1, + ..Default::default() + } } pub async fn swarm_command<'a, TFuture, F, T, E>( - nodes: &'a mut Vec, + nodes: &'a mut [DHTNode], func: F, ) -> Result, E> where @@ -48,28 +51,21 @@ where try_join_all(futures).await } -pub fn create_client_nodes_with_bootstrap_peers( +fn create_client_nodes_with_bootstrap_peers( bootstrap_count: usize, client_count: usize, ) -> Result<(Vec, Vec), DHTError> { let bootstrap_nodes = create_bootstrap_nodes(bootstrap_count)?; - let bootstrap_addresses: Vec = bootstrap_nodes + let bootstrap_addresses: Vec = bootstrap_nodes .iter() - // Remap Multiaddr to String for DHTNode interface - .map(|node| node.p2p_address().to_string()) + .map(|node| node.p2p_address().unwrap().to_owned()) .collect(); let mut client_nodes: Vec = vec![]; for _ in 0..client_count { let key_material = generate_ed25519_key(); - let listening_address = generate_listening_addr(); let config = create_test_config(); - let mut node = DHTNode::new( - &key_material, - &listening_address, - Some(&bootstrap_addresses), - &config, - )?; + let mut node = DHTNode::new(&key_material, Some(&bootstrap_addresses), &config)?; node.run()?; client_nodes.push(node); } @@ -80,14 +76,12 @@ pub fn create_client_nodes_with_bootstrap_peers( /// bootstrap nodes as bootstrap peers. pub fn create_bootstrap_nodes(count: usize) -> Result, DHTError> { let mut nodes: Vec = vec![]; - let mut addresses: Vec = vec![]; + let mut addresses: Vec = vec![]; for _ in 0..count { let key_material = generate_ed25519_key(); - let listening_address = generate_listening_addr(); let config = create_test_config(); - let node = DHTNode::new(&key_material, &listening_address, None, &config)?; - // Remap Multiaddr to String for DHTNode interface - addresses.push(node.p2p_address().to_string()); + let node = DHTNode::new(&key_material, None, &config)?; + addresses.push(node.p2p_address().unwrap().to_owned()); nodes.push(node); }