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 b6e14704a2..a122b781b2 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.92" @@ -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.20", +] + [[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 1.0.109", +] + [[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.12" @@ -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.34" @@ -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.34" @@ -2429,6 +2714,19 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +[[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.34" @@ -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.20", + "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.20", + "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.20", + "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" @@ -3169,6 +3610,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" @@ -3257,11 +3704,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.20", + "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" @@ -3364,6 +4176,22 @@ dependencies = [ "jiff-tzdb", ] +[[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" @@ -3373,7 +4201,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys", + "jni-sys 0.4.1", "log", "simd_cesu8", "thiserror 2.0.20", @@ -3394,6 +4222,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" @@ -3662,6 +4499,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" @@ -3678,6 +4524,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" @@ -4429,7 +5281,7 @@ dependencies = [ "http-body-util", "humantime", "itertools 0.14.0", - "lru", + "lru 0.16.4", "miden-crypto", "miden-node-tracing-macro", "miden-protocol", @@ -4827,11 +5679,17 @@ dependencies = [ "chacha20poly1305", "clap", "fs-err", + "futures", "golden-core", "golden-ehtdh1", "golden-evrf", "golden-halo2curves", "hex", + "iroh", + "iroh-blobs", + "iroh-docs", + "iroh-gossip", + "iroh-tickets", "miden-node-db", "miden-node-proto", "miden-node-proto-build", @@ -4840,6 +5698,7 @@ dependencies = [ "miden-protocol", "miden-testing", "miden-tx", + "postcard", "rand 0.10.2", "rand_chacha 0.3.1", "rand_core 0.6.4", @@ -4918,27 +5777,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.20", +] [[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" @@ -4962,6 +6023,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.20", + "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.20", + "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" @@ -5070,6 +6193,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" @@ -5092,6 +6327,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" @@ -5396,6 +6637,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" @@ -5424,6 +6675,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" @@ -5459,6 +6716,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" @@ -5486,6 +6752,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" @@ -5534,6 +6810,19 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[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" @@ -5578,7 +6867,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]] @@ -5586,6 +6887,9 @@ name = "portable-atomic" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +dependencies = [ + "serde", +] [[package]] name = "portable-atomic-util" @@ -5596,6 +6900,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.6" @@ -5620,6 +6959,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" @@ -5699,12 +7049,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]] @@ -5729,6 +7105,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" @@ -5927,6 +7309,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" @@ -6124,6 +7515,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" @@ -6153,6 +7557,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" @@ -6182,6 +7595,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" @@ -6233,6 +7658,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", + "futures-util", "h2 0.4.15", "http 1.5.0", "http-body 1.1.0", @@ -6255,15 +7681,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" @@ -6506,7 +7940,7 @@ checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls 0.23.43", @@ -6672,6 +8106,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" @@ -6715,6 +8165,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" @@ -6725,6 +8181,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" @@ -6748,6 +8213,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" @@ -6891,6 +8366,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" @@ -7001,6 +8482,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" @@ -7042,6 +8532,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" @@ -7051,6 +8558,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" @@ -7130,6 +8643,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" @@ -7169,6 +8703,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" @@ -7222,6 +8767,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" @@ -7337,6 +8888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", + "js-sys", "num-conv", "powerfmt", "serde_core", @@ -7479,6 +9031,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" @@ -7959,6 +9534,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" @@ -8012,6 +9597,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", ] @@ -8156,6 +9742,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.104" @@ -8173,6 +9772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", + "serde", "wasm-bindgen", ] @@ -8185,6 +9785,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" @@ -8340,13 +9955,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]] @@ -8358,20 +9982,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]] @@ -8383,18 +10022,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" @@ -8407,24 +10064,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" @@ -8446,6 +10127,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.20", + "windows", + "windows-core", +] + [[package]] name = "wnaf" version = "0.14.0" @@ -8463,6 +10159,25 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +[[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.20", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 6dacd9fc63..0311e4f36b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,11 +89,17 @@ 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" } +iroh-tickets = { version = "=1.0.0" } itertools = { version = "0.14" } libsqlite3-sys = { features = ["bundled"], version = "0.35" } lru = { default-features = false, version = "0.16" } miette = { version = "7.6" } opentelemetry = { version = "0.31" } +postcard = { features = ["use-std"], version = "1.1.3" } pretty_assertions = { version = "1.4" } proc-macro2 = { version = "1.0" } # prost and protox are from different authors and are _not_ released in diff --git a/bin/validator/Cargo.toml b/bin/validator/Cargo.toml index 24cd2c285c..2bfab3b806 100644 --- a/bin/validator/Cargo.toml +++ b/bin/validator/Cargo.toml @@ -29,11 +29,17 @@ 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 } +iroh-tickets = { workspace = true } miden-node-db = { workspace = true } miden-node-proto = { workspace = true } miden-node-proto-build = { features = ["internal"], workspace = true } @@ -41,12 +47,13 @@ miden-node-store = { workspace = true } miden-node-utils = { features = ["testing"], workspace = true } miden-protocol = { workspace = true } miden-tx = { features = ["concurrent"], workspace = true } +postcard = { workspace = true } rand_core_06 = { workspace = true } 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..70e3e77120 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, @@ -1749,14 +1759,76 @@ fn publish_directory( ) -> anyhow::Result<()> { ensure!(!output_directory.exists(), "output directory already exists"); let parent = output_directory.parent().unwrap_or_else(|| Path::new(".")); - fs_err::create_dir_all(parent).context("failed to create output parent directory")?; + durably_create_directory_all(parent).context("failed to create output parent directory")?; let temporary = tempfile::Builder::new() .prefix(".storage-key-dkg-") .tempdir_in(parent) .context("failed to create temporary output directory")?; write(temporary.path())?; + sync_directory_tree(temporary.path())?; fs_err::rename(temporary.path(), output_directory) .context("failed to publish output directory")?; + sync_directory(parent)?; + Ok(()) +} + +#[cfg(unix)] +fn sync_directory(directory: &Path) -> anyhow::Result<()> { + std::fs::File::open(directory) + .with_context(|| format!("failed to open directory {}", directory.display()))? + .sync_all() + .with_context(|| format!("failed to sync directory {}", directory.display())) +} + +#[cfg(not(unix))] +fn sync_directory(_directory: &Path) -> anyhow::Result<()> { + Ok(()) +} + +fn durably_create_directory_all(directory: &Path) -> anyhow::Result<()> { + let absolute_directory = if directory.is_absolute() { + directory.to_owned() + } else { + std::env::current_dir() + .context("failed to determine current directory")? + .join(directory) + }; + let existing_ancestor = absolute_directory + .ancestors() + .find(|ancestor| ancestor.exists()) + .context("directory has no existing ancestor")? + .to_owned(); + fs_err::create_dir_all(directory)?; + + let mut current = absolute_directory.as_path(); + while current != existing_ancestor { + current = current.parent().context("directory escaped its existing ancestor")?; + sync_directory(current)?; + } + Ok(()) +} + +#[cfg(unix)] +fn sync_directory_tree(directory: &Path) -> anyhow::Result<()> { + for entry in fs_err::read_dir(directory) + .with_context(|| format!("failed to read directory {}", directory.display()))? + { + let entry = entry?; + let file_type = entry.file_type()?; + if file_type.is_dir() { + sync_directory_tree(&entry.path())?; + } else if file_type.is_file() { + std::fs::File::open(entry.path()) + .with_context(|| format!("failed to open file {}", entry.path().display()))? + .sync_all() + .with_context(|| format!("failed to sync file {}", entry.path().display()))?; + } + } + sync_directory(directory) +} + +#[cfg(not(unix))] +fn sync_directory_tree(_directory: &Path) -> anyhow::Result<()> { Ok(()) } diff --git a/bin/validator/src/commands/dkg/board.rs b/bin/validator/src/commands/dkg/board.rs new file mode 100644 index 0000000000..bf3462ac0c --- /dev/null +++ b/bin/validator/src/commands/dkg/board.rs @@ -0,0 +1,1104 @@ +//! 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::io::Write; +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 iroh_tickets::{ParseError, Ticket}; +use serde::{Deserialize, Serialize}; + +use super::{ + decode_fixed_hex, + durably_create_directory_all, + publish_directory, + sync_directory, + write_new_file, +}; + +const ENDPOINT_SECRET_FILE: &str = "endpoint-secret.hex"; +const BOARD_METADATA_DIRECTORY: &str = "board-meta"; +const DOCUMENT_ID_FILE: &str = "document-id.hex"; +const BOARD_FORMAT_FILE: &str = "board-format"; +const BOARD_FORMAT: &[u8] = b"participant-upload-v4\n"; +const UPLOAD_SECRETS_DIRECTORY: &str = "upload-secrets"; +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, Deserialize, Serialize)] +pub(super) struct BoardTicket { + document: DocTicket, + pub(super) participant: u32, + upload_secret: [u8; 32], +} + +#[derive(Deserialize, Serialize)] +enum BoardTicketWireFormat { + Variant0(BoardTicket), +} + +impl Ticket for BoardTicket { + const KIND: &'static str = "miden-storage-key-dkg-board"; + + fn encode_bytes(&self) -> Vec { + postcard::to_stdvec(&BoardTicketWireFormat::Variant0(self.clone())) + .expect("postcard serialization failed") + } + + fn decode_bytes(bytes: &[u8]) -> Result { + let BoardTicketWireFormat::Variant0(ticket) = postcard::from_bytes(bytes)?; + if ticket.participant == 0 { + return Err(ParseError::verification_failed( + "DKG board participant index must be nonzero", + )); + } + if !matches!(ticket.document.capability, iroh_docs::Capability::Read(_)) { + return Err(ParseError::verification_failed( + "DKG board document ticket must be read-only", + )); + } + if ticket.document.nodes.is_empty() { + return Err(ParseError::verification_failed( + "DKG board document addressing info cannot be empty", + )); + } + Ok(ticket) + } +} + +impl fmt::Display for BoardTicket { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&Ticket::encode_string(self)) + } +} + +impl FromStr for BoardTicket { + type Err = ParseError; + + fn from_str(value: &str) -> Result { + Ticket::decode_string(value) + } +} + +/// 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 metadata_directory = data_directory.join(BOARD_METADATA_DIRECTORY); + let (document, upload_secrets) = if metadata_directory.exists() { + require_current_board_format(&metadata_directory)?; + let document_id_path = metadata_directory.join(DOCUMENT_ID_FILE); + 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")?; + let document = runtime + .docs + .open(iroh_docs::NamespaceId::from(&id)) + .await + .context("failed to open Iroh document")? + .context("persisted Iroh document is missing")?; + let upload_secrets = load_upload_secrets(&metadata_directory, participant_count)?; + (document, upload_secrets) + } else { + ensure!( + !data_directory.join(DOCUMENT_ID_FILE).exists() + && !data_directory.join(BOARD_FORMAT_FILE).exists() + && !data_directory.join(UPLOAD_SECRETS_DIRECTORY).exists(), + "unsupported DKG board format; start a new ceremony in a new data directory" + ); + let document = runtime.docs.create().await.context("failed to create Iroh document")?; + let upload_secrets = (0..participant_count) + .map(|_| SecretKey::generate().to_bytes()) + .collect::>(); + publish_board_metadata(&metadata_directory, &document, &upload_secrets)?; + (document, upload_secrets) + }; + 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: BoardTicket, + 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: BoardTicket, + participant_count: usize, + use_network_services: bool, + ) -> anyhow::Result { + 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 { + durably_create_directory_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 encoded = fs_err::read_to_string(&path) + .with_context(|| format!("failed to read Iroh endpoint secret {}", path.display()))?; + return SecretKey::from_str(encoded.trim()).context("invalid Iroh endpoint secret"); + } + + let secret = SecretKey::generate(); + let mut temporary = tempfile::Builder::new() + .prefix(".endpoint-secret-") + .tempfile_in(data_directory) + .context("failed to create temporary Iroh endpoint secret")?; + temporary + .write_all(hex::encode(secret.to_bytes()).as_bytes()) + .context("failed to write temporary Iroh endpoint secret")?; + temporary + .as_file() + .sync_all() + .context("failed to sync temporary Iroh endpoint secret")?; + temporary + .persist_noclobber(&path) + .map_err(|error| error.error) + .with_context(|| format!("failed to publish Iroh endpoint secret {}", path.display()))?; + sync_directory(data_directory)?; + Ok(secret) +} + +fn publish_board_metadata( + path: &Path, + document: &Doc, + upload_secrets: &[[u8; 32]], +) -> anyhow::Result<()> { + publish_directory(path, |temporary| { + write_new_file( + &temporary.join(DOCUMENT_ID_FILE), + hex::encode(document.id().to_bytes()).as_bytes(), + true, + )?; + write_new_file(&temporary.join(BOARD_FORMAT_FILE), BOARD_FORMAT, true)?; + let upload_secrets_directory = temporary.join(UPLOAD_SECRETS_DIRECTORY); + fs_err::create_dir(&upload_secrets_directory).with_context(|| { + format!( + "failed to create DKG board upload secrets {}", + upload_secrets_directory.display() + ) + })?; + for (position, secret) in upload_secrets.iter().enumerate() { + write_new_file( + &upload_secrets_directory.join(format!("participant-{}.hex", position + 1)), + hex::encode(secret).as_bytes(), + true, + )?; + } + Ok(()) + }) +} + +fn load_upload_secrets( + metadata_directory: &Path, + participant_count: usize, +) -> anyhow::Result> { + let path = metadata_directory.join(UPLOAD_SECRETS_DIRECTORY); + 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" + ); + (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() +} + +fn require_current_board_format(metadata_directory: &Path) -> anyhow::Result<()> { + let path = metadata_directory.join(BOARD_FORMAT_FILE); + let format = fs_err::read(&path) + .with_context(|| format!("failed to read DKG board format {}", 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..51e559ec55 --- /dev/null +++ b/bin/validator/src/commands/dkg/board/tests.rs @@ -0,0 +1,354 @@ +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, 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() +} + +#[test] +fn endpoint_secret_is_persisted_privately() -> anyhow::Result<()> { + let data_directory = tempfile::tempdir()?; + let first = load_or_create_endpoint_secret(data_directory.path())?; + let second = load_or_create_endpoint_secret(data_directory.path())?; + assert_eq!(first.to_bytes(), second.to_bytes()); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let mode = fs_err::metadata(data_directory.path().join(ENDPOINT_SECRET_FILE))? + .permissions() + .mode(); + assert_eq!(mode & 0o777, 0o600); + } + Ok(()) +} + +#[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 board_metadata_is_published_as_one_directory() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let data_directory = root.path().join("host"); + let (host, _) = BoardNode::create_for_test(&data_directory).await?; + + let metadata_directory = data_directory.join(BOARD_METADATA_DIRECTORY); + assert!(metadata_directory.join(DOCUMENT_ID_FILE).is_file()); + assert!(metadata_directory.join(BOARD_FORMAT_FILE).is_file()); + assert!(metadata_directory.join(UPLOAD_SECRETS_DIRECTORY).is_dir()); + assert!(!data_directory.join(DOCUMENT_ID_FILE).exists()); + assert!(!data_directory.join(BOARD_FORMAT_FILE).exists()); + assert!(!data_directory.join(UPLOAD_SECRETS_DIRECTORY).exists()); + + host.shutdown().await?; + Ok(()) +} + +#[tokio::test] +async fn incomplete_board_metadata_is_rejected() -> 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_METADATA_DIRECTORY).join(BOARD_FORMAT_FILE))?; + + let error = BoardNode::create_for_test(&data_directory) + .await + .err() + .context("incomplete board metadata unexpectedly reopened")?; + assert!(error.to_string().contains("failed to read DKG board format")); + Ok(()) +} + +#[tokio::test] +async fn legacy_board_metadata_is_rejected() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let data_directory = root.path().join("host"); + let upload_secrets_directory = data_directory.join(UPLOAD_SECRETS_DIRECTORY); + fs_err::create_dir_all(&upload_secrets_directory)?; + fs_err::write(data_directory.join(DOCUMENT_ID_FILE), hex::encode([0; 32]))?; + fs_err::write(data_directory.join(BOARD_FORMAT_FILE), b"participant-upload-v3\n")?; + for participant in 1..=3 { + fs_err::write( + upload_secrets_directory.join(format!("participant-{participant}.hex")), + hex::encode([0; 32]), + )?; + } + + let error = BoardNode::create_for_test(&data_directory) + .await + .err() + .context("legacy board metadata unexpectedly reopened")?; + assert!(error.to_string().contains("unsupported DKG board format")); + assert!(!data_directory.join(BOARD_METADATA_DIRECTORY).exists()); + 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_METADATA_DIRECTORY).join(BOARD_FORMAT_FILE), + b"participant-upload-v3\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 legacy_board_ticket_is_rejected() { + BoardTicket::from_str("miden-storage-key-dkg-board-v3:1:00:invalid") + .expect_err("old board ticket unexpectedly parsed"); +} + +#[tokio::test] +async fn board_ticket_round_trips_and_validates_fields() -> anyhow::Result<()> { + let root = tempfile::tempdir()?; + let (host, mut tickets) = BoardNode::create_for_test(root.path()).await?; + let ticket = tickets.remove(0); + let encoded = ticket.to_string(); + let decoded = BoardTicket::from_str(&encoded)?; + assert_eq!(decoded.to_string(), encoded); + + let mut invalid = ticket.clone(); + invalid.participant = 0; + let error = BoardTicket::from_str(&invalid.to_string()) + .expect_err("zero participant ticket unexpectedly parsed"); + assert!(error.to_string().contains("must be nonzero")); + + invalid = ticket; + invalid.document.nodes.clear(); + let error = BoardTicket::from_str(&invalid.to_string()) + .expect_err("ticket without addressing info unexpectedly parsed"); + assert!(error.to_string().contains("addressing info cannot be empty")); + + host.shutdown().await?; + Ok(()) +} + +#[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..209256e8ff --- /dev/null +++ b/bin/validator/src/commands/dkg/runner.rs @@ -0,0 +1,618 @@ +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, BoardTicket}; +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, + durably_create_directory_all, + 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 board = board.parse::().context("invalid storage key DKG board ticket")?; + 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: BoardTicket, + 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, +{ + durably_create_directory_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 ac4e4e19b4..761e97153d 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::{ @@ -899,3 +901,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].clone()) + }) + .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].clone(), + &genesis.path, + &signers[0], + 2, + &epoch, + &work_directories[0], + &bundle_directories[0], + false, + timeout, + ); + let second = runner::run_validator_with_network::( + tickets[1].clone(), + &genesis.path, + &signers[1], + 2, + &epoch, + &work_directories[1], + &bundle_directories[1], + false, + timeout, + ); + let third = runner::run_validator_with_network::( + tickets[2].clone(), + &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 687135989e..5b24d5d59f 100644 --- a/bin/validator/src/commands/mod.rs +++ b/bin/validator/src/commands/mod.rs @@ -275,7 +275,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) }, @@ -517,493 +517,4 @@ impl ValidatorSigningKey { // ================================================================================================ #[cfg(test)] -mod tests { - use std::collections::BTreeMap; - use std::path::Path; - - use golden_core::{GoldenGroup, GoldenScalar, ParticipantIndex, SessionId}; - use golden_ehtdh1::wire::{from_wire_bytes, to_wire_bytes}; - use golden_ehtdh1::{ - DecryptionShare, - PublicKeySet, - PublicShare, - SecretShare, - SetupContext, - derive_context_session_id, - }; - use golden_halo2curves::golden_group::{Secp256k1GoldenGroup, Secp256k1Scalar}; - use miden_protocol::Word; - use miden_protocol::account::auth::AuthScheme; - use miden_protocol::transaction::{TransactionId, TransactionInputs}; - use miden_protocol::utils::serde::{Deserializable, Serializable}; - use miden_testing::{Auth, MockChainBuilder}; - use rand_chacha_03::ChaCha20Rng; - use rand_chacha_03::rand_core::SeedableRng; - - use super::*; - - type TestStorageGroup = Secp256k1GoldenGroup; - - const TEST_SIGNING_KEY_HEX: &str = - "0101010101010101010101010101010101010101010101010101010101010101"; - const TEST_ENCRYPTION_KEY_HEX: &str = - "0202020202020202020202020202020202020202020202020202020202020202"; - - const BASE_START_ARGS: [&str; 8] = [ - "miden-validator", - "start", - "--listen", - "127.0.0.1:50101", - "--data-directory", - "/tmp/validator-data", - "--signing-key.hex", - TEST_SIGNING_KEY_HEX, - ]; - const ENCRYPTION_KEY_ARGS: [&str; 2] = ["--encryption-key.hex", TEST_ENCRYPTION_KEY_HEX]; - const STORAGE_KEY_ARGS: [&str; 8] = [ - "--storage-key.epoch", - "0909090909090909090909090909090909090909090909090909090909090909", - "--storage-key.setup-context", - "/tmp/setup.bin", - "--storage-key.public-key-set", - "/tmp/public.bin", - "--storage-key.secret-share", - "/tmp/secret.bin", - ]; - - fn parse_start(extra: &[&str]) -> Result { - ValidatorCommand::try_parse_from( - BASE_START_ARGS - .iter() - .copied() - .chain(extra.iter().copied()) - .chain(STORAGE_KEY_ARGS.iter().copied()), - ) - } - - fn test_operator_keys(epoch: u8, setup_marker: u8) -> Vec { - let participants = [1, 2, 3].map(|value| ParticipantIndex::new(value).unwrap()); - let decryption_secret = Secp256k1Scalar::from_u64(11).unwrap(); - let decryption_coefficient = Secp256k1Scalar::from_u64(7).unwrap(); - let context_coefficient = Secp256k1Scalar::from_u64(13).unwrap(); - let mut public_shares = BTreeMap::new(); - let secret_shares = participants.map(|participant| { - let point = participant.to_scalar::().unwrap(); - let decryption = decryption_secret.add(&decryption_coefficient.mul(&point)); - let context = context_coefficient.mul(&point); - public_shares.insert( - participant, - PublicShare { - decryption: TestStorageGroup::mul_generator(&decryption), - context: TestStorageGroup::mul_generator(&context), - }, - ); - SecretShare { participant, decryption, context } - }); - let public_key_set = PublicKeySet::new( - 2, - TestStorageGroup::mul_generator(&decryption_secret), - public_shares, - ) - .unwrap(); - let decryption_session_id = SessionId([2; 32]); - let setup_context = SetupContext { - backend_id: TestStorageGroup::BACKEND_ID.to_owned(), - threshold: 2, - registry_root: [1; 32], - participants: participants.to_vec(), - decryption_session_id, - context_session_id: derive_context_session_id(decryption_session_id), - decryption_transcript_root: [setup_marker; 32], - context_transcript_root: [4; 32], - epoch: [epoch; 32], - }; - secret_shares - .into_iter() - .map(|secret_share| { - GoldenOperatorKey::new( - StorageKeyEpoch::new([epoch; 32]), - setup_context.clone(), - public_key_set.clone(), - secret_share, - ) - .unwrap() - }) - .collect() - } - - fn transaction_inputs() -> TransactionInputs { - let mut builder = MockChainBuilder::new(); - let account = builder - .add_existing_wallet(Auth::BasicAuth { - auth_scheme: AuthScheme::Falcon512Poseidon2, - }) - .unwrap(); - builder.build().unwrap().get_transaction_inputs(&account, &[], &[]).unwrap() - } - - fn issue_error(record: &Path, output: &Path, operator_key: &GoldenOperatorKey) -> String { - let error = issue_private_record_share::issue(record, output, operator_key).unwrap_err(); - format!("{error:#}") - } - - fn assert_issue_error( - record: &Path, - output: &Path, - operator_key: &GoldenOperatorKey, - expected: &str, - ) { - let error = issue_error(record, output, operator_key); - assert!(error.contains(expected), "expected {expected:?} in {error:?}"); - } - - fn issue_share_file(record: &Path, output: &Path, operator_key: &GoldenOperatorKey) -> Vec { - issue_private_record_share::issue(record, output, operator_key).unwrap(); - fs_err::read(output).unwrap() - } - - async fn store_private_record( - writer: &miden_validator::db::ValidatorDbWriter, - record: miden_validator::StoredPrivateRecord, - ) { - writer.insert_validated_private_transaction(record).await.unwrap(); - } - - async fn export_record_file( - data_directory: &Path, - encoded_transaction_id: &str, - encoded_validator_id: &str, - output: &Path, - ) { - export_private_record::export(PrivateRecordExportOptions { - data_directory: data_directory.to_path_buf(), - transaction_id: encoded_transaction_id.to_owned(), - validator_id: encoded_validator_id.to_owned(), - output: output.to_path_buf(), - }) - .await - .unwrap(); - } - - const BASE_GENESIS_ARGS: [&str; 6] = [ - "miden-validator", - "genesis", - "--genesis-block-directory", - "/tmp/genesis", - "--accounts-directory", - "/tmp/accounts", - ]; - - fn parse_genesis(extra: &[&str]) -> Result { - ValidatorCommand::try_parse_from( - BASE_GENESIS_ARGS.iter().copied().chain(extra.iter().copied()), - ) - } - - #[test] - fn genesis_validator_keys_parse_from_repeated_flags() { - let keys = [7u8, 8].map(|seed| { - SigningKey::read_from_bytes(&[seed; 32]).expect("test signing key should decode") - }); - let hex_keys = keys.clone().map(|key| hex::encode(key.public_key().to_bytes())); - let command = - parse_genesis(&["--validator.key", &hex_keys[0], "--validator.key", &hex_keys[1]]) - .expect("genesis with explicit validator keys must parse"); - let ValidatorCommand::Genesis { validator_keys, .. } = command else { - panic!("expected the genesis command"); - }; - assert_eq!(validator_keys, keys.map(|key| key.public_key()).to_vec()); - } - - #[test] - fn genesis_requires_an_explicit_validator_set() { - let Err(error) = parse_genesis(&[]) else { - panic!("genesis without an explicit validator set must be rejected"); - }; - assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); - - let key = SigningKey::read_from_bytes(&[7; 32]).expect("test signing key should decode"); - let key_hex = hex::encode(key.public_key().to_bytes()); - parse_genesis(&["--config", "/tmp/genesis.toml", "--validator.key", &key_hex]) - .expect("--config with an explicit validator set must parse"); - } - - #[test] - fn genesis_rejects_an_invalid_validator_key() { - let Err(error) = parse_genesis(&["--validator.key", "not-hex"]) else { - panic!("an invalid validator key must be rejected"); - }; - assert_eq!(error.kind(), clap::error::ErrorKind::ValueValidation); - } - - #[test] - fn encryption_key_is_required() { - let Err(error) = parse_start(&[]) else { - panic!("start without an encryption key must be rejected"); - }; - assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); - } - - #[test] - fn signing_key_is_required() { - let Err(error) = ValidatorCommand::try_parse_from( - ["miden-validator", "start", "--listen", "127.0.0.1:50101"] - .into_iter() - .chain(["--data-directory", "/tmp/validator-data"]) - .chain(ENCRYPTION_KEY_ARGS) - .chain(STORAGE_KEY_ARGS), - ) else { - panic!("start without a signing key must be rejected"); - }; - assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); - } - - #[test] - fn admin_listener_is_opt_in() { - let command = - parse_start(&ENCRYPTION_KEY_ARGS).expect("start without an admin listener must parse"); - let ValidatorCommand::Start { admin_listen, .. } = command else { - panic!("expected the start command"); - }; - assert_eq!(admin_listen, None); - - let command = parse_start( - &[ENCRYPTION_KEY_ARGS.as_slice(), &["--admin.listen", "127.0.0.1:50102"]].concat(), - ) - .expect("start with an admin listener must parse"); - let ValidatorCommand::Start { admin_listen, .. } = command else { - panic!("expected the start command"); - }; - assert_eq!(admin_listen, Some("127.0.0.1:50102".parse().unwrap())); - } - - #[test] - fn encryption_key_kms_ciphertext_parses_alone() { - let command = parse_start(&["--encryption-key.kms-ciphertext", "deadbeef"]) - .expect("KMS ciphertext without a hex key must parse"); - let ValidatorCommand::Start { encryption_key, .. } = command else { - panic!("expected the start command"); - }; - assert_eq!(encryption_key.encryption_key_kms_ciphertext.as_deref(), Some("deadbeef")); - assert_eq!(encryption_key.encryption_key, None); - } - - #[test] - fn encryption_key_hex_and_kms_ciphertext_conflict() { - let result = parse_start(&[ - "--encryption-key.hex", - TEST_ENCRYPTION_KEY_HEX, - "--encryption-key.kms-ciphertext", - "deadbeef", - ]); - let Err(error) = result else { - panic!("hex key and KMS ciphertext together must be rejected"); - }; - assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict); - } - - #[test] - fn storage_key_is_required() { - let Err(error) = ValidatorCommand::try_parse_from( - BASE_START_ARGS.into_iter().chain(ENCRYPTION_KEY_ARGS), - ) else { - panic!("start without a storage key must fail"); - }; - assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); - } - - #[test] - fn partial_storage_key_configuration_fails() { - let Err(error) = ValidatorCommand::try_parse_from( - BASE_START_ARGS.into_iter().chain(ENCRYPTION_KEY_ARGS).chain([ - "--storage-key.epoch", - "0909090909090909090909090909090909090909090909090909090909090909", - ]), - ) else { - panic!("a partial storage key must fail"); - }; - assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); - } - - #[test] - fn keygen_command_parses() { - let command = ValidatorCommand::try_parse_from(["miden-validator", "keygen"]) - .expect("the keygen command must parse"); - assert!(matches!(command, ValidatorCommand::Keygen)); - } - - #[test] - fn private_record_share_command_parses() { - let command = ValidatorCommand::try_parse_from([ - "miden-validator", - "issue-private-record-share", - "--record", - "/tmp/record.bin", - "--output", - "/tmp/share.bin", - "--storage-key.epoch", - "0909090909090909090909090909090909090909090909090909090909090909", - "--storage-key.setup-context", - "/tmp/setup.bin", - "--storage-key.public-key-set", - "/tmp/public.bin", - "--storage-key.secret-share", - "/tmp/secret.bin", - ]) - .expect("the local share command must parse"); - - let ValidatorCommand::IssuePrivateRecordShare(PrivateRecordShareOptions { - record, - output, - .. - }) = command - else { - panic!("expected the private record share command"); - }; - assert_eq!(record, PathBuf::from("/tmp/record.bin")); - assert_eq!(output, PathBuf::from("/tmp/share.bin")); - } - - #[test] - fn private_record_export_command_parses() { - let validator_id = SigningKey::read_from_bytes(&[7; 32]).unwrap().public_key().to_bytes(); - let command = ValidatorCommand::try_parse_from([ - "miden-validator", - "export-private-record", - "--data-directory", - "/tmp/validator-data", - "--transaction-id", - "0101010101010101010101010101010101010101010101010101010101010101", - "--validator-id", - &hex::encode(validator_id), - "--output", - "/tmp/record.bin", - ]) - .expect("the private record export command must parse"); - - let ValidatorCommand::ExportPrivateRecord(PrivateRecordExportOptions { - data_directory, - transaction_id, - output, - .. - }) = command - else { - panic!("expected the private record export command"); - }; - assert_eq!(data_directory, PathBuf::from("/tmp/validator-data")); - assert_eq!( - transaction_id, - "0101010101010101010101010101010101010101010101010101010101010101", - ); - assert_eq!(output, PathBuf::from("/tmp/record.bin")); - } - - #[test] - fn private_record_share_output_is_required() { - let result = ValidatorCommand::try_parse_from([ - "miden-validator", - "issue-private-record-share", - "--record", - "/tmp/record.bin", - "--storage-key.epoch", - "0909090909090909090909090909090909090909090909090909090909090909", - "--storage-key.setup-context", - "/tmp/setup.bin", - "--storage-key.public-key-set", - "/tmp/public.bin", - "--storage-key.secret-share", - "/tmp/secret.bin", - ]); - - let Err(error) = result else { - panic!("share output must be explicit"); - }; - assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); - } - - #[tokio::test] - async fn two_validators_issue_shares_for_third_validator_record() { - let directory = tempfile::tempdir().unwrap(); - let writer = miden_validator::db::setup(directory.path().join("validator.sqlite3")) - .await - .unwrap(); - let operator_keys = test_operator_keys(9, 3); - let validator_signers = [7u8, 8, 9].map(|seed| { - SigningKey::read_from_bytes(&[seed; 32]).expect("test signing key should decode") - }); - let transaction_id = TransactionId::from_raw(Word::from([1u32, 2, 3, 4])); - let inputs = transaction_inputs(); - let mut records = Vec::new(); - for (index, signer) in validator_signers.iter().enumerate() { - let record_id = - miden_validator::PrivateRecordId::new(transaction_id, &signer.public_key()); - let context = miden_validator::PrivateRecordContext::new( - miden_validator::PrivateRecordChainId::new([5; 32]), - operator_keys[index].key_epoch(), - transaction_id, - ); - let mut rng = ChaCha20Rng::from_seed([6 + index as u8; 32]); - records.push( - miden_validator::PrivateRecordSealer::from_operator_key(&operator_keys[index]) - .seal(&mut rng, record_id, context, &inputs.to_bytes()) - .unwrap(), - ); - } - assert_ne!(records[0].record_id(), records[1].record_id()); - assert_ne!(records[1].record_id(), records[2].record_id()); - assert_ne!(records[0].encrypted_record_key(), records[1].encrypted_record_key()); - assert_ne!(records[1].encrypted_record_key(), records[2].encrypted_record_key()); - - let target = records.remove(2); - store_private_record(&writer, target.clone()).await; - let target_file = directory.path().join("target-record.bin"); - let encoded_transaction_id = hex::encode(transaction_id.to_bytes()); - let encoded_validator_id = hex::encode(target.record_id().validator_id()); - export_record_file( - directory.path(), - &encoded_transaction_id, - &encoded_validator_id, - &target_file, - ) - .await; - assert_eq!( - miden_validator::StoredPrivateRecord::read_from_bytes( - &fs_err::read(&target_file).unwrap() - ) - .unwrap(), - target, - ); - - let first_output = directory.path().join("share-1.bin"); - let second_output = directory.path().join("share-2.bin"); - let first_share = issue_share_file(&target_file, &first_output, &operator_keys[0]); - let second_share = issue_share_file(&target_file, &second_output, &operator_keys[1]); - let shares = [first_share, second_share]; - for share in &shares { - let decoded = from_wire_bytes::>(share).unwrap(); - assert_eq!(to_wire_bytes(&decoded), *share); - } - let request = miden_validator::PrivateRecordShareRequest::for_record(&target); - let opened = miden_validator::PrivateRecordCombiner::from_operator_key(&operator_keys[2]) - .unwrap() - .open(&request, &target, &shares) - .unwrap(); - assert_eq!(TransactionInputs::read_from_bytes(&opened).unwrap(), inputs); - - let wrong_epoch = test_operator_keys(10, 3).remove(0); - assert_issue_error(&target_file, &first_output, &wrong_epoch, "key epoch does not match"); - - let wrong_setup = test_operator_keys(9, 7).remove(0); - assert_issue_error(&target_file, &first_output, &wrong_setup, "setup does not match"); - - let missing_validator_id = hex::encode(validator_signers[0].public_key().to_bytes()); - let error = export_private_record::export(PrivateRecordExportOptions { - data_directory: directory.path().to_path_buf(), - transaction_id: encoded_transaction_id, - validator_id: missing_validator_id, - output: target_file, - }) - .await; - assert!(format!("{:#}", error.unwrap_err()).contains("was not found")); - } -} +mod tests; diff --git a/bin/validator/src/commands/tests.rs b/bin/validator/src/commands/tests.rs new file mode 100644 index 0000000000..4bb108d175 --- /dev/null +++ b/bin/validator/src/commands/tests.rs @@ -0,0 +1,477 @@ +use std::collections::BTreeMap; +use std::path::Path; + +use golden_core::{GoldenGroup, GoldenScalar, ParticipantIndex, SessionId}; +use golden_ehtdh1::wire::{from_wire_bytes, to_wire_bytes}; +use golden_ehtdh1::{ + DecryptionShare, + PublicKeySet, + PublicShare, + SecretShare, + SetupContext, + derive_context_session_id, +}; +use golden_halo2curves::golden_group::{Secp256k1GoldenGroup, Secp256k1Scalar}; +use miden_protocol::Word; +use miden_protocol::account::auth::AuthScheme; +use miden_protocol::transaction::{TransactionId, TransactionInputs}; +use miden_protocol::utils::serde::{Deserializable, Serializable}; +use miden_testing::{Auth, MockChainBuilder}; +use rand_chacha_03::ChaCha20Rng; +use rand_chacha_03::rand_core::SeedableRng; + +use super::*; + +type TestStorageGroup = Secp256k1GoldenGroup; + +const TEST_SIGNING_KEY_HEX: &str = + "0101010101010101010101010101010101010101010101010101010101010101"; +const TEST_ENCRYPTION_KEY_HEX: &str = + "0202020202020202020202020202020202020202020202020202020202020202"; + +const BASE_START_ARGS: [&str; 8] = [ + "miden-validator", + "start", + "--listen", + "127.0.0.1:50101", + "--data-directory", + "/tmp/validator-data", + "--signing-key.hex", + TEST_SIGNING_KEY_HEX, +]; +const ENCRYPTION_KEY_ARGS: [&str; 2] = ["--encryption-key.hex", TEST_ENCRYPTION_KEY_HEX]; +const STORAGE_KEY_ARGS: [&str; 8] = [ + "--storage-key.epoch", + "0909090909090909090909090909090909090909090909090909090909090909", + "--storage-key.setup-context", + "/tmp/setup.bin", + "--storage-key.public-key-set", + "/tmp/public.bin", + "--storage-key.secret-share", + "/tmp/secret.bin", +]; + +fn parse_start(extra: &[&str]) -> Result { + ValidatorCommand::try_parse_from( + BASE_START_ARGS + .iter() + .copied() + .chain(extra.iter().copied()) + .chain(STORAGE_KEY_ARGS.iter().copied()), + ) +} +fn test_operator_keys(epoch: u8, setup_marker: u8) -> Vec { + let participants = [1, 2, 3].map(|value| ParticipantIndex::new(value).unwrap()); + let decryption_secret = Secp256k1Scalar::from_u64(11).unwrap(); + let decryption_coefficient = Secp256k1Scalar::from_u64(7).unwrap(); + let context_coefficient = Secp256k1Scalar::from_u64(13).unwrap(); + let mut public_shares = BTreeMap::new(); + let secret_shares = participants.map(|participant| { + let point = participant.to_scalar::().unwrap(); + let decryption = decryption_secret.add(&decryption_coefficient.mul(&point)); + let context = context_coefficient.mul(&point); + public_shares.insert( + participant, + PublicShare { + decryption: TestStorageGroup::mul_generator(&decryption), + context: TestStorageGroup::mul_generator(&context), + }, + ); + SecretShare { participant, decryption, context } + }); + let public_key_set = + PublicKeySet::new(2, TestStorageGroup::mul_generator(&decryption_secret), public_shares) + .unwrap(); + let decryption_session_id = SessionId([2; 32]); + let setup_context = SetupContext { + backend_id: TestStorageGroup::BACKEND_ID.to_owned(), + threshold: 2, + registry_root: [1; 32], + participants: participants.to_vec(), + decryption_session_id, + context_session_id: derive_context_session_id(decryption_session_id), + decryption_transcript_root: [setup_marker; 32], + context_transcript_root: [4; 32], + epoch: [epoch; 32], + }; + secret_shares + .into_iter() + .map(|secret_share| { + GoldenOperatorKey::new( + StorageKeyEpoch::new([epoch; 32]), + setup_context.clone(), + public_key_set.clone(), + secret_share, + ) + .unwrap() + }) + .collect() +} + +fn transaction_inputs() -> TransactionInputs { + let mut builder = MockChainBuilder::new(); + let account = builder + .add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }) + .unwrap(); + builder.build().unwrap().get_transaction_inputs(&account, &[], &[]).unwrap() +} + +fn issue_error(record: &Path, output: &Path, operator_key: &GoldenOperatorKey) -> String { + let error = issue_private_record_share::issue(record, output, operator_key).unwrap_err(); + format!("{error:#}") +} + +fn assert_issue_error( + record: &Path, + output: &Path, + operator_key: &GoldenOperatorKey, + expected: &str, +) { + let error = issue_error(record, output, operator_key); + assert!(error.contains(expected), "expected {expected:?} in {error:?}"); +} + +fn issue_share_file(record: &Path, output: &Path, operator_key: &GoldenOperatorKey) -> Vec { + issue_private_record_share::issue(record, output, operator_key).unwrap(); + fs_err::read(output).unwrap() +} + +async fn store_private_record( + writer: &miden_validator::db::ValidatorDbWriter, + record: miden_validator::StoredPrivateRecord, +) { + writer.insert_validated_private_transaction(record).await.unwrap(); +} + +async fn export_record_file( + data_directory: &Path, + encoded_transaction_id: &str, + encoded_validator_id: &str, + output: &Path, +) { + export_private_record::export(PrivateRecordExportOptions { + data_directory: data_directory.to_path_buf(), + transaction_id: encoded_transaction_id.to_owned(), + validator_id: encoded_validator_id.to_owned(), + output: output.to_path_buf(), + }) + .await + .unwrap(); +} + +const BASE_GENESIS_ARGS: [&str; 6] = [ + "miden-validator", + "genesis", + "--genesis-block-directory", + "/tmp/genesis", + "--accounts-directory", + "/tmp/accounts", +]; + +fn parse_genesis(extra: &[&str]) -> Result { + ValidatorCommand::try_parse_from(BASE_GENESIS_ARGS.iter().copied().chain(extra.iter().copied())) +} + +#[test] +fn genesis_validator_keys_parse_from_repeated_flags() { + let keys = [7u8, 8].map(|seed| { + SigningKey::read_from_bytes(&[seed; 32]).expect("test signing key should decode") + }); + let hex_keys = keys.clone().map(|key| hex::encode(key.public_key().to_bytes())); + let command = + parse_genesis(&["--validator.key", &hex_keys[0], "--validator.key", &hex_keys[1]]) + .expect("genesis with explicit validator keys must parse"); + let ValidatorCommand::Genesis { validator_keys, .. } = command else { + panic!("expected the genesis command"); + }; + assert_eq!(validator_keys, keys.map(|key| key.public_key()).to_vec()); +} + +#[test] +fn genesis_requires_an_explicit_validator_set() { + let Err(error) = parse_genesis(&[]) else { + panic!("genesis without an explicit validator set must be rejected"); + }; + assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); + + let key = SigningKey::read_from_bytes(&[7; 32]).expect("test signing key should decode"); + let key_hex = hex::encode(key.public_key().to_bytes()); + parse_genesis(&["--config", "/tmp/genesis.toml", "--validator.key", &key_hex]) + .expect("--config with an explicit validator set must parse"); +} + +#[test] +fn genesis_rejects_an_invalid_validator_key() { + let Err(error) = parse_genesis(&["--validator.key", "not-hex"]) else { + panic!("an invalid validator key must be rejected"); + }; + assert_eq!(error.kind(), clap::error::ErrorKind::ValueValidation); +} + +#[test] +fn encryption_key_is_required() { + let Err(error) = parse_start(&[]) else { + panic!("start without an encryption key must be rejected"); + }; + assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); +} + +#[test] +fn signing_key_is_required() { + let Err(error) = ValidatorCommand::try_parse_from( + ["miden-validator", "start", "--listen", "127.0.0.1:50101"] + .into_iter() + .chain(["--data-directory", "/tmp/validator-data"]) + .chain(ENCRYPTION_KEY_ARGS) + .chain(STORAGE_KEY_ARGS), + ) else { + panic!("start without a signing key must be rejected"); + }; + assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); +} + +#[test] +fn admin_listener_is_opt_in() { + let command = + parse_start(&ENCRYPTION_KEY_ARGS).expect("start without an admin listener must parse"); + let ValidatorCommand::Start { admin_listen, .. } = command else { + panic!("expected the start command"); + }; + assert_eq!(admin_listen, None); + + let command = parse_start( + &[ENCRYPTION_KEY_ARGS.as_slice(), &["--admin.listen", "127.0.0.1:50102"]].concat(), + ) + .expect("start with an admin listener must parse"); + let ValidatorCommand::Start { admin_listen, .. } = command else { + panic!("expected the start command"); + }; + assert_eq!(admin_listen, Some("127.0.0.1:50102".parse().unwrap())); +} + +#[test] +fn encryption_key_kms_ciphertext_parses_alone() { + let command = parse_start(&["--encryption-key.kms-ciphertext", "deadbeef"]) + .expect("KMS ciphertext without a hex key must parse"); + let ValidatorCommand::Start { encryption_key, .. } = command else { + panic!("expected the start command"); + }; + assert_eq!(encryption_key.encryption_key_kms_ciphertext.as_deref(), Some("deadbeef")); + assert_eq!(encryption_key.encryption_key, None); +} + +#[test] +fn encryption_key_hex_and_kms_ciphertext_conflict() { + let result = parse_start(&[ + "--encryption-key.hex", + TEST_ENCRYPTION_KEY_HEX, + "--encryption-key.kms-ciphertext", + "deadbeef", + ]); + let Err(error) = result else { + panic!("hex key and KMS ciphertext together must be rejected"); + }; + assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict); +} + +#[test] +fn storage_key_is_required() { + let Err(error) = + ValidatorCommand::try_parse_from(BASE_START_ARGS.into_iter().chain(ENCRYPTION_KEY_ARGS)) + else { + panic!("start without a storage key must fail"); + }; + assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); +} + +#[test] +fn partial_storage_key_configuration_fails() { + let Err(error) = ValidatorCommand::try_parse_from( + BASE_START_ARGS.into_iter().chain(ENCRYPTION_KEY_ARGS).chain([ + "--storage-key.epoch", + "0909090909090909090909090909090909090909090909090909090909090909", + ]), + ) else { + panic!("a partial storage key must fail"); + }; + assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); +} + +#[test] +fn keygen_command_parses() { + let command = ValidatorCommand::try_parse_from(["miden-validator", "keygen"]) + .expect("the keygen command must parse"); + assert!(matches!(command, ValidatorCommand::Keygen)); +} + +#[test] +fn private_record_share_command_parses() { + let command = ValidatorCommand::try_parse_from([ + "miden-validator", + "issue-private-record-share", + "--record", + "/tmp/record.bin", + "--output", + "/tmp/share.bin", + "--storage-key.epoch", + "0909090909090909090909090909090909090909090909090909090909090909", + "--storage-key.setup-context", + "/tmp/setup.bin", + "--storage-key.public-key-set", + "/tmp/public.bin", + "--storage-key.secret-share", + "/tmp/secret.bin", + ]) + .expect("the local share command must parse"); + + let ValidatorCommand::IssuePrivateRecordShare(PrivateRecordShareOptions { + record, output, .. + }) = command + else { + panic!("expected the private record share command"); + }; + assert_eq!(record, PathBuf::from("/tmp/record.bin")); + assert_eq!(output, PathBuf::from("/tmp/share.bin")); +} + +#[test] +fn private_record_export_command_parses() { + let validator_id = SigningKey::read_from_bytes(&[7; 32]).unwrap().public_key().to_bytes(); + let command = ValidatorCommand::try_parse_from([ + "miden-validator", + "export-private-record", + "--data-directory", + "/tmp/validator-data", + "--transaction-id", + "0101010101010101010101010101010101010101010101010101010101010101", + "--validator-id", + &hex::encode(validator_id), + "--output", + "/tmp/record.bin", + ]) + .expect("the private record export command must parse"); + + let ValidatorCommand::ExportPrivateRecord(PrivateRecordExportOptions { + data_directory, + transaction_id, + output, + .. + }) = command + else { + panic!("expected the private record export command"); + }; + assert_eq!(data_directory, PathBuf::from("/tmp/validator-data")); + assert_eq!( + transaction_id, + "0101010101010101010101010101010101010101010101010101010101010101", + ); + assert_eq!(output, PathBuf::from("/tmp/record.bin")); +} + +#[test] +fn private_record_share_output_is_required() { + let result = ValidatorCommand::try_parse_from([ + "miden-validator", + "issue-private-record-share", + "--record", + "/tmp/record.bin", + "--storage-key.epoch", + "0909090909090909090909090909090909090909090909090909090909090909", + "--storage-key.setup-context", + "/tmp/setup.bin", + "--storage-key.public-key-set", + "/tmp/public.bin", + "--storage-key.secret-share", + "/tmp/secret.bin", + ]); + + let Err(error) = result else { + panic!("share output must be explicit"); + }; + assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); +} + +#[tokio::test] +async fn two_validators_issue_shares_for_third_validator_record() { + let directory = tempfile::tempdir().unwrap(); + let writer = miden_validator::db::setup(directory.path().join("validator.sqlite3")) + .await + .unwrap(); + let operator_keys = test_operator_keys(9, 3); + let validator_signers = [7u8, 8, 9].map(|seed| { + SigningKey::read_from_bytes(&[seed; 32]).expect("test signing key should decode") + }); + let transaction_id = TransactionId::from_raw(Word::from([1u32, 2, 3, 4])); + let inputs = transaction_inputs(); + let mut records = Vec::new(); + for (index, signer) in validator_signers.iter().enumerate() { + let record_id = miden_validator::PrivateRecordId::new(transaction_id, &signer.public_key()); + let context = miden_validator::PrivateRecordContext::new( + miden_validator::PrivateRecordChainId::new([5; 32]), + operator_keys[index].key_epoch(), + transaction_id, + ); + let mut rng = ChaCha20Rng::from_seed([6 + index as u8; 32]); + records.push( + miden_validator::PrivateRecordSealer::from_operator_key(&operator_keys[index]) + .seal(&mut rng, record_id, context, &inputs.to_bytes()) + .unwrap(), + ); + } + assert_ne!(records[0].record_id(), records[1].record_id()); + assert_ne!(records[1].record_id(), records[2].record_id()); + assert_ne!(records[0].encrypted_record_key(), records[1].encrypted_record_key()); + assert_ne!(records[1].encrypted_record_key(), records[2].encrypted_record_key()); + + let target = records.remove(2); + store_private_record(&writer, target.clone()).await; + let target_file = directory.path().join("target-record.bin"); + let encoded_transaction_id = hex::encode(transaction_id.to_bytes()); + let encoded_validator_id = hex::encode(target.record_id().validator_id()); + export_record_file( + directory.path(), + &encoded_transaction_id, + &encoded_validator_id, + &target_file, + ) + .await; + assert_eq!( + miden_validator::StoredPrivateRecord::read_from_bytes(&fs_err::read(&target_file).unwrap()) + .unwrap(), + target, + ); + + let first_output = directory.path().join("share-1.bin"); + let second_output = directory.path().join("share-2.bin"); + let first_share = issue_share_file(&target_file, &first_output, &operator_keys[0]); + let second_share = issue_share_file(&target_file, &second_output, &operator_keys[1]); + let shares = [first_share, second_share]; + for share in &shares { + let decoded = from_wire_bytes::>(share).unwrap(); + assert_eq!(to_wire_bytes(&decoded), *share); + } + let request = miden_validator::PrivateRecordShareRequest::for_record(&target); + let opened = miden_validator::PrivateRecordCombiner::from_operator_key(&operator_keys[2]) + .unwrap() + .open(&request, &target, &shares) + .unwrap(); + assert_eq!(TransactionInputs::read_from_bytes(&opened).unwrap(), inputs); + + let wrong_epoch = test_operator_keys(10, 3).remove(0); + assert_issue_error(&target_file, &first_output, &wrong_epoch, "key epoch does not match"); + + let wrong_setup = test_operator_keys(9, 7).remove(0); + assert_issue_error(&target_file, &first_output, &wrong_setup, "setup does not match"); + + let missing_validator_id = hex::encode(validator_signers[0].public_key().to_bytes()); + let error = export_private_record::export(PrivateRecordExportOptions { + data_directory: directory.path().to_path_buf(), + transaction_id: encoded_transaction_id, + validator_id: missing_validator_id, + output: target_file, + }) + .await; + assert!(format!("{:#}", error.unwrap_err()).contains("was not found")); +} diff --git a/compose/validator.yml b/compose/validator.yml index 966f48f6b3..d53ab7d9d2 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 84d92b8f0d..cf607ac38d 100644 --- a/docs/external/src/local-network-development.md +++ b/docs/external/src/local-network-development.md @@ -238,6 +238,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 01dd251aff..89f3c2349d 100644 --- a/docs/external/src/network-operator/bootstrap-and-genesis.md +++ b/docs/external/src/network-operator/bootstrap-and-genesis.md @@ -148,13 +148,15 @@ The key each validator operator starts their validator with must match the publi ## 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 fdf0edc99d..9a4f078d7e 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 \