diff --git a/.config/cspell.yaml b/.config/cspell.yaml index 6acc3c6963..47f208294e 100644 --- a/.config/cspell.yaml +++ b/.config/cspell.yaml @@ -16,6 +16,7 @@ words: - ciphertext - Devnet - grpcurl + - Iroh - Merkle - Miden - Midenscan diff --git a/Cargo.lock b/Cargo.lock index e94dcca4fd..44119bd3ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + [[package]] name = "aead" version = "0.6.1" @@ -12,6 +22,31 @@ dependencies = [ "inout 0.2.2", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead 0.5.2", + "aes", + "cipher 0.4.4", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.5" @@ -481,6 +516,18 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-trait" version = "0.1.91" @@ -492,6 +539,26 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version 0.4.1", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1010,6 +1077,25 @@ dependencies = [ "tokio", ] +[[package]] +name = "bao-tree" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06384416b1825e6e04fde63262fda2dc408f5b64c02d04e0d8b70ae72c17a52b" +dependencies = [ + "blake3", + "bytes", + "futures-lite", + "genawaiter", + "iroh-io", + "positioned-io", + "range-collections", + "self_cell", + "serde", + "smallvec", + "tokio", +] + [[package]] name = "base16ct" version = "1.0.0" @@ -1063,6 +1149,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "binary-merge" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597bb81c80a54b6a4381b23faba8d7774b144c94cbd1d6fe3f1329bd776554ab" + [[package]] name = "bindgen" version = "0.72.1" @@ -1152,6 +1244,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bon" version = "3.9.3" @@ -1235,6 +1336,9 @@ name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] [[package]] name = "bytes-utils" @@ -1274,6 +1378,12 @@ dependencies = [ "shlex 2.0.1", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -1324,7 +1434,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" dependencies = [ - "aead", + "aead 0.6.1", "chacha20 0.10.1", "cipher 0.5.2", "poly1305", @@ -1458,6 +1568,15 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.19", +] + [[package]] name = "codegen" version = "0.3.0" @@ -1483,6 +1602,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-hex" version = "1.19.1" @@ -1537,6 +1665,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cordyceps" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9ab7e0ca1d179628fa0172b2b97203c7fa0cd81be2448bd446fb9559ca9261" +dependencies = [ + "loom", + "tracing", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1628,6 +1766,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -1695,6 +1842,15 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -1716,7 +1872,9 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.11.3", "fiat-crypto", + "rand_core 0.10.1", "rustc_version 0.4.1", + "serde", "subtle", "zeroize", ] @@ -1815,6 +1973,32 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "data-encoding-macro" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a127ecbb3c4632e1525380e04c0c3fcf8dcb44d32a79ea290d8a36906edcd8" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" +dependencies = [ + "data-encoding", + "syn 3.0.3", +] + [[package]] name = "deadpool" version = "0.12.3" @@ -1894,6 +2078,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -1940,6 +2125,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + [[package]] name = "diesel" version = "2.3.11" @@ -2017,6 +2208,18 @@ dependencies = [ "ctutils", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -2034,6 +2237,17 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + [[package]] name = "downcast-rs" version = "2.0.2" @@ -2088,6 +2302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" dependencies = [ "pkcs8", + "serdect", "signature", ] @@ -2099,6 +2314,7 @@ checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core 0.10.1", "serde", "sha2 0.11.0", "signature", @@ -2145,6 +2361,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -2154,6 +2382,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-assoc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "enum-ordinalize" version = "4.4.2" @@ -2224,6 +2463,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -2396,6 +2655,19 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-buffered" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.1", +] + [[package]] name = "futures-channel" version = "0.3.33" @@ -2406,6 +2678,19 @@ dependencies = [ "futures-sink", ] +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + [[package]] name = "futures-core" version = "0.3.33" @@ -2429,6 +2714,19 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.33" @@ -2475,6 +2773,37 @@ dependencies = [ "slab", ] +[[package]] +name = "genawaiter" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" +dependencies = [ + "futures-core", + "genawaiter-macro", + "genawaiter-proc-macro", + "proc-macro-hack", +] + +[[package]] +name = "genawaiter-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" + +[[package]] +name = "genawaiter-proc-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738" +dependencies = [ + "proc-macro-error", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "generator" version = "0.8.9" @@ -2541,6 +2870,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob" version = "0.3.4" @@ -2753,6 +3092,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2791,6 +3139,8 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.2.0", ] @@ -2803,6 +3153,20 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version 0.4.1", + "serde", + "spin 0.9.9", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -2822,16 +3186,93 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hkdf" -version = "0.12.4" +name = "hickory-net" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ - "hmac 0.12.1", -] - -[[package]] -name = "hkdf" + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "h2 0.4.15", + "hickory-proto", + "http 1.5.0", + "idna", + "ipnet", + "jni 0.22.4", + "rand 0.10.2", + "rustls 0.23.43", + "thiserror 2.0.19", + "tinyvec", + "tokio", + "tokio-rustls 0.26.4", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni 0.22.4", + "once_cell", + "prefix-trie", + "rand 0.10.2", + "ring", + "thiserror 2.0.19", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni 0.22.4", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.2", + "resolv-conf", + "rustls 0.23.43", + "smallvec", + "system-configuration", + "thiserror 2.0.19", + "tokio", + "tokio-rustls 0.26.4", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" @@ -3168,6 +3609,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + [[package]] name = "idna" version = "1.1.0" @@ -3256,11 +3703,376 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "inplace-vec-builder" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf64c2edc8226891a71f127587a2861b132d2b942310843814d5001d99a1d307" +dependencies = [ + "smallvec", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2 0.6.5", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", +] + [[package]] name = "ipnet" version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +dependencies = [ + "serde", +] + +[[package]] +name = "iroh" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460de6bc52163b41b1646931f2897e5ab986f0966ade444467fec25024751a72" +dependencies = [ + "backon", + "blake3", + "bytes", + "cfg_aliases", + "ctutils", + "data-encoding", + "derive_more", + "ed25519-dalek", + "futures-util", + "getrandom 0.4.3", + "hickory-resolver", + "http 1.5.0", + "ipnet", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netwatch", + "noq", + "noq-proto", + "noq-udp", + "papaya", + "pin-project", + "portable-atomic", + "rand 0.10.2", + "reqwest", + "rustc-hash", + "rustls 0.23.43", + "rustls-pki-types", + "serde", + "smallvec", + "strum", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "iroh-base" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6be73e16ee21c923aca9b3121aaa0db936f7c7ecc156ff47b8dac944c68d59a8" +dependencies = [ + "curve25519-dalek", + "data-encoding", + "data-encoding-macro", + "derive_more", + "ed25519-dalek", + "getrandom 0.4.3", + "n0-error", + "rand 0.10.2", + "serde", + "url", + "zeroize", +] + +[[package]] +name = "iroh-blobs" +version = "0.103.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be50b0e2d0a9ba65cee4e0dfb708b3704e02ad12bd4c14c6307e94245943126" +dependencies = [ + "arrayvec", + "bao-tree", + "bytes", + "cfg_aliases", + "chrono", + "constant_time_eq", + "data-encoding", + "derive_more", + "genawaiter", + "getrandom 0.4.3", + "hex", + "iroh", + "iroh-base", + "iroh-io", + "iroh-metrics", + "iroh-tickets", + "iroh-util", + "irpc", + "n0-error", + "n0-future", + "nested_enum_utils", + "postcard", + "rand 0.10.2", + "range-collections", + "redb", + "ref-cast", + "reflink-copy", + "self_cell", + "serde", + "smallvec", + "tokio", + "tracing", +] + +[[package]] +name = "iroh-dns" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f6a9b39d18e6345f5c151afd299f2488e2cb5c520fe41b107b6bd3dc4c3349" +dependencies = [ + "arc-swap", + "cfg_aliases", + "derive_more", + "hickory-resolver", + "iroh-base", + "n0-error", + "n0-future", + "ndk-context", + "portable-atomic", + "rand 0.10.2", + "rustls 0.23.43", + "simple-dns", + "strum", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "iroh-docs" +version = "0.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd1bd5e39d0321a3c4a2bcef9650476c076e2df41a0e84577eca23d6de6c8ab" +dependencies = [ + "anyhow", + "async-channel", + "blake3", + "bytes", + "cfg_aliases", + "derive_more", + "futures-buffered", + "hex", + "iroh", + "iroh-blobs", + "iroh-gossip", + "iroh-metrics", + "iroh-tickets", + "irpc", + "n0-error", + "n0-future", + "num_enum", + "postcard", + "rand 0.10.2", + "redb", + "self_cell", + "serde", + "serde-error", + "strum", + "tempfile", + "thiserror 2.0.19", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "iroh-gossip" +version = "0.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1dc4b05f73e7a1b9e83b531eb63c3fd671b0af3aeb13b59c546dd7ca747515" +dependencies = [ + "blake3", + "bytes", + "data-encoding", + "derive_more", + "futures-concurrency", + "hex", + "indexmap 2.14.0", + "iroh", + "iroh-base", + "iroh-metrics", + "irpc", + "n0-error", + "n0-future", + "postcard", + "rand 0.10.2", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "iroh-io" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a5feb781017b983ff1b155cd1faf8174da2acafd807aa482876da2d7e6577a" +dependencies = [ + "bytes", + "futures-lite", + "pin-project", + "smallvec", + "tokio", +] + +[[package]] +name = "iroh-metrics" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291065721ad7c477b972e581bbc528df031dc8eb5e39fe1ff3300ae5dfb157ef" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "portable-atomic", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae5f0c4405d1fbc9fb16ff422ca40620e93dc36c30ecaba0c2aee3992b7bd48" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "iroh-relay" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24bd586cf927f7b700f56ec3639b53cb5fa901ce284784051ff71092bfbf8193" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more", + "getrandom 0.4.3", + "hickory-resolver", + "http 1.5.0", + "http-body-util", + "hyper 1.11.0", + "hyper-util", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "lru 0.18.2", + "n0-error", + "n0-future", + "noq", + "noq-proto", + "num_enum", + "pin-project", + "postcard", + "rand 0.10.2", + "reqwest", + "rustls 0.23.43", + "rustls-pki-types", + "serde", + "serde_bytes", + "strum", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "webpki-roots", + "ws_stream_wasm", +] + +[[package]] +name = "iroh-tickets" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da53233419ca36bf521ed45683b7748366f9b233032891eefc2d70567a84ac54" +dependencies = [ + "data-encoding", + "derive_more", + "iroh-base", + "n0-error", + "postcard", + "serde", +] + +[[package]] +name = "iroh-util" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20e41eb982f15230c55f0a70a74a514360e1f565b07861924fd0e8db172b3d00" +dependencies = [ + "derive_more", + "iroh", + "n0-error", + "n0-future", + "tokio", + "tracing", +] + +[[package]] +name = "irpc" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3623d6ff582b415904b29bbe6ebcb4a4f9a262ccdee05a45fdd003ef0950c386" +dependencies = [ + "futures-util", + "irpc-derive", + "n0-error", + "n0-future", + "postcard", + "serde", + "smallvec", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "irpc-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c254013736de16472140d26904e6ac98e8f3887284dcf4af40f88c77411b56" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "is_terminal_polyfill" @@ -3346,6 +4158,22 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -3355,7 +4183,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys", + "jni-sys 0.4.1", "log", "simd_cesu8", "thiserror 2.0.19", @@ -3376,6 +4204,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + [[package]] name = "jni-sys" version = "0.4.1" @@ -3644,6 +4481,15 @@ version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +[[package]] +name = "lru" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -3660,6 +4506,12 @@ dependencies = [ "libc", ] +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + [[package]] name = "macro-string" version = "0.2.0" @@ -4391,7 +5243,7 @@ dependencies = [ "http-body-util", "humantime", "itertools 0.14.0", - "lru", + "lru 0.16.4", "miden-crypto", "miden-node-tracing-macro", "miden-protocol", @@ -4789,11 +5641,16 @@ dependencies = [ "chacha20poly1305", "clap", "fs-err", + "futures", "golden-core", "golden-ehtdh1", "golden-evrf", "golden-halo2curves", "hex", + "iroh", + "iroh-blobs", + "iroh-docs", + "iroh-gossip", "miden-node-db", "miden-node-proto", "miden-node-proto-build", @@ -4880,27 +5737,229 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "minimal-lexical" -version = "0.2.1" +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "n0-error" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c37e81176a83a77d2514528b91bdafc70ef88aab428f0e1b91aebb8d99888895" +dependencies = [ + "anyhow", + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2acd8b070213b0299282f884b4beba4e7b52d624fdcd504a3ad3665390c11e1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "n0-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" +dependencies = [ + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "n0-watcher" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc618745ad0b7414b149d0517ad8b5573b2fb4d4e2717add3d2446ce1fdd826" +dependencies = [ + "derive_more", + "n0-error", + "n0-future", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "nested_enum_utils" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d5475271bdd36a4a2769eac1ef88df0f99428ea43e52dfd8b0ee5cb674695f" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "netdev" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "569dfbdd2efd771b24ec9bb57f956e04d4fbfc72f62b2f11961723f9b3f4b020" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "jni 0.21.1", + "libc", + "mac-addr", + "ndk-context", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "objc2", + "objc2-core-foundation", + "objc2-core-wlan", + "objc2-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.61.2", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b897d7bd4f0af82e68d40d0344cf37e97f9c97ddf74a098de3e4da05e96ca395" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2288fcb784eb3defd5fb16f4c4160d5f477de192eac730f43e1d11c24d9a007" +dependencies = [ + "bitflags 2.13.1", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +checksum = "e6f7398dddf5f152d2a91a2921a134c6097056e292c0d4b9906007855e7cece6" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.19", +] [[package]] -name = "mio" -version = "1.2.2" +name = "netlink-sys" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" dependencies = [ + "bytes", + "futures-util", "libc", - "wasi", - "windows-sys 0.61.2", + "log", + "tokio", ] [[package]] -name = "multimap" -version = "0.10.1" +name = "netwatch" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +checksum = "4d9cbe01741347ef750d743d6690603f5eed8341e679fb51c8e629337aa11976" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more", + "ipnet", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "noq-udp", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2 0.6.5", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows", + "windows-result", + "wmi", +] [[package]] name = "nom" @@ -4924,6 +5983,68 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "noq" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e4bb6601fa543c110d8957813267d5a8d775a0f8fbaccf1f615d06ba9b10da" +dependencies = [ + "bytes", + "cfg_aliases", + "derive_more", + "noq-proto", + "noq-udp", + "pin-project-lite", + "rustc-hash", + "rustls 0.23.43", + "socket2 0.6.5", + "thiserror 2.0.19", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "noq-proto" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa7b5ccd819a9c68a0d955e67a881032d09b1a17219b1f90b0997a0888e1a15" +dependencies = [ + "aes-gcm", + "bytes", + "derive_more", + "enum-assoc", + "getrandom 0.4.3", + "identity-hash", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls 0.23.43", + "rustls-pki-types", + "slab", + "sorted-index-buffer", + "thiserror 2.0.19", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "noq-udp" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bba20e097a5a16cd0ad14ec882fae1e80a092a124e9422fc4dddd92e96a647" +dependencies = [ + "cfg_aliases", + "libc", + "socket2 0.6.5", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -5032,6 +6153,118 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-core-wlan" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-security", + "objc2-security-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -5054,6 +6287,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -5358,6 +6597,16 @@ dependencies = [ "group 0.13.0", ] +[[package]] +name = "papaya" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7" +dependencies = [ + "equivalent", + "seize", +] + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -5386,6 +6635,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -5421,6 +6676,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -5448,6 +6712,16 @@ dependencies = [ "indexmap 2.14.0", ] +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version 0.4.1", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -5496,6 +6770,19 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + [[package]] name = "plonky2_maybe_rayon" version = "1.0.0" @@ -5540,7 +6827,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" dependencies = [ "cpufeatures 0.3.0", - "universal-hash", + "universal-hash 0.6.1", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.5.1", ] [[package]] @@ -5548,6 +6847,9 @@ name = "portable-atomic" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +dependencies = [ + "serde", +] [[package]] name = "portable-atomic-util" @@ -5558,6 +6860,41 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "positioned-io" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ec4b80060f033312b99b6874025d9503d2af87aef2dd4c516e253fbfcdada7" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -5582,6 +6919,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -5661,12 +7009,38 @@ dependencies = [ ] [[package]] -name = "proc-macro-crate" -version = "3.5.0" +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" dependencies = [ - "toml_edit", + "proc-macro2", + "quote", + "syn 1.0.109", + "syn-mid", + "version_check", ] [[package]] @@ -5691,6 +7065,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.107" @@ -5889,6 +7269,15 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.11" @@ -6086,6 +7475,19 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "range-collections" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "861706ea9c4aded7584c5cd1d241cec2ea7f5f50999f236c22b65409a1f1a0d0" +dependencies = [ + "binary-merge", + "inplace-vec-builder", + "ref-cast", + "serde", + "smallvec", +] + [[package]] name = "raw-cpuid" version = "11.6.0" @@ -6115,6 +7517,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redb" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e925444704b5f17d32bf42f5b6e2df050bceebc3dcd6e71cc73dafe8092e839" +dependencies = [ + "libc", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -6144,6 +7555,18 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "reflink-copy" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9dd7ab4af0363d5ccfd2838d782a28196cf32a5cc2e4fe3c5dc83f2be588b8b" +dependencies = [ + "cfg-if", + "libc", + "rustix", + "windows", +] + [[package]] name = "regex" version = "1.13.1" @@ -6195,6 +7618,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", + "futures-util", "h2 0.4.15", "http 1.5.0", "http-body 1.1.0", @@ -6217,15 +7641,23 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls 0.26.4", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "rfc6979" version = "0.6.0" @@ -6468,7 +7900,7 @@ checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls 0.23.43", @@ -6634,6 +8066,22 @@ dependencies = [ "libc", ] +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + [[package]] name = "semver" version = "0.9.0" @@ -6677,6 +8125,12 @@ dependencies = [ "pest", ] +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.229" @@ -6687,6 +8141,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-error" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "342110fb7a5d801060c885da03bf91bfa7c7ca936deafcc64bb6706375605d47" +dependencies = [ + "serde", +] + [[package]] name = "serde-untagged" version = "0.1.9" @@ -6710,6 +8173,16 @@ dependencies = [ "wincode", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -6852,6 +8325,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -6962,6 +8441,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple-dns" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a75cbde1bf934313596a004973e462f9a82caa814dcf1a5f507bdf51597eeb4" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "slab" version = "0.4.12" @@ -7003,6 +8491,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "spin" version = "0.9.9" @@ -7012,6 +8517,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + [[package]] name = "spin" version = "0.12.2" @@ -7091,6 +8602,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "subtle" version = "2.6.1" @@ -7130,6 +8662,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-mid" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "syn-solidity" version = "1.6.1" @@ -7183,6 +8726,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -7298,6 +8847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", + "js-sys", "num-conv", "powerfmt", "serde_core", @@ -7440,6 +8990,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-websockets" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52efb639344a7c6adb8e62c6f3d2c19c001ff1b79a5041ba1c6ed42e19c6aa5" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.4.3", + "http 1.5.0", + "httparse", + "rand 0.10.2", + "ring", + "rustls-pki-types", + "sha1_smol", + "simdutf8", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", +] + [[package]] name = "toml" version = "1.1.4+spec-1.1.0" @@ -7920,6 +9493,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "universal-hash" version = "0.6.1" @@ -7973,6 +9556,7 @@ version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -8117,6 +9701,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.103" @@ -8134,6 +9731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", + "serde", "wasm-bindgen", ] @@ -8146,6 +9744,21 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -8301,13 +9914,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -8319,20 +9941,35 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -8344,18 +9981,36 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -8368,24 +10023,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -8407,6 +10086,21 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wmi" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.19", + "windows", + "windows-core", +] + [[package]] name = "wnaf" version = "0.14.0" @@ -8424,6 +10118,25 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version 0.4.1", + "send_wrapper", + "thiserror 2.0.19", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 84139473d5..b5800a81a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,10 @@ hex = { version = "0.4" } http = { version = "1.3" } humantime = { version = "2.2" } indexmap = { version = "2.12" } +iroh = { default-features = false, features = ["tls-ring"], version = "=1.0.3" } +iroh-blobs = { default-features = false, features = ["fs-store"], version = "=0.103.0" } +iroh-docs = { default-features = false, features = ["fs-store"], version = "=0.101.0" } +iroh-gossip = { default-features = false, features = ["net"], version = "=0.101.0" } itertools = { version = "0.14" } libsqlite3-sys = { features = ["bundled"], version = "0.35" } lru = { default-features = false, version = "0.16" } diff --git a/bin/validator/Cargo.toml b/bin/validator/Cargo.toml index 2c7e7e40c1..d57a74a477 100644 --- a/bin/validator/Cargo.toml +++ b/bin/validator/Cargo.toml @@ -26,11 +26,16 @@ base64 = { version = "0.22" } chacha20poly1305 = { workspace = true } clap = { features = ["env", "string"], workspace = true } fs-err = { workspace = true } +futures = { workspace = true } golden-core = { workspace = true } golden-ehtdh1 = { workspace = true } golden-evrf = { workspace = true } golden-halo2curves = { workspace = true } hex = { workspace = true } +iroh = { workspace = true } +iroh-blobs = { workspace = true } +iroh-docs = { workspace = true } +iroh-gossip = { workspace = true } miden-node-db = { workspace = true } miden-node-proto = { workspace = true } miden-node-proto-build = { features = ["internal"], workspace = true } @@ -43,7 +48,7 @@ serde = { workspace = true } sha2 = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } -tokio = { features = ["macros", "net", "rt-multi-thread"], workspace = true } +tokio = { features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"], workspace = true } tokio-stream = { features = ["net"], workspace = true } toml = { workspace = true } tonic = { default-features = true, features = ["transport"], workspace = true } diff --git a/bin/validator/src/commands/dkg.rs b/bin/validator/src/commands/dkg.rs index 7714049397..9ccb3608dc 100644 --- a/bin/validator/src/commands/dkg.rs +++ b/bin/validator/src/commands/dkg.rs @@ -47,6 +47,8 @@ use zeroize::Zeroizing; use super::ValidatorSigningKey; +mod board; +mod runner; #[cfg(test)] mod tests; @@ -92,6 +94,12 @@ pub struct DkgOptions { /// DKG ceremony commands. #[derive(clap::Subcommand)] enum DkgCommand { + /// Runs the shared Iroh bulletin board for a ceremony. + Board(runner::DkgBoardServeOptions), + + /// Runs every ceremony stage for one validator through an Iroh board. + Run(runner::DkgRunOptions), + /// Generates this validator's DKG identity and public registration. Identity { /// Trusted genesis block for the network. @@ -339,6 +347,8 @@ struct DealingSet { /// Runs one DKG ceremony command. pub async fn run(options: DkgOptions) -> anyhow::Result<()> { match options.command { + DkgCommand::Board(options) => runner::serve_board(options).await, + DkgCommand::Run(options) => runner::run_validator(options).await, DkgCommand::Identity { genesis, epoch, diff --git a/bin/validator/src/commands/dkg/board.rs b/bin/validator/src/commands/dkg/board.rs new file mode 100644 index 0000000000..a69a03f352 --- /dev/null +++ b/bin/validator/src/commands/dkg/board.rs @@ -0,0 +1,1062 @@ +//! A bounded, append-only exchange for storage key DKG artifacts. +//! +//! The board process is the only writer to the Iroh document. Validators receive a read-only +//! document ticket plus a participant-scoped secret for the board's bounded upload protocol. Each +//! [`ArtifactSlot`] is valid only while it holds at most one content-addressed value. A second +//! distinct value poisons that slot and stops the ceremony. This module only moves and stores +//! artifacts. The ceremony phases that use those artifacts are ordered in `runner`. + +use std::collections::BTreeMap; +use std::fmt; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{Context, ensure}; +use futures::StreamExt; +use iroh::endpoint::{Connection, presets}; +use iroh::protocol::{AcceptError, ProtocolHandler, Router}; +use iroh::{Endpoint, EndpointAddr, EndpointId, SecretKey}; +use iroh_blobs::api::downloader::{DownloadProgressItem, Downloader}; +use iroh_blobs::store::fs::FsStore; +use iroh_blobs::{BlobsProtocol, Hash}; +use iroh_docs::DocTicket; +use iroh_docs::api::Doc; +use iroh_docs::api::protocol::{AddrInfoOptions, ShareMode}; +use iroh_docs::engine::LiveEvent; +use iroh_docs::protocol::Docs; +use iroh_docs::store::{DownloadPolicy, Query}; +use iroh_gossip::net::Gossip; + +use super::{decode_fixed_hex, publish_directory, write_new_file}; + +const ENDPOINT_SECRET_FILE: &str = "endpoint-secret.hex"; +const DOCUMENT_ID_FILE: &str = "document-id.hex"; +const BOARD_FORMAT_FILE: &str = "board-format"; +const BOARD_FORMAT: &[u8] = b"participant-upload-v3\n"; +const UPLOAD_SECRETS_DIRECTORY: &str = "upload-secrets"; +const BOARD_TICKET_PREFIX: &str = "miden-storage-key-dkg-board-v3"; +const UPLOAD_ALPN: &[u8] = b"/miden/storage-key-dkg-board-upload/3"; +const UPLOAD_HEADER_BYTES: usize = 32 + 1 + 4 + 8; +const UPLOAD_RESPONSE_BYTES: usize = 1 + 32; +const MAX_ARTIFACT_BYTES: u64 = 64 * 1024 * 1024; +const MAX_CONCURRENT_UPLOADS: usize = 3; +const MAX_UPLOAD_ERROR_BYTES: usize = 1024; +const UPLOAD_TIMEOUT: Duration = Duration::from_secs(30); +const PEER_READY_TIMEOUT: Duration = Duration::from_secs(30); +const COMMON_ARTIFACT_COUNT: usize = 3; +const ARTIFACTS_PER_PARTICIPANT: usize = 4; +const MAX_VALUES_PER_SLOT: usize = 2; + +/// The board address and read capability, paired with one participant's upload permission. +/// +/// This credential contains no DKG private material. Its holder can read public ceremony artifacts +/// and upload only to the named participant's slots. +#[derive(Clone, Debug)] +pub(super) struct BoardTicket { + document: DocTicket, + pub(super) participant: u32, + upload_secret: [u8; 32], +} + +impl fmt::Display for BoardTicket { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "{BOARD_TICKET_PREFIX}:{}:{}:{}", + self.participant, + hex::encode(self.upload_secret), + self.document + ) + } +} + +impl FromStr for BoardTicket { + type Err = anyhow::Error; + + fn from_str(value: &str) -> Result { + let mut parts = value.splitn(4, ':'); + ensure!(parts.next() == Some(BOARD_TICKET_PREFIX), "invalid DKG board ticket prefix"); + let participant = parts + .next() + .context("DKG board ticket is missing its participant index")? + .parse::() + .context("invalid DKG board participant index")?; + ensure!(participant > 0, "DKG board participant index must be nonzero"); + let secret = parts.next().context("DKG board ticket is missing its upload secret")?; + let document = parts.next().context("DKG board ticket is missing its document ticket")?; + let upload_secret = decode_fixed_hex::<32>(secret, "DKG board upload secret")?; + let document = DocTicket::from_str(document).context("invalid Iroh document ticket")?; + ensure!( + matches!(document.capability, iroh_docs::Capability::Read(_)), + "DKG board document ticket must be read-only" + ); + Ok(Self { document, participant, upload_secret }) + } +} + +/// One immutable location in a DKG ceremony document. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) enum ArtifactSlot { + Registration(u32), + Manifest, + DecryptionConfig, + ContextConfig, + DecryptionDealing(u32), + ContextDealing(u32), + TranscriptAcceptance(u32), +} + +impl ArtifactSlot { + fn prefix(&self) -> String { + match self { + Self::Registration(participant) => format!("registration/{participant}/"), + Self::Manifest => "common/manifest/".to_owned(), + Self::DecryptionConfig => "common/decryption-config/".to_owned(), + Self::ContextConfig => "common/context-config/".to_owned(), + Self::DecryptionDealing(participant) => { + format!("dealing/{participant}/decryption/") + }, + Self::ContextDealing(participant) => format!("dealing/{participant}/context/"), + Self::TranscriptAcceptance(participant) => { + format!("acceptance/{participant}/signature/") + }, + } + } + + fn key(&self, hash: Hash) -> String { + format!("{}{}", self.prefix(), hash.to_hex()) + } + + fn upload_fields(&self) -> anyhow::Result<(u8, u32)> { + let fields = match self { + Self::Registration(participant) => (1, *participant), + Self::DecryptionDealing(participant) => (2, *participant), + Self::ContextDealing(participant) => (3, *participant), + Self::TranscriptAcceptance(participant) => (4, *participant), + Self::Manifest | Self::DecryptionConfig | Self::ContextConfig => { + anyhow::bail!("only the DKG board may publish common ceremony artifacts") + }, + }; + Ok(fields) + } + + fn from_upload_fields(kind: u8, participant: u32) -> anyhow::Result { + ensure!(participant > 0, "DKG board participant index must be nonzero"); + match kind { + 1 => Ok(Self::Registration(participant)), + 2 => Ok(Self::DecryptionDealing(participant)), + 3 => Ok(Self::ContextDealing(participant)), + 4 => Ok(Self::TranscriptAcceptance(participant)), + _ => anyhow::bail!("DKG board upload contains an unknown artifact kind"), + } + } +} + +#[derive(Clone, Debug)] +struct BoardWriter { + author: iroh_docs::AuthorId, + document: Doc, + allowed_prefixes: Arc>, + lock: Arc>, +} + +#[derive(Debug)] +enum Publisher { + Local(BoardWriter), + Remote { + endpoint: Endpoint, + participant: u32, + target: EndpointAddr, + upload_secret: [u8; 32], + }, +} + +#[derive(Clone, Debug)] +struct UploadProtocol { + permits: Arc, + upload_secrets: Arc>, + writer: BoardWriter, +} + +/// A persistent Iroh node joined to one ceremony document. +pub(super) struct BoardNode { + blobs: FsStore, + document: Doc, + downloader: Downloader, + event_error: tokio::sync::watch::Receiver>, + event_task: tokio::task::JoinHandle<()>, + allowed_prefixes: Arc>, + max_document_entries: usize, + peer_ready: tokio::sync::watch::Receiver, + publisher: Publisher, + remote_providers: std::sync::Arc>>>, + router: Router, + sync_generation: tokio::sync::watch::Receiver, + sync_targets: Vec, +} + +struct BoardRuntime { + author: iroh_docs::AuthorId, + blobs: FsStore, + docs: Docs, + downloader: Downloader, + endpoint: Endpoint, + gossip: Gossip, +} + +struct BoardEvents { + error: tokio::sync::watch::Receiver>, + peer_ready: tokio::sync::watch::Receiver, + remote_providers: Arc>>>, + sync_generation: tokio::sync::watch::Receiver, + task: tokio::task::JoinHandle<()>, +} + +impl Drop for BoardNode { + fn drop(&mut self) { + self.event_task.abort(); + } +} + +impl BoardNode { + /// Creates a new ceremony document and returns one scoped ticket per participant. + pub(super) async fn create( + data_directory: &Path, + participant_count: usize, + ) -> anyhow::Result<(Self, Vec)> { + Self::create_with_network(data_directory, participant_count, true).await + } + + pub(super) async fn create_with_network( + data_directory: &Path, + participant_count: usize, + use_network_services: bool, + ) -> anyhow::Result<(Self, Vec)> { + let runtime = BoardRuntime::start(data_directory, use_network_services).await?; + let document_id_path = data_directory.join(DOCUMENT_ID_FILE); + let existing_document = document_id_path.exists(); + let document = if existing_document { + require_current_board_format(data_directory)?; + let id = fs_err::read_to_string(&document_id_path).with_context(|| { + format!("failed to read Iroh document ID {}", document_id_path.display()) + })?; + let id = decode_fixed_hex::<32>(id.trim(), "Iroh document ID")?; + runtime + .docs + .open(iroh_docs::NamespaceId::from(&id)) + .await + .context("failed to open Iroh document")? + .context("persisted Iroh document is missing")? + } else { + let document = runtime.docs.create().await.context("failed to create Iroh document")?; + write_new_file( + &document_id_path, + hex::encode(document.id().to_bytes()).as_bytes(), + true, + )?; + write_new_file(&data_directory.join(BOARD_FORMAT_FILE), BOARD_FORMAT, true)?; + document + }; + let upload_secrets = + load_or_create_upload_secrets(data_directory, participant_count, !existing_document)?; + document + .set_download_policy(DownloadPolicy::NothingExcept(Vec::new())) + .await + .context("failed to restrict DKG board downloads")?; + let mut document_ticket = document + .share( + ShareMode::Read, + if use_network_services { + AddrInfoOptions::RelayAndAddresses + } else { + AddrInfoOptions::Id + }, + ) + .await + .context("failed to create Iroh document ticket")?; + if !use_network_services { + let mut socket = runtime + .endpoint + .bound_sockets() + .into_iter() + .find(std::net::SocketAddr::is_ipv4) + .context("Iroh test endpoint has no IPv4 socket")?; + socket.set_ip(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)); + document_ticket.nodes = vec![iroh::EndpointAddr::from_parts( + runtime.endpoint.id(), + [iroh::TransportAddr::Ip(socket)], + )]; + } + let tickets = upload_secrets + .iter() + .enumerate() + .map(|(position, upload_secret)| { + Ok(BoardTicket { + document: document_ticket.clone(), + participant: u32::try_from(position + 1) + .context("too many DKG participants")?, + upload_secret: *upload_secret, + }) + }) + .collect::>>()?; + let board = runtime + .attach(document, participant_count, Vec::new(), Some(upload_secrets), None) + .await?; + board + .document + .start_sync(Vec::new()) + .await + .context("failed to start DKG board synchronization")?; + Ok((board, tickets)) + } + + /// Joins an existing ceremony document through its read and upload ticket. + pub(super) async fn join( + data_directory: &Path, + ticket: &str, + participant_count: usize, + ) -> anyhow::Result { + Self::join_with_network(data_directory, ticket, participant_count, true).await + } + + pub(super) async fn join_with_network( + data_directory: &Path, + ticket: &str, + participant_count: usize, + use_network_services: bool, + ) -> anyhow::Result { + let ticket = BoardTicket::from_str(ticket)?; + let runtime = BoardRuntime::start(data_directory, use_network_services).await?; + let BoardTicket { document, participant, upload_secret } = ticket; + ensure!( + usize::try_from(participant).context("participant index does not fit usize")? + <= participant_count, + "DKG board ticket names an unknown participant" + ); + let DocTicket { capability, nodes } = document; + let target = nodes.first().cloned().context("DKG board ticket has no endpoint")?; + let document = runtime + .docs + .import_namespace(capability) + .await + .context("failed to join Iroh ceremony document")?; + document + .set_download_policy(DownloadPolicy::NothingExcept(Vec::new())) + .await + .context("failed to restrict DKG board downloads")?; + let mut board = runtime + .attach( + document, + participant_count, + nodes.clone(), + None, + Some((target, participant, upload_secret)), + ) + .await?; + board + .document + .start_sync(nodes) + .await + .context("failed to start DKG board synchronization")?; + board.wait_for_peer().await?; + Ok(board) + } + + /// Publishes one artifact without replacing another value in the same slot. + pub(super) async fn publish(&self, slot: &ArtifactSlot, value: &[u8]) -> anyhow::Result { + self.ensure_admitted()?; + validate_artifact_length(value.len())?; + let expected_hash = Hash::new(value); + let sync_generation = *self.sync_generation.borrow(); + let stored_hash = match &self.publisher { + Publisher::Local(writer) => writer.store(slot, value).await?, + Publisher::Remote { + endpoint, + participant, + target, + upload_secret, + } => { + upload_artifact(endpoint, target, *participant, upload_secret, slot, value).await? + }, + }; + ensure!(stored_hash == expected_hash, "Iroh stored artifact under an unexpected hash"); + self.document + .start_sync(self.sync_targets.clone()) + .await + .context("failed to synchronize DKG board artifact")?; + if !self.sync_targets.is_empty() || *self.peer_ready.borrow() { + let mut completed = self.sync_generation.clone(); + tokio::time::timeout( + PEER_READY_TIMEOUT, + completed.wait_for(|generation| *generation > sync_generation), + ) + .await + .context("timed out synchronizing DKG board artifact")? + .context("DKG board synchronization monitor stopped")?; + } + Ok(stored_hash) + } + + /// Reads the unique content value published for one artifact slot. + pub(super) async fn read_unique(&self, slot: &ArtifactSlot) -> anyhow::Result>> { + self.validate_document_metadata().await?; + let prefix = slot.prefix(); + let entries = self + .document + .get_many(Query::key_prefix(prefix.as_bytes())) + .await + .context("failed to query DKG board artifacts")?; + futures::pin_mut!(entries); + let mut values = BTreeMap::new(); + while let Some(entry) = entries.next().await { + let entry = entry.context("failed to read DKG board entry")?; + ensure!( + entry.content_len() > 0 && entry.content_len() <= MAX_ARTIFACT_BYTES, + "DKG board artifact exceeds {MAX_ARTIFACT_BYTES} bytes", + ); + let expected_key = slot.key(entry.content_hash()); + ensure!( + entry.key() == expected_key.as_bytes(), + "DKG board key does not match its content hash" + ); + let hash = entry.content_hash(); + if self.blobs.blobs().get_bytes(hash).await.is_err() { + let mut providers = + self.remote_providers.read().await.get(&hash).cloned().unwrap_or_default(); + let sync_peers = self + .document + .get_sync_peers() + .await + .context("failed to list DKG board peers")? + .unwrap_or_default() + .into_iter() + .map(|id| EndpointId::from_bytes(&id).context("invalid DKG board peer ID")) + .collect::>>()?; + for peer in sync_peers { + if !providers.contains(&peer) { + providers.push(peer); + } + } + if providers.is_empty() { + return Ok(None); + } + let Ok(mut progress) = self.downloader.download(hash, providers).stream().await + else { + return Ok(None); + }; + while let Some(item) = progress.next().await { + match item { + DownloadProgressItem::Progress(downloaded) => ensure!( + downloaded <= MAX_ARTIFACT_BYTES, + "DKG board artifact exceeds {MAX_ARTIFACT_BYTES} bytes", + ), + DownloadProgressItem::Error(_) | DownloadProgressItem::DownloadError => { + return Ok(None); + }, + DownloadProgressItem::TryProvider { .. } + | DownloadProgressItem::ProviderFailed { .. } + | DownloadProgressItem::PartComplete { .. } => {}, + } + } + } + let bytes = self + .blobs + .blobs() + .get_bytes(hash) + .await + .context("downloaded DKG board artifact is missing")?; + ensure!( + u64::try_from(bytes.len()).context("artifact length does not fit u64")? + == entry.content_len(), + "DKG board artifact length does not match its entry" + ); + values.entry(hash).or_insert_with(|| bytes.to_vec()); + } + ensure!(values.len() <= 1, "DKG board contains conflicting artifacts for {prefix}"); + Ok(values.into_values().next()) + } + + async fn validate_document_metadata(&self) -> anyhow::Result<()> { + self.ensure_admitted()?; + inspect_document_metadata(&self.document, &self.allowed_prefixes, self.max_document_entries) + .await + } + + fn ensure_admitted(&self) -> anyhow::Result<()> { + if let Some(error) = self.event_error.borrow().as_ref() { + anyhow::bail!("DKG board synchronization stopped: {error}"); + } + Ok(()) + } + + async fn wait_for_peer(&mut self) -> anyhow::Result<()> { + if *self.peer_ready.borrow() { + return Ok(()); + } + tokio::time::timeout(PEER_READY_TIMEOUT, self.peer_ready.wait_for(|ready| *ready)) + .await + .context("timed out waiting for the DKG board peer")? + .context("DKG board peer monitor stopped")?; + Ok(()) + } + + /// Waits until one unique artifact has synchronized locally. + pub(super) async fn wait_unique( + &self, + slot: &ArtifactSlot, + timeout: Duration, + ) -> anyhow::Result> { + let mut events = self + .document + .subscribe() + .await + .context("failed to subscribe to DKG board updates")?; + tokio::time::timeout(timeout, async { + loop { + if let Some(value) = self.read_unique(slot).await? { + return Ok(value); + } + tokio::select! { + event = events.next() => { + event.transpose()?.context("DKG board update stream ended")?; + }, + () = tokio::time::sleep(Duration::from_millis(250)) => {}, + } + } + }) + .await + .with_context(|| format!("timed out waiting for DKG board slot {}", slot.prefix()))? + } + + /// Stops the board node and flushes its persistent stores. + pub(super) async fn shutdown(self) -> anyhow::Result<()> { + self.event_task.abort(); + self.router.shutdown().await.context("failed to stop Iroh board node")?; + Ok(()) + } +} + +fn validate_artifact_length(length: usize) -> anyhow::Result<()> { + ensure!(length > 0, "DKG board artifact must not be empty"); + ensure!( + u64::try_from(length).context("artifact length does not fit u64")? <= MAX_ARTIFACT_BYTES, + "DKG board artifact exceeds {MAX_ARTIFACT_BYTES} bytes", + ); + Ok(()) +} + +impl BoardWriter { + fn validate_slot(&self, slot: &ArtifactSlot) -> anyhow::Result<()> { + ensure!( + self.allowed_prefixes.contains(&slot.prefix()), + "DKG board upload targets an unknown participant or artifact slot" + ); + Ok(()) + } + + async fn store(&self, slot: &ArtifactSlot, value: &[u8]) -> anyhow::Result { + validate_artifact_length(value.len())?; + self.validate_slot(slot)?; + let prefix = slot.prefix(); + let expected_hash = Hash::new(value); + let _guard = self.lock.lock().await; + let entries = self + .document + .get_many(Query::key_prefix(prefix.as_bytes())) + .await + .context("failed to inspect DKG board artifact slot")?; + futures::pin_mut!(entries); + let mut hashes = Vec::new(); + while let Some(entry) = entries.next().await { + let entry = entry.context("failed to read DKG board artifact slot")?; + if entry.content_hash() == expected_hash { + return Ok(expected_hash); + } + hashes.push(entry.content_hash()); + ensure!( + hashes.len() < MAX_VALUES_PER_SLOT, + "DKG board artifact slot already contains conflicting values" + ); + } + + let stored_hash = self + .document + .set_bytes(self.author, slot.key(expected_hash), value.to_vec()) + .await + .context("failed to publish DKG board artifact")?; + ensure!(stored_hash == expected_hash, "Iroh stored artifact under an unexpected hash"); + Ok(stored_hash) + } +} + +impl UploadProtocol { + async fn receive(&self, recv: &mut iroh::endpoint::RecvStream) -> anyhow::Result { + let mut header = [0u8; UPLOAD_HEADER_BYTES]; + recv.read_exact(&mut header) + .await + .context("failed to read DKG board upload header")?; + let kind = header[32]; + let participant = u32::from_be_bytes(header[33..37].try_into().expect("fixed slice")); + let secret_position = usize::try_from(participant) + .context("participant index does not fit usize")? + .checked_sub(1) + .context("DKG board participant index must be nonzero")?; + let expected_secret = self + .upload_secrets + .get(secret_position) + .context("DKG board upload targets an unknown participant")?; + ensure!( + secrets_match(&header[..32], expected_secret), + "DKG board ticket does not authorize this participant" + ); + let length = u64::from_be_bytes(header[37..45].try_into().expect("fixed slice")); + ensure!( + length > 0 && length <= MAX_ARTIFACT_BYTES, + "DKG board artifact exceeds {MAX_ARTIFACT_BYTES} bytes" + ); + let slot = ArtifactSlot::from_upload_fields(kind, participant)?; + self.writer.validate_slot(&slot)?; + let length = usize::try_from(length).context("DKG board artifact length is too large")?; + let mut value = vec![0u8; length]; + recv.read_exact(&mut value) + .await + .context("failed to read DKG board upload body")?; + recv.read_to_end(0).await.context("DKG board upload has trailing bytes")?; + self.writer.store(&slot, &value).await + } +} + +impl ProtocolHandler for UploadProtocol { + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + if let Ok(result) = + tokio::time::timeout(UPLOAD_TIMEOUT, self.serve_connection(&connection)).await + { + result + } else { + connection.close(1u32.into(), b"DKG board upload timed out"); + Ok(()) + } + } +} + +impl UploadProtocol { + async fn serve_connection(&self, connection: &Connection) -> Result<(), AcceptError> { + let _permit = self.permits.acquire().await.map_err(AcceptError::from_err)?; + let (mut send, mut recv) = connection.accept_bi().await?; + let response = match self.receive(&mut recv).await { + Ok(hash) => { + let mut response = Vec::with_capacity(UPLOAD_RESPONSE_BYTES); + response.push(0); + response.extend_from_slice(hash.as_bytes()); + response + }, + Err(error) => upload_error_response(&error), + }; + send.write_all(&response).await.map_err(AcceptError::from_err)?; + send.finish()?; + connection.closed().await; + Ok(()) + } +} + +async fn upload_artifact( + endpoint: &Endpoint, + target: &EndpointAddr, + authorized_participant: u32, + upload_secret: &[u8; 32], + slot: &ArtifactSlot, + value: &[u8], +) -> anyhow::Result { + validate_artifact_length(value.len())?; + let (kind, participant) = slot.upload_fields()?; + ensure!( + participant == authorized_participant, + "DKG board ticket does not authorize participant {participant}" + ); + upload_artifact_request( + endpoint, + target, + upload_secret, + kind, + participant, + u64::try_from(value.len()).context("artifact length does not fit u64")?, + value, + ) + .await +} + +async fn upload_artifact_request( + endpoint: &Endpoint, + target: &EndpointAddr, + upload_secret: &[u8; 32], + kind: u8, + participant: u32, + declared_length: u64, + value: &[u8], +) -> anyhow::Result { + let connection = endpoint + .connect(target.clone(), UPLOAD_ALPN) + .await + .context("failed to connect to the DKG board upload service")?; + let (mut send, mut recv) = + connection.open_bi().await.context("failed to open a DKG board upload stream")?; + let mut header = [0u8; UPLOAD_HEADER_BYTES]; + header[..32].copy_from_slice(upload_secret); + header[32] = kind; + header[33..37].copy_from_slice(&participant.to_be_bytes()); + header[37..45].copy_from_slice(&declared_length.to_be_bytes()); + send.write_all(&header) + .await + .context("failed to write DKG board upload header")?; + send.write_all(value).await.context("failed to write DKG board upload body")?; + send.finish().context("failed to finish DKG board upload")?; + let response = tokio::time::timeout( + UPLOAD_TIMEOUT, + recv.read_to_end(UPLOAD_RESPONSE_BYTES + MAX_UPLOAD_ERROR_BYTES), + ) + .await + .context("timed out waiting for the DKG board upload response")? + .context("failed to read DKG board upload response")?; + connection.close(0u32.into(), b"upload complete"); + ensure!(!response.is_empty(), "DKG board returned an empty upload response"); + if response[0] != 0 { + let message = std::str::from_utf8(&response[1..]) + .context("DKG board returned a non-UTF-8 upload error")?; + anyhow::bail!("DKG board rejected the artifact: {message}"); + } + ensure!( + response.len() == UPLOAD_RESPONSE_BYTES, + "DKG board returned an invalid upload response" + ); + Ok(Hash::from_bytes(response[1..].try_into().expect("validated response length"))) +} + +fn upload_error_response(error: &anyhow::Error) -> Vec { + let mut message = format!("{error:#}"); + if message.len() > MAX_UPLOAD_ERROR_BYTES { + let mut end = MAX_UPLOAD_ERROR_BYTES; + while !message.is_char_boundary(end) { + end -= 1; + } + message.truncate(end); + } + let mut response = Vec::with_capacity(1 + message.len()); + response.push(1); + response.extend_from_slice(message.as_bytes()); + response +} + +fn secrets_match(candidate: &[u8], expected: &[u8; 32]) -> bool { + candidate + .iter() + .zip(expected) + .fold(0u8, |difference, (left, right)| difference | (left ^ right)) + == 0 +} + +impl BoardRuntime { + async fn start(data_directory: &Path, use_network_services: bool) -> anyhow::Result { + fs_err::create_dir_all(data_directory).with_context(|| { + format!("failed to create Iroh data directory {}", data_directory.display()) + })?; + let secret = load_or_create_endpoint_secret(data_directory)?; + let builder = if use_network_services { + Endpoint::builder(presets::N0) + } else { + Endpoint::builder(presets::Minimal) + }; + let endpoint = builder + .secret_key(secret) + .bind() + .await + .context("failed to bind Iroh endpoint")?; + let blobs_directory = data_directory.join("blobs"); + let docs_directory = data_directory.join("docs"); + fs_err::create_dir_all(&blobs_directory).context("failed to create Iroh blob directory")?; + fs_err::create_dir_all(&docs_directory) + .context("failed to create Iroh document directory")?; + let blobs = + FsStore::load(blobs_directory).await.context("failed to load Iroh blob store")?; + let downloader = blobs.downloader(&endpoint); + let gossip = Gossip::builder().spawn(endpoint.clone()); + let docs = Docs::persistent(docs_directory) + .spawn(endpoint.clone(), blobs.as_ref().clone(), gossip.clone()) + .await + .context("failed to load Iroh document store")?; + let author = docs.author_default().await.context("failed to load Iroh author")?; + Ok(Self { + author, + blobs, + docs, + downloader, + endpoint, + gossip, + }) + } + + async fn attach( + self, + document: Doc, + participant_count: usize, + sync_targets: Vec, + served_upload_secrets: Option>, + remote_upload: Option<(EndpointAddr, u32, [u8; 32])>, + ) -> anyhow::Result { + ensure!(participant_count > 0, "DKG board requires at least one participant"); + ensure!( + served_upload_secrets.is_some() ^ remote_upload.is_some(), + "DKG board must either serve or submit uploads" + ); + let artifact_slot_count = participant_count + .checked_mul(ARTIFACTS_PER_PARTICIPANT) + .and_then(|count| count.checked_add(COMMON_ARTIFACT_COUNT)) + .context("DKG board participant count is too large")?; + let max_document_entries = artifact_slot_count + .checked_mul(MAX_VALUES_PER_SLOT) + .context("DKG board participant count is too large")?; + let allowed_prefixes = Arc::new(allowed_slot_prefixes(participant_count)?); + inspect_document_metadata(&document, &allowed_prefixes, max_document_entries).await?; + let writer = BoardWriter { + author: self.author, + document: document.clone(), + allowed_prefixes: allowed_prefixes.clone(), + lock: Arc::new(tokio::sync::Mutex::new(())), + }; + let publisher = match remote_upload { + Some((target, participant, upload_secret)) => Publisher::Remote { + endpoint: self.endpoint.clone(), + participant, + target, + upload_secret, + }, + None => Publisher::Local(writer.clone()), + }; + let mut router = Router::builder(self.endpoint) + .accept(iroh_blobs::ALPN, BlobsProtocol::new(self.blobs.as_ref(), None)) + .accept(iroh_gossip::ALPN, self.gossip) + .accept(iroh_docs::ALPN, self.docs.clone()); + if let Some(upload_secrets) = served_upload_secrets { + ensure!( + upload_secrets.len() == participant_count, + "DKG board requires one upload secret per participant" + ); + router = router.accept( + UPLOAD_ALPN, + UploadProtocol { + permits: Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_UPLOADS)), + upload_secrets: Arc::new(upload_secrets), + writer, + }, + ); + } + let router = router.spawn(); + let events = BoardEvents::start(&document).await?; + Ok(BoardNode { + blobs: self.blobs, + document, + downloader: self.downloader, + event_error: events.error, + event_task: events.task, + allowed_prefixes, + max_document_entries, + peer_ready: events.peer_ready, + publisher, + remote_providers: events.remote_providers, + router, + sync_generation: events.sync_generation, + sync_targets, + }) + } +} + +impl BoardEvents { + async fn start(document: &Doc) -> anyhow::Result { + let mut events = + document.subscribe().await.context("failed to start DKG board event monitor")?; + let (event_tx, error) = tokio::sync::watch::channel(None); + let (peer_ready_tx, peer_ready) = tokio::sync::watch::channel(false); + let (sync_generation_tx, sync_generation) = tokio::sync::watch::channel(0u64); + let remote_providers = + Arc::new(tokio::sync::RwLock::>>::default()); + let monitored_providers = remote_providers.clone(); + let task = tokio::spawn(async move { + let mut neighbor_ready = false; + let mut sync_ready = false; + while let Some(event) = events.next().await { + let event = match event { + Ok(event) => event, + Err(error) => { + event_tx.send_replace(Some(error.to_string())); + break; + }, + }; + match &event { + LiveEvent::NeighborUp(_) => { + neighbor_ready = true; + if sync_ready { + peer_ready_tx.send_replace(true); + } + }, + LiveEvent::NeighborDown(_) => { + neighbor_ready = false; + sync_ready = false; + peer_ready_tx.send_replace(false); + }, + LiveEvent::SyncFinished(sync) if sync.result.is_ok() => { + sync_ready = true; + sync_generation_tx.send_modify(|generation| *generation += 1); + if neighbor_ready { + peer_ready_tx.send_replace(true); + } + }, + _ => {}, + } + if let LiveEvent::InsertRemote { from, entry, .. } = &event { + let mut providers = monitored_providers.write().await; + let providers = providers.entry(entry.content_hash()).or_default(); + if !providers.contains(from) { + providers.push(*from); + } + } + } + }); + Ok(Self { + error, + peer_ready, + remote_providers, + sync_generation, + task, + }) + } +} + +fn allowed_slot_prefixes(participant_count: usize) -> anyhow::Result> { + let mut prefixes = vec![ + ArtifactSlot::Manifest.prefix(), + ArtifactSlot::DecryptionConfig.prefix(), + ArtifactSlot::ContextConfig.prefix(), + ]; + for position in 0..participant_count { + let participant = u32::try_from(position + 1).context("too many DKG participants")?; + prefixes.extend([ + ArtifactSlot::Registration(participant).prefix(), + ArtifactSlot::DecryptionDealing(participant).prefix(), + ArtifactSlot::ContextDealing(participant).prefix(), + ArtifactSlot::TranscriptAcceptance(participant).prefix(), + ]); + } + Ok(prefixes) +} + +async fn inspect_document_metadata( + document: &Doc, + allowed_prefixes: &[String], + max_document_entries: usize, +) -> anyhow::Result<()> { + let entries = document.get_many(Query::all()).await.context("failed to inspect DKG board")?; + futures::pin_mut!(entries); + let mut slots = BTreeMap::new(); + let mut count = 0usize; + while let Some(entry) = entries.next().await { + let entry = entry.context("failed to read DKG board entry")?; + count += 1; + ensure!(count <= max_document_entries, "DKG board contains too many entries"); + ensure!( + entry.content_len() > 0 && entry.content_len() <= MAX_ARTIFACT_BYTES, + "DKG board artifact exceeds {MAX_ARTIFACT_BYTES} bytes", + ); + let key = std::str::from_utf8(entry.key()).context("DKG board key is not UTF-8")?; + let (prefix, hash) = allowed_prefixes + .iter() + .find_map(|prefix| key.strip_prefix(prefix).map(|hash| (prefix, hash))) + .context("DKG board contains an unrecognized artifact slot")?; + ensure!( + hash.len() == 64 && hash.bytes().all(|byte| byte.is_ascii_hexdigit()), + "DKG board key has an invalid content hash" + ); + if let Some(previous) = slots.insert(prefix.clone(), hash.to_owned()) { + ensure!(previous == hash, "DKG board contains conflicting artifacts for {prefix}"); + } + } + Ok(()) +} + +fn load_or_create_endpoint_secret(data_directory: &Path) -> anyhow::Result { + let path = data_directory.join(ENDPOINT_SECRET_FILE); + if path.exists() { + let bytes = fs_err::read_to_string(&path) + .with_context(|| format!("failed to read Iroh endpoint secret {}", path.display()))?; + let bytes = decode_fixed_hex::<32>(bytes.trim(), "Iroh endpoint secret")?; + return Ok(SecretKey::from_bytes(&bytes)); + } + + let secret = SecretKey::generate(); + write_new_file(&path, hex::encode(secret.to_bytes()).as_bytes(), true)?; + Ok(secret) +} + +fn load_or_create_upload_secrets( + data_directory: &Path, + participant_count: usize, + allow_create: bool, +) -> anyhow::Result> { + let path = data_directory.join(UPLOAD_SECRETS_DIRECTORY); + if path.exists() { + let entry_count = fs_err::read_dir(&path) + .with_context(|| format!("failed to read DKG board upload secrets {}", path.display()))? + .collect::, _>>()? + .len(); + ensure!( + entry_count == participant_count, + "DKG board upload secret count does not match the participant count" + ); + return (1..=participant_count) + .map(|participant| { + let secret_path = path.join(format!("participant-{participant}.hex")); + let bytes = fs_err::read_to_string(&secret_path).with_context(|| { + format!("failed to read DKG board upload secret {}", secret_path.display()) + })?; + decode_fixed_hex::<32>(bytes.trim(), "DKG board upload secret") + }) + .collect(); + } + ensure!( + allow_create, + "this DKG board predates participant-scoped uploads; start a new ceremony in a new data directory" + ); + + let secrets = (0..participant_count) + .map(|_| SecretKey::generate().to_bytes()) + .collect::>(); + publish_directory(&path, |temporary| { + for (position, secret) in secrets.iter().enumerate() { + write_new_file( + &temporary.join(format!("participant-{}.hex", position + 1)), + hex::encode(secret).as_bytes(), + true, + )?; + } + Ok(()) + })?; + Ok(secrets) +} + +fn require_current_board_format(data_directory: &Path) -> anyhow::Result<()> { + let path = data_directory.join(BOARD_FORMAT_FILE); + let format = fs_err::read(&path).with_context(|| { + format!( + "this DKG board predates participant-scoped uploads; start a new ceremony in a new data directory ({})", + path.display() + ) + })?; + ensure!( + format == BOARD_FORMAT, + "unsupported DKG board format; start a new ceremony in a new data directory" + ); + Ok(()) +} + +#[cfg(test)] +mod tests; diff --git a/bin/validator/src/commands/dkg/board/tests.rs b/bin/validator/src/commands/dkg/board/tests.rs new file mode 100644 index 0000000000..dd7ff8abd7 --- /dev/null +++ b/bin/validator/src/commands/dkg/board/tests.rs @@ -0,0 +1,266 @@ +use super::*; + +impl BoardNode { + async fn create_for_test(data_directory: &Path) -> anyhow::Result<(Self, Vec)> { + Self::create_with_network(data_directory, 3, false).await + } + + async fn join_for_test(data_directory: &Path, ticket: BoardTicket) -> anyhow::Result { + Self::join_with_network(data_directory, &ticket.to_string(), 3, false).await + } + + fn local_writer_for_test(&self) -> &BoardWriter { + match &self.publisher { + Publisher::Local(writer) => writer, + Publisher::Remote { .. } => panic!("expected local DKG board writer"), + } + } + + async fn upload_raw_for_test( + &self, + kind: u8, + participant: u32, + declared_length: u64, + value: &[u8], + ) -> anyhow::Result { + match &self.publisher { + Publisher::Remote { endpoint, target, upload_secret, .. } => { + upload_artifact_request( + endpoint, + target, + upload_secret, + kind, + participant, + declared_length, + value, + ) + .await + }, + Publisher::Local(_) => anyhow::bail!("expected remote DKG board publisher"), + } + } + + async fn publish_hash_for_test( + &self, + slot: &ArtifactSlot, + hash: Hash, + size: u64, + ) -> anyhow::Result<()> { + let writer = self.local_writer_for_test(); + self.document + .set_hash(writer.author, slot.key(hash), hash, size) + .await + .context("failed to publish raw test hash") + } +} + +fn ticket_for(tickets: &[BoardTicket], participant: u32) -> BoardTicket { + tickets + .iter() + .find(|ticket| ticket.participant == participant) + .expect("participant ticket must exist") + .clone() +} + +#[tokio::test] +async fn artifact_syncs_between_board_nodes() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let (host, tickets) = BoardNode::create_for_test(&root.path().join("host")).await?; + let ticket = ticket_for(&tickets, 1); + assert!(matches!(ticket.document.capability, iroh_docs::Capability::Read(_))); + let client = BoardNode::join_for_test(&root.path().join("client"), ticket).await?; + let slot = ArtifactSlot::Registration(1); + let value = b"signed registration"; + + client.publish(&slot, value).await?; + assert_eq!(host.wait_unique(&slot, Duration::from_secs(10)).await?, value,); + + client.shutdown().await?; + host.shutdown().await?; + Ok(()) +} + +#[tokio::test] +async fn conflicting_artifacts_are_rejected() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let (host, _) = BoardNode::create_for_test(&root.path().join("host")).await?; + let slot = ArtifactSlot::Manifest; + + host.publish(&slot, b"first").await?; + host.publish(&slot, b"second").await?; + let error = host.read_unique(&slot).await.unwrap_err(); + assert!(error.to_string().contains("conflicting artifacts")); + + host.shutdown().await?; + Ok(()) +} + +#[tokio::test] +async fn board_reopens_the_same_document_after_restart() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let data_directory = root.path().join("host"); + let (host, first_tickets) = BoardNode::create_for_test(&data_directory).await?; + host.publish(&ArtifactSlot::Manifest, b"manifest").await?; + host.shutdown().await?; + + let (host, second_tickets) = BoardNode::create_for_test(&data_directory).await?; + assert_eq!(first_tickets.len(), second_tickets.len()); + for (first, second) in first_tickets.iter().zip(&second_tickets) { + assert_eq!(first.participant, second.participant); + assert_eq!(first.document.capability.id(), second.document.capability.id()); + assert_eq!(first.upload_secret, second.upload_secret); + } + assert_eq!(host.read_unique(&ArtifactSlot::Manifest).await?, Some(b"manifest".to_vec())); + + host.shutdown().await?; + Ok(()) +} + +#[tokio::test] +async fn unmarked_board_is_not_reopened_even_with_upload_secrets() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let data_directory = root.path().join("host"); + let (host, _) = BoardNode::create_for_test(&data_directory).await?; + host.shutdown().await?; + fs_err::remove_file(data_directory.join(BOARD_FORMAT_FILE))?; + + let error = BoardNode::create_for_test(&data_directory) + .await + .err() + .context("legacy board unexpectedly reopened")?; + assert!(error.to_string().contains("predates participant-scoped uploads")); + Ok(()) +} + +#[tokio::test] +async fn previous_board_format_is_not_reopened() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let data_directory = root.path().join("host"); + let (host, _) = BoardNode::create_for_test(&data_directory).await?; + host.shutdown().await?; + fs_err::write(data_directory.join(BOARD_FORMAT_FILE), b"participant-upload-v2\n")?; + + let error = BoardNode::create_for_test(&data_directory) + .await + .err() + .context("old board format unexpectedly reopened")?; + assert!(error.to_string().contains("unsupported DKG board format")); + Ok(()) +} + +#[test] +fn previous_board_ticket_version_is_rejected() { + let error = BoardTicket::from_str("miden-storage-key-dkg-board-v2:1:00:invalid") + .expect_err("old board ticket unexpectedly parsed"); + assert!(error.to_string().contains("ticket prefix")); +} + +#[tokio::test] +async fn unknown_participants_and_artifact_kinds_are_rejected_before_body_allocation() +-> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let (host, tickets) = BoardNode::create_for_test(&root.path().join("host")).await?; + let ticket = ticket_for(&tickets, 1); + let client = BoardNode::join_for_test(&root.path().join("client"), ticket).await?; + + let error = client.upload_raw_for_test(1, 99, MAX_ARTIFACT_BYTES, &[]).await.unwrap_err(); + assert!(error.to_string().contains("unknown participant")); + let error = client.upload_raw_for_test(255, 1, 16, b"private artifact").await.unwrap_err(); + assert!(error.to_string().contains("unknown artifact kind")); + assert!(host.read_unique(&ArtifactSlot::Registration(1)).await?.is_none()); + + client.shutdown().await?; + host.shutdown().await?; + Ok(()) +} + +#[test] +fn oversized_artifacts_are_rejected_before_allocation() { + let oversized = usize::try_from(MAX_ARTIFACT_BYTES).unwrap() + 1; + assert!(validate_artifact_length(oversized).is_err()); +} + +#[tokio::test] +async fn oversized_upload_is_rejected_before_body_allocation() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let (host, tickets) = BoardNode::create_for_test(&root.path().join("host")).await?; + let ticket = ticket_for(&tickets, 1); + let client = BoardNode::join_for_test(&root.path().join("client"), ticket).await?; + let error = client.upload_raw_for_test(1, 1, MAX_ARTIFACT_BYTES + 1, &[]).await.unwrap_err(); + assert!(error.to_string().contains("exceeds")); + assert!(host.read_unique(&ArtifactSlot::Registration(1)).await?.is_none()); + + client.shutdown().await?; + host.shutdown().await?; + Ok(()) +} + +#[tokio::test] +async fn invalid_upload_secret_is_rejected_before_storage() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let (host, tickets) = BoardNode::create_for_test(&root.path().join("host")).await?; + let mut ticket = ticket_for(&tickets, 1); + ticket.upload_secret[0] ^= 1; + let client = BoardNode::join_for_test(&root.path().join("client"), ticket).await?; + + let error = client + .publish(&ArtifactSlot::Registration(1), b"signed registration") + .await + .unwrap_err(); + assert!(error.to_string().contains("does not authorize this participant")); + assert!(host.read_unique(&ArtifactSlot::Registration(1)).await?.is_none()); + + client.shutdown().await?; + host.shutdown().await?; + Ok(()) +} + +#[tokio::test] +async fn participant_ticket_cannot_publish_another_participants_slot() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let (host, tickets) = BoardNode::create_for_test(&root.path().join("host")).await?; + let first = + BoardNode::join_for_test(&root.path().join("first"), ticket_for(&tickets, 1)).await?; + + let error = first + .upload_raw_for_test( + 1, + 2, + u64::try_from(b"wrong registration".len())?, + b"wrong registration", + ) + .await + .unwrap_err(); + assert!(error.to_string().contains("does not authorize this participant")); + assert!(host.read_unique(&ArtifactSlot::Registration(2)).await?.is_none()); + + let second = + BoardNode::join_for_test(&root.path().join("second"), ticket_for(&tickets, 2)).await?; + second.publish(&ArtifactSlot::Registration(2), b"signed registration").await?; + assert_eq!( + host.wait_unique(&ArtifactSlot::Registration(2), Duration::from_secs(10)) + .await?, + b"signed registration" + ); + + first.shutdown().await?; + second.shutdown().await?; + host.shutdown().await?; + Ok(()) +} + +#[tokio::test] +async fn invalid_download_metadata_is_rejected() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let (host, _) = BoardNode::create_for_test(&root.path().join("host")).await?; + let slot = ArtifactSlot::Manifest; + let value = b"manifest"; + let hash = host.publish(&slot, value).await?; + + host.publish_hash_for_test(&slot, hash, 1).await?; + let error = host.read_unique(&slot).await.unwrap_err(); + assert!(error.to_string().contains("length does not match")); + + host.shutdown().await?; + Ok(()) +} diff --git a/bin/validator/src/commands/dkg/runner.rs b/bin/validator/src/commands/dkg/runner.rs new file mode 100644 index 0000000000..a2e5159d5d --- /dev/null +++ b/bin/validator/src/commands/dkg/runner.rs @@ -0,0 +1,616 @@ +use std::num::NonZeroUsize; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use anyhow::{Context, ensure}; +use golden_core::{EvrfProofBackend, ParticipantIndex}; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey; +use miden_validator::ValidatorSigner; + +use super::board::{ArtifactSlot, BoardNode}; +use super::{ + CONTEXT_CONFIG_FILE, + CONTEXT_DEALING_FILE, + DECRYPTION_CONFIG_FILE, + DECRYPTION_DEALING_FILE, + GoldenGroup, + IDENTITY_SECRET_FILE, + MANIFEST_FILE, + OsRng, + PRIVATE_STATE_FILE, + REGISTRATION_FILE, + Registration, + SecpSecqBackend, + Serializable, + StorageGroup, + TRANSCRIPT_ACCEPTANCE_FILE, + TRANSCRIPT_FILE, + ValidatorSigningKey, + Zeroizing, + accept_transcript, + deal, + decode_fixed_hex, + decode_identity_secret, + finalize, + generate_identity, + prepare, + publish_directory, + read_ceremony, + read_registration, + read_trusted_genesis, + read_validated_registrations, + validate_bundle, + write_new_file, +}; + +const IDENTITY_DIRECTORY: &str = "identity"; +const REGISTRATIONS_DIRECTORY: &str = "registrations"; +const CEREMONY_DIRECTORY: &str = "ceremony"; +const DEALINGS_DIRECTORY: &str = "dealings"; +const PUBLIC_DEALINGS_DIRECTORY: &str = "public-dealings"; +const ACCEPTANCE_DIRECTORY: &str = "acceptance"; +const PUBLIC_ACCEPTANCES_DIRECTORY: &str = "public-acceptances"; +const BOARD_DIRECTORY: &str = "board"; +const CEREMONY_WAIT_TIMEOUT: Duration = Duration::from_hours(24); +/// Inputs for the shared storage key DKG board. +#[derive(clap::Args)] +pub(super) struct DkgBoardServeOptions { + /// Durable directory for the Iroh endpoint, document, and common ceremony files. + #[arg(long, value_name = "DIR")] + data_directory: PathBuf, + + /// Trusted genesis block for the network. + #[arg(long, value_name = "FILE")] + genesis: PathBuf, + + /// Number of shares needed to decrypt a private record. + #[arg(long, value_name = "NUM")] + threshold: NonZeroUsize, + + /// Hex-encoded 32-byte storage-key epoch. + #[arg(long, value_name = "HEX")] + epoch: String, + + /// New private directory that receives one board ticket per genesis validator. + #[arg(long, value_name = "DIR")] + ticket_directory: PathBuf, +} + +/// Inputs for one validator's automatic storage key DKG ceremony runner. +#[derive(clap::Args)] +pub(super) struct DkgRunOptions { + /// Private file containing the read and upload board ticket. + #[arg(long, value_name = "FILE")] + board_file: PathBuf, + + /// Trusted genesis block for the network. + #[arg(long, value_name = "FILE")] + genesis: PathBuf, + + /// Expected number of shares needed to decrypt a private record. + #[arg(long, value_name = "NUM")] + threshold: NonZeroUsize, + + /// Expected hex-encoded 32-byte storage-key epoch. + #[arg(long, value_name = "HEX")] + epoch: String, + + /// Validator signing key committed by genesis. + #[command(flatten)] + signing_key: ValidatorSigningKey, + + /// Durable directory for private ceremony state and the local Iroh endpoint. + #[arg(long, value_name = "DIR")] + work_directory: PathBuf, + + /// New directory that receives the final storage-key bundle. + #[arg(long, value_name = "DIR")] + output_directory: PathBuf, +} + +/// Runs one validator through every DKG phase. +pub(super) async fn run_validator(options: DkgRunOptions) -> anyhow::Result<()> { + let board = fs_err::read_to_string(&options.board_file) + .with_context(|| { + format!("failed to read storage key DKG board ticket {}", options.board_file.display()) + })? + .trim() + .to_owned(); + ensure!(!board.is_empty(), "storage key DKG board ticket must not be empty"); + let signer = options.signing_key.into_signer().await?; + run_validator_with_network::( + &board, + &options.genesis, + &signer, + options.threshold.get(), + &options.epoch, + &options.work_directory, + &options.output_directory, + true, + CEREMONY_WAIT_TIMEOUT, + ) + .await +} + +pub(super) async fn serve_board(options: DkgBoardServeOptions) -> anyhow::Result<()> { + let genesis = read_trusted_genesis(&options.genesis)?; + let participant_count = genesis.inner().header().validator_keys().as_keys().len(); + let (board, tickets) = BoardNode::create(&options.data_directory, participant_count).await?; + publish_directory(&options.ticket_directory, |temporary| { + for ticket in &tickets { + write_new_file( + &temporary.join(format!("participant-{}.ticket", ticket.participant)), + ticket.to_string().as_bytes(), + true, + )?; + } + Ok(()) + })?; + println!( + "storage key DKG board tickets written to {}", + options.ticket_directory.display() + ); + + let result = async { + coordinate_common_files( + &board, + &options.data_directory, + &options.genesis, + options.threshold.get(), + &options.epoch, + CEREMONY_WAIT_TIMEOUT, + ) + .await?; + println!("storage key DKG board is ready. Press Ctrl-C to stop it."); + tokio::signal::ctrl_c().await.context("failed to wait for Ctrl-C") + } + .await; + let shutdown = board.shutdown().await; + result.and(shutdown) +} + +/// Waits for signed registrations, prepares the ceremony, and publishes its common files. +pub(super) async fn coordinate_common_files( + board: &BoardNode, + data_directory: &Path, + genesis_path: &Path, + threshold: usize, + epoch: &str, + timeout: Duration, +) -> anyhow::Result<()> { + let genesis = read_trusted_genesis(genesis_path)?; + let validator_keys = genesis.inner().header().validator_keys().as_keys(); + ensure!( + threshold > 0 && threshold <= validator_keys.len(), + "threshold must be between 1 and {}", + validator_keys.len(), + ); + decode_fixed_hex::<32>(epoch, "storage-key epoch")?; + + let ceremony_directory = data_directory.join(CEREMONY_DIRECTORY); + if !ceremony_directory.exists() { + let registrations = wait_for_registrations(board, validator_keys, timeout).await?; + let registration_directory = data_directory.join(REGISTRATIONS_DIRECTORY); + materialize_or_compare(®istration_directory, ®istrations)?; + let paths = registrations + .iter() + .map(|(name, _)| registration_directory.join(name)) + .collect::>(); + prepare(genesis_path, threshold, epoch, &paths, &ceremony_directory)?; + } + + let ceremony = read_ceremony(genesis_path, &ceremony_directory)?; + ensure!( + ceremony.manifest.threshold == threshold, + "board threshold changed after creation" + ); + ensure!(ceremony.manifest.epoch == epoch, "board epoch changed after creation"); + publish_named_file(board, &ArtifactSlot::Manifest, &ceremony_directory.join(MANIFEST_FILE)) + .await?; + publish_named_file( + board, + &ArtifactSlot::DecryptionConfig, + &ceremony_directory.join(DECRYPTION_CONFIG_FILE), + ) + .await?; + publish_named_file( + board, + &ArtifactSlot::ContextConfig, + &ceremony_directory.join(CONTEXT_CONFIG_FILE), + ) + .await?; + Ok(()) +} + +async fn wait_for_registrations( + board: &BoardNode, + validator_keys: &[PublicKey], + timeout: Duration, +) -> anyhow::Result)>> { + let mut files = Vec::with_capacity(validator_keys.len()); + for (position, validator_key) in validator_keys.iter().enumerate() { + let participant = participant_at(position)?; + let bytes = board + .wait_unique(&ArtifactSlot::Registration(participant.get()), timeout) + .await?; + let registration: Registration = toml::from_slice(&bytes).with_context(|| { + format!("invalid registration for participant {}", participant.get()) + })?; + ensure!( + registration.validator_public_key == hex::encode(validator_key.to_bytes()), + "registration slot {} belongs to another genesis validator", + participant.get(), + ); + files.push((format!("registration-{}.toml", participant.get()), bytes)); + } + Ok(files) +} + +/// Runs the restartable validator state machine over one board. +#[expect( + clippy::too_many_arguments, + reason = "the inputs separate ceremony policy, durable paths, and test networking" +)] +pub(super) async fn run_validator_with_network( + ticket: &str, + genesis_path: &Path, + signer: &ValidatorSigner, + threshold: usize, + epoch: &str, + work_directory: &Path, + output_directory: &Path, + use_network_services: bool, + timeout: Duration, +) -> anyhow::Result<()> +where + B: EvrfProofBackend, +{ + fs_err::create_dir_all(work_directory).with_context(|| { + format!("failed to create DKG work directory {}", work_directory.display()) + })?; + let genesis = read_trusted_genesis(genesis_path)?; + let validator_keys = genesis.inner().header().validator_keys().as_keys(); + let participant_count = validator_keys.len(); + ensure!( + threshold > 0 && threshold <= participant_count, + "threshold must be between 1 and {participant_count}", + ); + decode_fixed_hex::<32>(epoch, "storage-key epoch")?; + let participant = prepare_local_identity(genesis_path, epoch, signer, work_directory).await?; + let board_directory = work_directory.join(BOARD_DIRECTORY); + let board = if use_network_services { + BoardNode::join(&board_directory, ticket, participant_count).await? + } else { + BoardNode::join_with_network(&board_directory, ticket, participant_count, false).await? + }; + let result = run_validator_on_board::( + &board, + genesis_path, + signer, + participant, + threshold, + epoch, + work_directory, + output_directory, + timeout, + ) + .await; + let shutdown = board.shutdown().await; + result.and(shutdown) +} + +#[expect( + clippy::too_many_arguments, + clippy::too_many_lines, + reason = "the inputs and linear body mirror the ceremony policy and phase order" +)] +async fn run_validator_on_board( + board: &BoardNode, + genesis_path: &Path, + signer: &ValidatorSigner, + participant: ParticipantIndex, + threshold: usize, + epoch: &str, + work_directory: &Path, + output_directory: &Path, + timeout: Duration, +) -> anyhow::Result<()> +where + B: EvrfProofBackend, +{ + let identity_directory = work_directory.join(IDENTITY_DIRECTORY); + publish_named_file( + board, + &ArtifactSlot::Registration(participant.get()), + &identity_directory.join(REGISTRATION_FILE), + ) + .await?; + + let genesis = read_trusted_genesis(genesis_path)?; + let registrations = + wait_for_registrations(board, genesis.inner().header().validator_keys().as_keys(), timeout) + .await?; + let registration_directory = work_directory.join(REGISTRATIONS_DIRECTORY); + materialize_or_compare(®istration_directory, ®istrations)?; + let registration_paths = registrations + .iter() + .map(|(name, _)| registration_directory.join(name)) + .collect::>(); + let ceremony_directory = work_directory.join(CEREMONY_DIRECTORY); + if !ceremony_directory.exists() { + prepare(genesis_path, threshold, epoch, ®istration_paths, &ceremony_directory)?; + } + let common = vec![ + ( + MANIFEST_FILE.to_owned(), + board.wait_unique(&ArtifactSlot::Manifest, timeout).await?, + ), + ( + DECRYPTION_CONFIG_FILE.to_owned(), + board.wait_unique(&ArtifactSlot::DecryptionConfig, timeout).await?, + ), + ( + CONTEXT_CONFIG_FILE.to_owned(), + board.wait_unique(&ArtifactSlot::ContextConfig, timeout).await?, + ), + ]; + materialize_or_compare(&ceremony_directory, &common)?; + let ceremony = read_ceremony(genesis_path, &ceremony_directory)?; + ensure!( + ceremony.manifest.threshold == threshold, + "board threshold does not match the validator's expected threshold" + ); + ensure!( + ceremony.manifest.epoch == epoch, + "board epoch does not match the validator's expected epoch" + ); + ensure!( + ceremony.manifest.participants[participant.get() as usize - 1].validator_public_key + == hex::encode(signer.public_key().to_bytes()), + "validator signing key has the wrong ceremony participant index", + ); + + let dealings_directory = work_directory.join(DEALINGS_DIRECTORY); + if !dealings_directory.exists() { + deal::( + genesis_path, + &ceremony_directory, + &identity_directory.join(IDENTITY_SECRET_FILE), + &dealings_directory, + &mut OsRng, + )?; + } + publish_named_file( + board, + &ArtifactSlot::DecryptionDealing(participant.get()), + &dealings_directory.join(DECRYPTION_DEALING_FILE), + ) + .await?; + publish_named_file( + board, + &ArtifactSlot::ContextDealing(participant.get()), + &dealings_directory.join(CONTEXT_DEALING_FILE), + ) + .await?; + + let participant_count = ceremony.manifest.participants.len(); + let public_dealings_directory = work_directory.join(PUBLIC_DEALINGS_DIRECTORY); + let public_dealings = wait_for_dealings(board, participant_count, timeout).await?; + materialize_or_compare(&public_dealings_directory, &public_dealings)?; + let decryption_dealings = participant_files( + &public_dealings_directory, + "decryption-dealing", + "wire", + participant_count, + )?; + let context_dealings = participant_files( + &public_dealings_directory, + "context-dealing", + "wire", + participant_count, + )?; + + let acceptance_directory = work_directory.join(ACCEPTANCE_DIRECTORY); + if !acceptance_directory.exists() { + accept_transcript::( + genesis_path, + &ceremony_directory, + signer, + &decryption_dealings, + &context_dealings, + &acceptance_directory, + ) + .await?; + } + publish_named_file( + board, + &ArtifactSlot::TranscriptAcceptance(participant.get()), + &acceptance_directory.join(TRANSCRIPT_ACCEPTANCE_FILE), + ) + .await?; + + let acceptances = wait_for_acceptances(board, participant_count, timeout).await?; + let public_acceptances_directory = work_directory.join(PUBLIC_ACCEPTANCES_DIRECTORY); + materialize_or_compare(&public_acceptances_directory, &acceptances)?; + let transcript_path = acceptance_directory.join(TRANSCRIPT_FILE); + let transcript_acceptances = participant_files( + &public_acceptances_directory, + "transcript-acceptance", + "toml", + participant_count, + )?; + + if !output_directory.exists() { + finalize::( + genesis_path, + &ceremony_directory, + &identity_directory.join(IDENTITY_SECRET_FILE), + &dealings_directory.join(PRIVATE_STATE_FILE), + &decryption_dealings, + &context_dealings, + &transcript_path, + &transcript_acceptances, + output_directory, + )?; + } + validate_bundle( + genesis_path, + &ceremony_directory, + &hex::encode(signer.public_key().to_bytes()), + output_directory, + )?; + + println!("storage key DKG completed for participant {}.", participant.get()); + Ok(()) +} + +pub(super) async fn prepare_local_identity( + genesis_path: &Path, + epoch: &str, + signer: &ValidatorSigner, + work_directory: &Path, +) -> anyhow::Result { + let validator_key = signer.public_key(); + let participant = participant_for_validator(genesis_path, &validator_key)?; + let identity_directory = work_directory.join(IDENTITY_DIRECTORY); + if !identity_directory.exists() { + generate_identity(genesis_path, epoch, signer, &identity_directory).await?; + } + validate_local_identity(genesis_path, epoch, &validator_key, &identity_directory)?; + Ok(participant) +} + +fn participant_for_validator( + genesis_path: &Path, + validator_key: &PublicKey, +) -> anyhow::Result { + let genesis = read_trusted_genesis(genesis_path)?; + let position = genesis + .inner() + .header() + .validator_keys() + .as_keys() + .iter() + .position(|key| key == validator_key) + .context("validator signing key is not committed by genesis")?; + participant_at(position) +} + +fn participant_at(position: usize) -> anyhow::Result { + ParticipantIndex::new(u32::try_from(position + 1).context("too many DKG participants")?) + .map_err(Into::into) +} + +fn validate_local_identity( + genesis_path: &Path, + epoch: &str, + validator_key: &PublicKey, + identity_directory: &Path, +) -> anyhow::Result<()> { + let registration_path = identity_directory.join(REGISTRATION_FILE); + let registration = read_registration(®istration_path)?; + ensure!( + registration.validator_public_key == hex::encode(validator_key.to_bytes()), + "stored DKG identity belongs to another validator", + ); + let genesis = read_trusted_genesis(genesis_path)?; + let expected_epoch = decode_fixed_hex::<32>(epoch, "storage-key epoch")?; + read_validated_registrations( + &[registration_path], + genesis.inner().header().commitment(), + &expected_epoch, + )?; + let secret = Zeroizing::new(fs_err::read(identity_directory.join(IDENTITY_SECRET_FILE))?); + let secret = decode_identity_secret(&secret)?; + ensure!( + hex::encode(StorageGroup::encode_element(&StorageGroup::mul_generator(&secret))) + == registration.dkg_identity_public_key, + "stored DKG identity secret does not match its registration", + ); + Ok(()) +} + +async fn wait_for_dealings( + board: &BoardNode, + participant_count: usize, + timeout: Duration, +) -> anyhow::Result)>> { + let mut files = Vec::with_capacity(participant_count * 2); + for position in 0..participant_count { + let participant = participant_at(position)?; + files.push(( + format!("decryption-dealing-{}.wire", participant.get()), + board + .wait_unique(&ArtifactSlot::DecryptionDealing(participant.get()), timeout) + .await?, + )); + files.push(( + format!("context-dealing-{}.wire", participant.get()), + board + .wait_unique(&ArtifactSlot::ContextDealing(participant.get()), timeout) + .await?, + )); + } + Ok(files) +} + +async fn wait_for_acceptances( + board: &BoardNode, + participant_count: usize, + timeout: Duration, +) -> anyhow::Result)>> { + let mut acceptances = Vec::with_capacity(participant_count); + for position in 0..participant_count { + let participant = participant_at(position)?; + acceptances.push(( + format!("transcript-acceptance-{}.toml", participant.get()), + board + .wait_unique(&ArtifactSlot::TranscriptAcceptance(participant.get()), timeout) + .await?, + )); + } + Ok(acceptances) +} + +async fn publish_named_file( + board: &BoardNode, + slot: &ArtifactSlot, + path: &Path, +) -> anyhow::Result<()> { + let bytes = fs_err::read(path) + .with_context(|| format!("failed to read ceremony artifact {}", path.display()))?; + board.publish(slot, &bytes).await?; + Ok(()) +} + +fn materialize_or_compare(directory: &Path, files: &[(String, Vec)]) -> anyhow::Result<()> { + if directory.exists() { + for (name, expected) in files { + let path = directory.join(name); + let actual = fs_err::read(&path).with_context(|| { + format!("failed to read cached board artifact {}", path.display()) + })?; + ensure!(actual == *expected, "cached board artifact {} changed", path.display()); + } + return Ok(()); + } + publish_directory(directory, |temporary| { + for (name, bytes) in files { + write_new_file(&temporary.join(name), bytes, false)?; + } + Ok(()) + }) +} + +fn participant_files( + directory: &Path, + stem: &str, + extension: &str, + participant_count: usize, +) -> anyhow::Result> { + (0..participant_count) + .map(|position| { + Ok(directory.join(format!("{stem}-{}.{}", participant_at(position)?.get(), extension))) + }) + .collect() +} diff --git a/bin/validator/src/commands/dkg/tests.rs b/bin/validator/src/commands/dkg/tests.rs index f871c71ced..9160ef9ffd 100644 --- a/bin/validator/src/commands/dkg/tests.rs +++ b/bin/validator/src/commands/dkg/tests.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use golden_core::wire::from_wire_bytes; use golden_ehtdh1::wire::from_wire_bytes as from_ehtdh1_wire_bytes; use golden_ehtdh1::{ @@ -902,3 +904,134 @@ async fn deal_rejects_unknown_identity_and_existing_output() -> TestResult { ); Ok(()) } +#[tokio::test] +/// Proves a validator can resume from its saved identity after its ceremony process stops. +#[expect( + clippy::too_many_lines, + reason = "the test runs every ceremony phase for three validators" +)] +async fn iroh_ceremony_resumes_after_validator_restart() -> TestResult { + let root = tempfile::tempdir()?; + let genesis = write_genesis(root.path())?; + let board_directory = root.path().join("board"); + let (board, tickets) = + board::BoardNode::create_with_network(&board_directory, 3, false).await?; + let timeout = Duration::from_mins(2); + let restart_checkpoint_timeout = Duration::from_secs(10); + let epoch = "66".repeat(32); + let first_work = root.path().join("work-1"); + fs_err::create_dir(&first_work)?; + let first_signer = ValidatorSigner::new_local(genesis.signing_keys[0].clone()); + + let interrupted = tokio::spawn({ + let genesis_path = genesis.path.clone(); + let first_work = first_work.clone(); + let epoch = epoch.clone(); + async move { + runner::prepare_local_identity(&genesis_path, &epoch, &first_signer, &first_work) + .await?; + std::future::pending::>().await + } + }); + let registration = first_work.join("identity").join(REGISTRATION_FILE); + tokio::time::timeout(restart_checkpoint_timeout, async { + while !registration.exists() { + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await?; + interrupted.abort(); + assert!(interrupted.await.unwrap_err().is_cancelled()); + tokio::time::sleep(Duration::from_millis(100)).await; + + let signers = genesis + .signing_keys + .iter() + .cloned() + .map(ValidatorSigner::new_local) + .collect::>(); + let trusted_genesis = read_trusted_genesis(&genesis.path)?; + let validator_keys = trusted_genesis.inner().header().validator_keys().as_keys(); + let tickets = signers + .iter() + .map(|signer| { + let position = validator_keys + .iter() + .position(|key| *key == signer.public_key()) + .context("test signer is missing from genesis")?; + Ok(tickets[position].to_string()) + }) + .collect::>>()?; + let work_directories = (1..=3) + .map(|participant| root.path().join(format!("work-{participant}"))) + .collect::>(); + let bundle_directories = (1..=3) + .map(|participant| root.path().join(format!("bundle-{participant}"))) + .collect::>(); + let coordinate = runner::coordinate_common_files( + &board, + &board_directory, + &genesis.path, + 2, + &epoch, + timeout, + ); + let first = runner::run_validator_with_network::( + &tickets[0], + &genesis.path, + &signers[0], + 2, + &epoch, + &work_directories[0], + &bundle_directories[0], + false, + timeout, + ); + let second = runner::run_validator_with_network::( + &tickets[1], + &genesis.path, + &signers[1], + 2, + &epoch, + &work_directories[1], + &bundle_directories[1], + false, + timeout, + ); + let third = runner::run_validator_with_network::( + &tickets[2], + &genesis.path, + &signers[2], + 2, + &epoch, + &work_directories[2], + &bundle_directories[2], + false, + timeout, + ); + tokio::try_join!(coordinate, first, second, third)?; + + let shared_setup = fs_err::read(bundle_directories[0].join(SETUP_CONTEXT_FILE))?; + let shared_public_keys = fs_err::read(bundle_directories[0].join(PUBLIC_KEY_SET_FILE))?; + let secret_shares = bundle_directories + .iter() + .map(|bundle| fs_err::read(bundle.join(SECRET_SHARE_FILE))) + .collect::, _>>()?; + assert!(bundle_directories.iter().all(|bundle| { + fs_err::read(bundle.join(SETUP_CONTEXT_FILE)).unwrap() == shared_setup + && fs_err::read(bundle.join(PUBLIC_KEY_SET_FILE)).unwrap() == shared_public_keys + })); + assert_ne!(secret_shares[0], secret_shares[1]); + assert_ne!(secret_shares[1], secret_shares[2]); + assert_ne!(secret_shares[0], secret_shares[2]); + let common_files = [MANIFEST_FILE, DECRYPTION_CONFIG_FILE, CONTEXT_CONFIG_FILE]; + assert!(common_files.iter().all(|name| { + let expected = fs_err::read(board_directory.join("ceremony").join(name)).unwrap(); + work_directories + .iter() + .all(|work| fs_err::read(work.join("ceremony").join(name)).unwrap() == expected) + })); + + board.shutdown().await?; + Ok(()) +} diff --git a/bin/validator/src/commands/mod.rs b/bin/validator/src/commands/mod.rs index 3c4f51d16d..cc080fb515 100644 --- a/bin/validator/src/commands/mod.rs +++ b/bin/validator/src/commands/mod.rs @@ -294,7 +294,7 @@ impl ValidatorCommand { .context("failed to apply validator database migrations")?; Ok(()) }, - Self::Dkg(options) => dkg::run(options).await, + Self::Dkg(options) => Box::pin(dkg::run(options)).await, Self::IssuePrivateRecordShare(options) => { issue_private_record_share::issue_from_options(options) }, diff --git a/compose/validator.yml b/compose/validator.yml index 1aa93454b2..f7a6e947df 100644 --- a/compose/validator.yml +++ b/compose/validator.yml @@ -15,6 +15,124 @@ x-validator: &validator - --admin.listen=0.0.0.0:50102 services: + # Opt-in end-to-end check for the Iroh exchange. Validator bootstrap is defined in node.yml. + storage-key-dkg-check: + profiles: ["storage-key-dkg"] + image: ${MIDEN_VALIDATOR_IMAGE:-miden-validator} + pull_policy: missing + volumes: + - node-data:/data + environment: + MIDEN_VALIDATOR_STORAGE_KEY_EPOCH: ${MIDEN_VALIDATOR_STORAGE_KEY_EPOCH:-0909090909090909090909090909090909090909090909090909090909090909} + depends_on: + bootstrap-validator: + condition: service_completed_successfully + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -eu + ROOT=/data/storage-key-dkg-check + TICKETS="$${ROOT}/board-tickets" + GENESIS=/data/genesis/genesis.dat + EPOCH="$${MIDEN_VALIDATOR_STORAGE_KEY_EPOCH}" + rm -rf "$${ROOT}" + mkdir -p "$${ROOT}" + + miden-validator dkg board \ + --data-directory "$${ROOT}/board" \ + --genesis "$${GENESIS}" \ + --threshold 2 \ + --epoch "$${EPOCH}" \ + --ticket-directory "$${TICKETS}" & + BOARD_PID=$$! + RUNNER_PIDS="" + cleanup() { + for PID in $${RUNNER_PIDS}; do + kill -TERM "$${PID}" 2>/dev/null || true + done + kill -INT "$${BOARD_PID}" 2>/dev/null || true + for PID in $${RUNNER_PIDS}; do + wait "$${PID}" 2>/dev/null || true + done + wait "$${BOARD_PID}" 2>/dev/null || true + } + trap cleanup EXIT INT TERM + + while [ ! -s "$${TICKETS}/participant-1.ticket" ]; do + kill -0 "$${BOARD_PID}" 2>/dev/null + sleep 1 + done + run_validator() { + PARTICIPANT="$$1" + SIGNING_KEY="$$2" + exec miden-validator dkg run \ + --board-file "$${TICKETS}/participant-$${PARTICIPANT}.ticket" \ + --genesis "$${GENESIS}" \ + --threshold 2 \ + --epoch "$${EPOCH}" \ + --signing-key.hex "$${SIGNING_KEY}" \ + --work-directory "$${ROOT}/validator-$${PARTICIPANT}/work" \ + --output-directory "$${ROOT}/validator-$${PARTICIPANT}/bundle" + } + + run_checked() { + PARTICIPANT="$$1" + SIGNING_KEY="$$2" + run_validator "$${PARTICIPANT}" "$${SIGNING_KEY}" & + VALIDATOR_PID=$$! + terminate_validator() { + kill -TERM "$${VALIDATOR_PID}" 2>/dev/null || true + wait "$${VALIDATOR_PID}" 2>/dev/null || true + echo 143 > "$${ROOT}/status-$${PARTICIPANT}" + exit 143 + } + trap terminate_validator INT TERM + set +e + wait "$${VALIDATOR_PID}" + STATUS=$$? + set -e + trap - INT TERM + echo "$${STATUS}" > "$${ROOT}/status-$${PARTICIPANT}" + exit "$${STATUS}" + } + + run_checked 1 0101010101010101010101010101010101010101010101010101010101010101 & + FIRST_PID=$$! + run_checked 2 0303030303030303030303030303030303030303030303030303030303030303 & + SECOND_PID=$$! + run_checked 3 0404040404040404040404040404040404040404040404040404040404040404 & + THIRD_PID=$$! + RUNNER_PIDS="$${FIRST_PID} $${SECOND_PID} $${THIRD_PID}" + + while :; do + COMPLETE=0 + for PARTICIPANT in 1 2 3; do + STATUS_FILE="$${ROOT}/status-$${PARTICIPANT}" + if [ -f "$${STATUS_FILE}" ]; then + STATUS="$$(cat "$${STATUS_FILE}")" + [ "$${STATUS}" -eq 0 ] || exit "$${STATUS}" + COMPLETE=$$((COMPLETE + 1)) + fi + done + [ "$${COMPLETE}" -eq 3 ] && break + sleep 1 + done + wait "$${FIRST_PID}" + wait "$${SECOND_PID}" + wait "$${THIRD_PID}" + + for PARTICIPANT in 2 3; do + cmp "$${ROOT}/validator-1/bundle/epoch.hex" \ + "$${ROOT}/validator-$${PARTICIPANT}/bundle/epoch.hex" + cmp "$${ROOT}/validator-1/bundle/setup-context.wire" \ + "$${ROOT}/validator-$${PARTICIPANT}/bundle/setup-context.wire" + cmp "$${ROOT}/validator-1/bundle/public-key-set.wire" \ + "$${ROOT}/validator-$${PARTICIPANT}/bundle/public-key-set.wire" + ! cmp -s "$${ROOT}/validator-1/bundle/secret-share.wire" \ + "$${ROOT}/validator-$${PARTICIPANT}/bundle/secret-share.wire" + done + echo "storage key DKG completed over the Iroh document." + validator-1: <<: *validator environment: diff --git a/docs/external/src/local-network-development.md b/docs/external/src/local-network-development.md index 9cb29bcc9c..6bdaf24c0c 100644 --- a/docs/external/src/local-network-development.md +++ b/docs/external/src/local-network-development.md @@ -235,6 +235,17 @@ starting the network. This can take several minutes. For a faster local start, s `MIDEN_VALIDATOR_USE_STORAGE_KEY_FIXTURE=true` to use the committed insecure fixture instead. The fixture is public test data and must never be used outside local development. +To exercise the production ceremony through an Iroh document, build the validator image and run the opt-in check. The +fixture flag skips the separate bootstrap ceremony, so the command runs only the Iroh ceremony under test. + +```bash +make docker-build-validator +MIDEN_VALIDATOR_USE_STORAGE_KEY_FIXTURE=true \ +docker compose --profile storage-key-dkg run --rm storage-key-dkg-check +``` + +This runs the production proof backend and can take several minutes. + ## Check the RPC API The RPC server exposes gRPC reflection. With `grpcurl` installed, a basic status check looks like: diff --git a/docs/external/src/network-operator/bootstrap-and-genesis.md b/docs/external/src/network-operator/bootstrap-and-genesis.md index eeddd7f434..81c58636b6 100644 --- a/docs/external/src/network-operator/bootstrap-and-genesis.md +++ b/docs/external/src/network-operator/bootstrap-and-genesis.md @@ -151,13 +151,15 @@ configuration's `validators` list. ## Storage Key Ceremony -After genesis is built, every listed validator must join one offline DKG ceremony. The ceremony creates the shared -public storage key and one distinct secret share per validator. No coordinator can derive those shares. - -Each operator first registers a fresh DKG identity with the validator signing key committed in genesis. One coordinator -uses every signed registration to prepare the common ceremony. Every operator then creates two public dealings, checks -and signs the same full transcript, and completes both rounds locally. The DKG and database bootstrap may run in either -order, but both must finish before the validator starts. +After genesis is built, every listed validator must join one DKG ceremony before validator startup. The ceremony creates +the shared public storage key and one distinct secret share per validator. No coordinator can derive those shares. + +For the normal flow, one operator starts the durable Iroh board and sends its private ticket to each validator through +an authenticated and confidential bootstrap channel. Each validator runs the full ceremony with the signing key +committed in genesis. The board carries only public ceremony artifacts. Validator signatures authenticate registrations +and transcript checkpoints. Each validator keeps its identity, private DKG state, and final secret share local. The +manual file commands remain available for recovery. The DKG and database bootstrap may run in either order, but both +must finish before the validator starts. All listed validators must contribute to the ceremony even when the recovery threshold is lower. If any participant drops out or any transcript differs, discard the incomplete ceremony and start a new one with fresh identities and diff --git a/docs/external/src/network-operator/validator.md b/docs/external/src/network-operator/validator.md index a1f93c0cbc..f61418ff07 100644 --- a/docs/external/src/network-operator/validator.md +++ b/docs/external/src/network-operator/validator.md @@ -40,9 +40,49 @@ This flow supports initial storage-key bootstrap only. The validator loads one s shares, and validator-set changes are not yet supported. Keep each operator bundle available for as long as records from its epoch may need to be decrypted. -First, each operator creates a DKG identity for the agreed storage-key epoch and sends `registration.toml` to the -coordinator. The registration proves ownership of the DKG identity secret. The signing key must match one key in -genesis. Use `--signing-key.hex` instead of KMS only for local or private deployments. +For the normal ceremony, one operator starts the durable Iroh bulletin board: + +```bash +miden-validator dkg board \ + --data-directory storage-key-board \ + --genesis genesis.dat \ + --threshold 2 \ + --epoch <32-byte-hex-epoch> \ + --ticket-directory storage-key-board-tickets +``` + +The command writes one ticket per genesis validator. Each ticket contains the board's Iroh address, a shared read-only +document capability, and permission to upload only to that participant's slots. It contains no private DKG share. Send +each file through an authenticated and confidential bootstrap channel. A ticket holder can stop the ceremony by +uploading a conflicting value to that participant's slot. Keep the board running until every validator reports ceremony +completion, then stop it with Ctrl-C. + +The current command uses Iroh's public discovery and relay services. The board and validators need outbound network +access. This is a setup flow run before validator startup, not an isolated network transport. + +Each validator then runs the full ceremony with its own signing key and private work directory: + +```bash +miden-validator dkg run \ + --board-file \ + --genesis genesis.dat \ + --threshold 2 \ + --epoch <32-byte-hex-epoch> \ + --signing-key.kms-id \ + --work-directory storage-key-work \ + --output-directory storage-key +``` + +Both commands can restart with the same data and work directories. Give `--ticket-directory` a new path when restarting +the board because it will not overwrite a ticket directory. Each validator collects the signed registrations and derives +the common files from its expected threshold and epoch. It rejects different board copies, checks every later artifact, +and validates its storage key bundle against the transcript accepted by every validator. A board directory from an older +format cannot be reopened; start that ceremony again in a new directory. + +The commands below provide a manual recovery path. First, each operator creates a DKG identity for the agreed +storage-key epoch and sends `registration.toml` to the coordinator. The registration proves ownership of the DKG +identity secret. The signing key must match one key in genesis. Use `--signing-key.hex` instead of KMS only for local or +private deployments. ```bash miden-validator dkg identity \