diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..00fc34b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-D warnings" + +jobs: + check: + name: fmt + clippy + test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust + run: rustup toolchain install stable --component rustfmt clippy --profile minimal + - uses: Swatinem/rust-cache@v2 + - name: Format + run: cargo fmt --all -- --check + - name: Clippy + run: cargo clippy --workspace --all-targets -- -D warnings + - name: Test + run: cargo test --workspace diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fa828b8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3638 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e06aeba2c965fc446d13c56a6ccb2631b78445d7544543dd9a25289977630914" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2d2dde4ec1352715595b5cfa6fe2e5b8ebb9da3457b3ee8db0aa2808c069aa" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4982ef9fa54ec9eee841e891e7ddc5434be1250e88de31572e000c888f30b" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529143118c4eeb58c39ecb02319557d512be6c61348486422974ab8e3906b8a8" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7780677247ad3577e3a6a3ebf43f39b325a11d6393db72b2c9968a910d4d13d" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9645250416cbf92454fe61160e17e026e0ce405906a54500b114f923ddffc9" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ee8d222ff0fd3681791979afbf88586ac9f49010d3db96b3cbe4c96759aee3" + +[[package]] +name = "cranelift-control" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591abe6f5312bd2c4220f1b3bead56c2ad00257c52668015ba013b85dcf2a17a" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5300c49cf940526fe771517b3b3eabd5d0ff164ee61698579cf403fe8d3af3c" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4adbf760207fdbbe130f1191cce01cdef66831a9f648b1f39ff2800d126d45" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8315b21ff018226a42a60a4702c2dd75f6447cac26e9bca622e14c22088c2ff5" + +[[package]] +name = "cranelift-native" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d506ef23a60715bde451b06620b14402166ded3b648454fccbf04f3e46a4aa70" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ed47e602652e3410f9387fc0db70fefadcee4d78a78881421aabcab4e26b89" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ducp-conformance" +version = "0.2.0" +dependencies = [ + "borsh", + "ducp-consensus", + "ducp-dvm", + "ducp-governance", + "ducp-ledger", + "ducp-types", + "ducp-verification", + "hex", + "serde", + "serde_json", +] + +[[package]] +name = "ducp-consensus" +version = "0.2.0" +dependencies = [ + "ducp-governance", + "ducp-ledger", + "ducp-types", +] + +[[package]] +name = "ducp-dvm" +version = "0.2.0" +dependencies = [ + "borsh", + "ducp-types", + "wasmtime", + "wat", +] + +[[package]] +name = "ducp-governance" +version = "0.2.0" +dependencies = [ + "ducp-types", + "serde", +] + +[[package]] +name = "ducp-ledger" +version = "0.2.0" +dependencies = [ + "borsh", + "ducp-governance", + "ducp-types", +] + +[[package]] +name = "ducp-node" +version = "0.2.0" +dependencies = [ + "anyhow", + "clap", + "ducp-consensus", + "ducp-dvm", + "ducp-governance", + "ducp-ledger", + "ducp-types", + "ducp-verification", + "hex", + "jsonrpsee", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ducp-types" +version = "0.2.0" +dependencies = [ + "blake3", + "borsh", + "ed25519-dalek", + "getrandom 0.2.17", + "hex", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "ducp-verification" +version = "0.2.0" +dependencies = [ + "ducp-dvm", + "ducp-types", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" +dependencies = [ + "gloo-timers", + "send_wrapper", +] + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "482ce8a491a501da4cd806bd190275363d674f2845005c6ddbd5d3e1dd54495d" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[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-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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpsee" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3f48dc3e6b8bd21e15436c1ddd0bc22a6a54e8ec46fedd6adf3425f396ec6a" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-proc-macros", + "jsonrpsee-server", + "jsonrpsee-types", + "jsonrpsee-wasm-client", + "jsonrpsee-ws-client", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf36eb27f8e13fa93dcb50ccb44c417e25b818cfa1a481b5470cd07b19c60b98" +dependencies = [ + "base64", + "futures-channel", + "futures-util", + "gloo-net", + "http", + "jsonrpsee-core", + "pin-project", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "soketto", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "316c96719901f05d1137f19ba598b5fe9c9bc39f4335f67f6be8613921946480" +dependencies = [ + "async-trait", + "bytes", + "futures-timer", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "parking_lot", + "pin-project", + "rand 0.9.4", + "rustc-hash", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tower", + "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" +dependencies = [ + "base64", + "http-body", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tower", + "url", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da3f8ab5ce1bb124b6d082e62dffe997578ceaf0aeb9f3174a214589dc00f07" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c51b7c290bb68ce3af2d029648148403863b982f138484a73f02a9dd52dbd7f" +dependencies = [ + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc88ff4688e43cc3fa9883a8a95c6fa27aa2e76c96e610b737b6554d650d7fd5" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7902885de4779f711a95d82c8da2d7e5f9f3a7c7cfa44d51c067fd1c29d72a3c" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "tower", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79" +dependencies = [ + "http", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "tower", + "url", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[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", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +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-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulley-interpreter" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b92604caae1a1899b6a5b54967289dd538177c626004c91accf9d0ec7e4a12" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7ac85c0bb3fb351f10d531230aaa5e366b46d7c4e5328e5f02801d6dac1165" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[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.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "http", + "httparse", + "log", + "rand 0.8.6", + "sha1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-compose" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b089037d7eb453ed57b560fe7833de0707411c8b9fdc429745ced77e2a1bacb9" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "log", + "petgraph", + "smallvec", + "wasm-encoder 0.251.0", + "wasmparser 0.251.0", + "wat", +] + +[[package]] +name = "wasm-encoder" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a879a421bd17c528b74721b2abf4c62e8f1d1889c2ba8c3c50d02deaf2ce395" +dependencies = [ + "leb128fmt", + "wasmparser 0.251.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" +dependencies = [ + "leb128fmt", + "wasmparser 0.252.0", +] + +[[package]] +name = "wasmparser" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437970b35b1a85cfde9c74b2398352d8d653f3bd8e3a3db0c063ea8f5b4b36ff" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8798c1a699bd25648b6708eefe94d97c6f9891febb94b42cca1f7a4b086ea64e" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.251.0", +] + +[[package]] +name = "wasmtime" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4213d2f019a5e44aa8a61d8826dd33a505bff79f749b14a8bafd67321cb9351" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.251.0", + "wasmparser 0.251.0", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wat", + "windows-sys 0.61.2", + "wit-parser", +] + +[[package]] +name = "wasmtime-environ" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45863de41977ec6453e859cf843d456fa3fcb45a659b66d16e794f90ec4f5b7" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder 0.251.0", + "wasmparser 0.251.0", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "438bc7dc45fb75297d75f79a9a0ce852345d13ebc6a6863f6f688f013836a9dd" +dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix", + "serde", + "serde_derive", + "sha2", + "toml", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e48f8d4966d62a10b6d70722bc432c1e163890be2801d3b5784589ad36ffc3" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ad5abd5822a22dbf4014475cdfd1fe790707761cd732d74aaa3ba4d5ba489" + +[[package]] +name = "wasmtime-internal-core" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc28372e36eaf8cf70faa83b5779137f7e99c8d18569a125d1580e735cc9e4d" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a433efc6e35112a5457e1dc8bc4d8d39820ac7722267e89bc04e5df641f32124" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.251.0", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a1d3a39d0d210f6b8574ee96a4315e0a14c67f3a1fc3cd5372cb10d2fb4422" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f667288cb4dfa68a4639ffac4d5628535dda64ebdc2b990526efb12b30ba803" +dependencies = [ + "cc", + "object", + "rustix", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba651d44ab0faad4c58106b3adb45068189fb65ef50f0c404b6d9e3bf81a357" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ecc52563b0558af2a7487eb710de07cc4532564b55528876129238e83118cb1" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e747f4a074699ba1b4e4d841fb263f9b7df5bd1555181c4752bf5990d21ba676" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80009f46991622814196d96fac6fc0a938f46b5cba737a8f4e21e24e5a03856f" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-parser", +] + +[[package]] +name = "wast" +version = "252.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942a3449d6a593fccc111a6241c8df52bda168af30e40bf9580d4394d7374c65" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.252.0", +] + +[[package]] +name = "wat" +version = "1.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c72a4ba7088f7bac94cf516e49882bdf97068904a563768cf249efc839ec42cb" +dependencies = [ + "wast", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.8", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +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 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "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]] +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-parser" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e960732e824fab95099971a09e638979347c94ca48568d3c854c945729196947" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.251.0", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index b49d2a2..42e88c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,12 @@ members = [ "crates/ledger", "crates/consensus", "crates/governance", + "crates/conformance", "node", ] [workspace.package] -version = "0.1.0" +version = "0.2.0" edition = "2021" rust-version = "1.75" license = "BUSL-1.1" @@ -20,5 +21,36 @@ repository = "https://github.com/ducp-protocol/ducp-node-rs" homepage = "https://github.com/ducp-protocol/spec" [workspace.dependencies] -# Shared dependencies are declared here and referenced from member crates with -# `.workspace = true` as the implementation grows. +# Internal crates (path-based; shared so member manifests stay terse). +ducp-types = { path = "crates/ducp-types" } +ducp-dvm = { path = "crates/dvm" } +ducp-verification = { path = "crates/verification" } +ducp-ledger = { path = "crates/ledger" } +ducp-consensus = { path = "crates/consensus" } +ducp-governance = { path = "crates/governance" } + +# Canonical codec + hashing + identity (Profile 0 locked choices, spec/implementation/01). +borsh = { version = "1.7", features = ["derive"] } +blake3 = "1.8" +ed25519-dalek = "2" +getrandom = "0.2" + +# JSON-RPC wire layer + async runtime + CLI + logging (node). +serde = { version = "1", features = ["derive"] } +serde_json = "1" +hex = "0.4" +thiserror = "2" +jsonrpsee = { version = "0.26", features = ["server", "client", "macros"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "signal"] } +clap = { version = "4", features = ["derive"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# WebAssembly DVM (spec/implementation/02). +wasmtime = "46" +wat = "1" + +anyhow = "1" + +# Dev / test. +proptest = "1" diff --git a/README.md b/README.md index f027b23..8cb4800 100644 --- a/README.md +++ b/README.md @@ -2,37 +2,93 @@ The reference implementation of the **Decentralized Universal Compute Protocol (DUCP)**, in Rust. -> **Status:** scaffold for spec **v0.1.0** โ€” not yet functional. This repository lays out the architecture; the subsystems are stubs. +> **Status:** **Profile 0** (MVP / devnet) reference node for spec **v0.2.0** โ€” functional end to end. A single-sequencer devnet accepts a task, executes it deterministically, verifies it by sampled re-execution / open challenge, and settles it with real ๐•Œ and Standing accounting. Advanced tiers (TEE/ZK, BFT, trustless energy attestation, on-chain governance) sit behind traits as documented seams. > **"node" means a network participant** โ€” the software you run to take part in the DUCP network (as a Provider, Validator, etc.). It is **not** Node.js and contains no JavaScript. ## What this is -A node implementation of the DUCP protocol. The protocol itself โ€” the white paper and the normative specification โ€” lives in [`ducp-protocol/spec`](https://github.com/ducp-protocol/spec). This repository implements **spec v0.1.0**. +A node implementation of the DUCP protocol. The protocol itself โ€” the white paper and the normative specification โ€” lives in [`ducp-protocol/spec`](https://github.com/ducp-protocol/spec). This repository implements **spec v0.2.0**, **Profile 0** (the build-ready subset in `spec/implementation/`). + +Profile 0 locks the buildable choices โ€” **WebAssembly** IR (wasmtime), **single-sequencer** devnet, **sampled re-execution** โ€” while preserving every protocol invariant. The Quant (โ„š) efficiency observable is recorded as the reward-neutral **(๐•Œ, โ„š)** pair from genesis (โ„š null until energy attestation exists). ## Workspace layout | Crate | Path | Responsibility | |---|---|---| -| `ducp-dvm` | `crates/dvm` | The DUCP Virtual Machine โ€” deterministic execution and ๐•Œ (UCU) metering | -| `ducp-verification` | `crates/verification` | Layered verification: TEE attestation, ZK proofs, sampled re-execution | -| `ducp-ledger` | `crates/ledger` | Settlement of ๐•Œ and the Standing reputation ledger | -| `ducp-consensus` | `crates/consensus` | Transaction ordering and finality | -| `ducp-governance` | `crates/governance` | Reputation-weighted, role-chamber governance | -| `ducp-node` | `node` | The binary that wires the subsystems into a runnable node | +| `ducp-types` | `crates/ducp-types` | Canonical data model: identifiers, tasks, records, txs/blocks, the Compute Proof, and the โ„š types; borsh codec, BLAKE3 hashing, Ed25519 keys | +| `ducp-dvm` | `crates/dvm` | The DUCP Virtual Machine โ€” deterministic WebAssembly execution and fuel-based ๐•Œ metering; the benchmark; the `Ir` registry seam | +| `ducp-verification` | `crates/verification` | Sampled re-execution + challenge; the `EnergyAttestor` seam (โ„š); reserved TEE/ZK verifier seams | +| `ducp-ledger` | `crates/ledger` | The state machine: accounts, ๐•Œ, Standing, the โ„š-ledger, settlement, fraud resolution, clawback/finality | +| `ducp-consensus` | `crates/consensus` | Transaction ordering and finality โ€” `SingleSequencer` (BFT is a later `ConsensusEngine`) | +| `ducp-governance` | `crates/governance` | Static parameter set (devnet defaults); the `ParamSource` on-chain-governance seam | +| `ducp-node` | `node` | The node binary + JSON-RPC server, mempool, scheduler, keystore, blob store; plus the `ducp-worker` load driver | +| `ducp-conformance` | `crates/conformance` | Loads the published golden vectors and checks them against the reference crates | ## Build & run -``` +```bash cargo build -cargo run -p ducp-node +cargo test --workspace # unit + integration + conformance + +# Run a single-sequencer node: +cargo run -p ducp-node -- --listen 127.0.0.1:8645 +``` + +Requires a recent stable Rust toolchain (pinned in `rust-toolchain.toml`); the DVM uses [wasmtime](https://wasmtime.dev/). + +### Devnet demo (1 sequencer + 1 worker) + +```bash +PORT=8650 TASKS=5 scripts/devnet.sh +``` + +This starts a sequencer and runs the beachhead workload against it over JSON-RPC โ€” `submit โ†’ claim โ†’ execute โ†’ proof โ†’ settle`, repeatedly โ€” printing the settled ๐•Œ and the (๐•Œ, โ„š) record for each task. + +## Architecture & milestones + +The node was built along the Profile 0 roadmap (`spec/implementation/README.md`): + +| # | Milestone | What it delivers | +|---|---|---| +| M0 | Data model + codecs | `ducp-types`: canonical borsh encoding, BLAKE3 hashing, Ed25519, the โ„š types | +| M1 | Wasm DVM + metering | deterministic wasmtime runtime, fuel โ†’ ๐•Œ, the benchmark, deterministic failures | +| M2 | Devnet ledger | accounts, escrow, settlement, Standing, the โ„š-ledger; `I-LEDGER-CONSERVE` | +| M3 | End-to-end | single-sequencer consensus + JSON-RPC node; `submit โ†’ settle` over RPC | +| M4 | Verification | sampled re-execution + open challenge; clawback, offsetting burn, fine, Standing floor | +| M5 | Clawback + finality | bonded stake locked then released; settled tx never rewritten (`I-ECON-FINAL`) | +| M6 | Devnet + dogfood | multi-process devnet, state-machine replication, beachhead workload | + +## The Quant (โ„š) efficiency observable + +Every settled task records the reward-neutral **(๐•Œ, โ„š)** pair (DP-0001, spec/09). On the live devnet โ„š is **null** (`NullAttestor`, no energy measured) and the efficiency multiplier on Standing is `1.0`, so base settlement is strictly ๐•Œ-proportional (`I-Q-REWARDNEUTRAL`). The **Sealed-โ„š floor** computation and the three gated Power-Seal checks are implemented (`SealedAttestor`) and verified against the DP-0001 ยง9 / spec/09 ยง10 conformance vector โ€” four providers, identical ๐•Œ and payment, โ„š โ‰ˆ {0.43, 1.00, 1.64, null}. + +## Conformance test vectors + +Published in [`test-vectors/`](test-vectors/) and checked by `ducp-conformance`. Six families: + +| Family | Source | Milestone | +|---|---|---| +| `codec/` | spec/implementation/01 ยง7 | M0 | +| `metering/` | spec/implementation/02 ยง5 | M1 | +| `settlement/` | spec/implementation/04 ยง3 | M2/M3/M5 | +| `fraud/` | spec/implementation/03 ยง4, 04 ยง4 | M4/M5 | +| `replication/` | spec/implementation/04 ยง6 | M6 | +| `q-observable/` | spec/09 ยง10, DP-0001 ยง9 | โ„š | + +Regenerate after an intentional change: + +```bash +cargo run -p ducp-conformance --bin gen-vectors ``` -Requires a recent stable Rust toolchain (pinned in `rust-toolchain.toml`). +## Profile 0 scope & deferred seams + +Out of scope for Profile 0, each represented by a trait so it is additive later: TEE/ZK tiers (`Verifier`), BFT consensus (`ConsensusEngine`), trustless energy attestation and the efficiency bonus (`EnergyAttestor`), multiple IRs (`IrRegistry`), on-chain governance (`ParamSource`), and persistent/Merkle state (`Storage`). Provisional choices (borsh, BLAKE3, Ed25519, `UCU_DECIMALS = 9`, the fuel cost model) are tuned on devnet and frozen toward 1.0. ## Specification <-> implementation -This implementation pins to a specific specification version; the current target is **DUCP spec v0.1.0**. Anything that would change the protocol belongs first as a proposal in the [spec repo](https://github.com/ducp-protocol/spec/tree/main/proposals) โ€” this repository implements the spec, it does not define it. +This implementation pins to a specification version; the current target is **DUCP spec v0.2.0**. Anything that would change the protocol belongs first as a proposal in the [spec repo](https://github.com/ducp-protocol/spec/tree/main/proposals) โ€” this repository implements the spec, it does not define it. ## Contributing diff --git a/crates/conformance/Cargo.toml b/crates/conformance/Cargo.toml new file mode 100644 index 0000000..2f7479d --- /dev/null +++ b/crates/conformance/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ducp-conformance" +description = "DUCP Profile 0 conformance harness: loads the published test vectors and checks them against the reference crates" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +publish = false + +[dependencies] +ducp-types.workspace = true +ducp-dvm.workspace = true +ducp-verification.workspace = true +ducp-ledger.workspace = true +ducp-consensus.workspace = true +ducp-governance.workspace = true +borsh.workspace = true +serde.workspace = true +serde_json.workspace = true +hex.workspace = true diff --git a/crates/conformance/src/bin/gen-vectors.rs b/crates/conformance/src/bin/gen-vectors.rs new file mode 100644 index 0000000..eff272c --- /dev/null +++ b/crates/conformance/src/bin/gen-vectors.rs @@ -0,0 +1,73 @@ +//! Regenerate the committed conformance vector files from the reference crates. +//! +//! Run: `cargo run -p ducp-conformance --bin gen-vectors` +//! +//! Writes the codec/hash family now; later milestones extend this with the +//! metering, settlement, fraud, replication, and โ„š-observable families. + +use std::fs; + +fn main() { + let dir = ducp_conformance::vectors_dir(); + + // --- codec / hash (M0, spec/implementation/01 ยง7) --- + let codec_dir = dir.join("codec"); + fs::create_dir_all(&codec_dir).expect("create codec dir"); + let records = ducp_conformance::codec_records(); + let json = serde_json::to_string_pretty(&records).expect("serialize records"); + let path = codec_dir.join("types.json"); + fs::write(&path, format!("{json}\n")).expect("write codec vectors"); + println!("wrote {} ({} records)", path.display(), records.len()); + + // --- metering (M1, spec/implementation/02 ยง5) --- + let metering_dir = dir.join("metering"); + fs::create_dir_all(&metering_dir).expect("create metering dir"); + let metering = ducp_conformance::metering_records(); + let json = serde_json::to_string_pretty(&metering).expect("serialize metering"); + let path = metering_dir.join("cases.json"); + fs::write(&path, format!("{json}\n")).expect("write metering vectors"); + println!("wrote {} ({} records)", path.display(), metering.len()); + + // --- settlement (M2/M3, spec/implementation/04 ยง3) --- + let settlement_dir = dir.join("settlement"); + fs::create_dir_all(&settlement_dir).expect("create settlement dir"); + let settlement = ducp_conformance::settlement_record(); + let json = serde_json::to_string_pretty(&settlement).expect("serialize settlement"); + let path = settlement_dir.join("happy_path.json"); + fs::write(&path, format!("{json}\n")).expect("write settlement vector"); + println!("wrote {}", path.display()); + + // --- finality / clawback window (M5, spec/implementation/04 ยง3) --- + let finality = ducp_conformance::finality_record(); + let json = serde_json::to_string_pretty(&finality).expect("serialize finality"); + let path = settlement_dir.join("finality.json"); + fs::write(&path, format!("{json}\n")).expect("write finality vector"); + println!("wrote {}", path.display()); + + // --- fraud (M4/M5, spec/implementation/03 ยง4, 04 ยง4) --- + let fraud_dir = dir.join("fraud"); + fs::create_dir_all(&fraud_dir).expect("create fraud dir"); + let fraud = ducp_conformance::fraud_record(); + let json = serde_json::to_string_pretty(&fraud).expect("serialize fraud"); + let path = fraud_dir.join("challenge.json"); + fs::write(&path, format!("{json}\n")).expect("write fraud vector"); + println!("wrote {}", path.display()); + + // --- replication (M6, spec/implementation/04 ยง6) --- + let replication_dir = dir.join("replication"); + fs::create_dir_all(&replication_dir).expect("create replication dir"); + let replication = ducp_conformance::replication_record(); + let json = serde_json::to_string_pretty(&replication).expect("serialize replication"); + let path = replication_dir.join("blocks.json"); + fs::write(&path, format!("{json}\n")).expect("write replication vector"); + println!("wrote {}", path.display()); + + // --- โ„š observable (spec/09 ยง10, DP-0001 ยง9) --- + let q_dir = dir.join("q-observable"); + fs::create_dir_all(&q_dir).expect("create q-observable dir"); + let q = ducp_conformance::q_observable_record(); + let json = serde_json::to_string_pretty(&q).expect("serialize q-observable"); + let path = q_dir.join("dp0001.json"); + fs::write(&path, format!("{json}\n")).expect("write q-observable vector"); + println!("wrote {}", path.display()); +} diff --git a/crates/conformance/src/lib.rs b/crates/conformance/src/lib.rs new file mode 100644 index 0000000..4ed378a --- /dev/null +++ b/crates/conformance/src/lib.rs @@ -0,0 +1,795 @@ +//! # ducp-conformance +//! +//! Profile 0 conformance harness. Loads the published golden vectors from the +//! workspace-root `test-vectors/` directory and exposes helpers + canonical sample +//! values the per-milestone integration tests (under `tests/`) check the reference +//! crates against. +//! +//! The six vector families (spec/implementation/05 ยง5, spec/09 ยง10): +//! `codec`, `metering`, `settlement`, `fraud`, `replication`, `q-observable`. +//! +//! Regenerate the committed vector files with the generator binary: +//! `cargo run -p ducp-conformance --bin gen-vectors`. +//! +//! Specification: + +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// Absolute path to the workspace-root `test-vectors/` directory. +pub fn vectors_dir() -> PathBuf { + // CARGO_MANIFEST_DIR = /crates/conformance + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("test-vectors") +} + +/// Load and parse a JSON vector file under `test-vectors//`. +pub fn load_json(family: &str, name: &str) -> T { + let path = vectors_dir().join(family).join(name); + let bytes = + std::fs::read(&path).unwrap_or_else(|e| panic!("read vector {}: {e}", path.display())); + serde_json::from_slice(&bytes) + .unwrap_or_else(|e| panic!("parse vector {}: {e}", path.display())) +} + +/// Decode a `0x`-optional hex string into bytes (vectors store binary as hex). +pub fn unhex(s: &str) -> Vec { + let s = s.strip_prefix("0x").unwrap_or(s); + hex::decode(s).unwrap_or_else(|e| panic!("bad hex {s:?}: {e}")) +} + +/// One codec/hash golden vector (spec/implementation/01 ยง7): a value, its canonical +/// (borsh) bytes as hex, and the BLAKE3-256 of those bytes. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CodecRecord { + pub name: String, + pub value: serde_json::Value, + pub canonical_hex: String, + pub hash: String, +} + +/// Build a [`CodecRecord`] for a value that is both serde- and borsh-encodable. +pub fn codec_record(name: &str, value: &T) -> CodecRecord +where + T: Serialize + borsh::BorshSerialize, +{ + let canonical = ducp_types::canonical_bytes(value); + CodecRecord { + name: name.to_string(), + value: serde_json::to_value(value).expect("serde value"), + canonical_hex: hex::encode(&canonical), + hash: hex::encode(ducp_types::hash_bytes(&canonical)), + } +} + +/// Canonical sample values used to derive the codec golden vectors. Kept in one +/// place so the generator and the test never drift. +pub mod samples { + use ducp_types::*; + + pub fn task_body() -> TaskBody { + TaskBody { + ir: IrId::Wasm, + program: [0x11; 32], + input: [0x22; 32], + limits: Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 1 << 20, + }, + tier: VerificationTier::SampledReexec, + benchmark: 0, + deadline: 100, + failure_policy: FailurePolicy::ReturnOnFailure, + nonce: 7, + } + } + + pub fn submission() -> Submission { + Submission { + task: task_body().task_id(), + requester: keys::identity(&[1u8; 32]), + ucu_count: 0, + fee: UCU_SCALE / 100, + status: TaskStatus::Submitted, + provider: None, + claim_stake: 0, + } + } + + pub fn proof_no_seal() -> ComputeProof { + ComputeProof { + task: task_body().task_id(), + provider: keys::identity(&[2u8; 32]), + output: content_id(b"result-bytes"), + result_hash: hash_bytes(b"result-bytes"), + ucu_count: 4 * UCU_SCALE, + benchmark: 0, + tier_data: TierData::SampledReexec, + power_seal: None, + } + } + + pub fn power_seal() -> PowerSeal { + PowerSeal { + seal_grade: SealGrade::S1Witnessed, + boundary: Boundary::Node, + power_cap_milliwatts: 300_000, + window_millis: 1_000, + t_max_millikelvin: 350_000, + attestation_evidence: content_id(b"attestation-quote"), + benchmark: 0, + } + } + + pub fn proof_with_seal() -> ComputeProof { + ComputeProof { + power_seal: Some(power_seal()), + ..proof_no_seal() + } + } + + pub fn receipt() -> Receipt { + Receipt { + task: task_body().task_id(), + paid_to_provider: 4 * UCU_SCALE, + work_issuance: 4 * UCU_SCALE / 100, + validator_fee: UCU_SCALE / 100, + standing_delta: 4 * UCU_SCALE as Sp, + settled_epoch: 12, + clawback_until: 44, + } + } + + pub fn account() -> Account { + Account { + id: keys::identity(&[2u8; 32]), + balance: 1_000 * UCU_SCALE, + escrowed: 0, + bonded: 2 * UCU_SCALE, + } + } + + pub fn standing() -> StandingRecord { + StandingRecord { + id: keys::identity(&[2u8; 32]), + sp: 123 * UCU_SCALE as Sp, + last_decay_epoch: 12, + strikes: 0, + } + } + + pub fn signed_transfer() -> SignedTx { + SignedTx::sign( + &[7u8; 32], + Tx::Transfer { + to: keys::identity(&[9u8; 32]), + amount: 5 * UCU_SCALE, + }, + 1, + ) + } + + pub fn q_entry_null() -> QLedgerEntry { + QLedgerEntry::unmeasured(task_body().task_id(), 4 * UCU_SCALE, 0) + } + + pub fn q_entry_valued() -> QLedgerEntry { + QLedgerEntry { + task: task_body().task_id(), + ucu: 4 * UCU_SCALE, + q: Some(Quant::from_micro(1_640_000)), + seal_grade: Some(SealGrade::S2Locked), + boundary: Some(Boundary::Chip), + benchmark: 0, + } + } + + pub fn block() -> Block { + let tx = signed_transfer(); + Block { + height: 1, + parent: [0u8; 32], + epoch: 0, + txs: vec![tx.tx_id()], + state_root: hash_bytes(b"state-root-placeholder"), + proposer: keys::identity(&[0xAA; 32]), + } + } +} + +/// One metering golden vector (spec/implementation/02 ยง5): a canonical module + +/// input and its deterministic `{total_fuel, ucu_count, result_hash}` under the +/// devnet benchmark. `total_fuel` is wasmtime-fuel-model-specific (provisional). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MeteringRecord { + pub name: String, + pub input_hex: String, + pub total_fuel: u64, + pub ucu_count: String, + pub result_hash: String, +} + +/// All metering golden records, freshly computed from the reference DVM + the +/// canonical reference/echo workloads under a calibrated devnet benchmark. +pub fn metering_records() -> Vec { + use ducp_dvm::{echo_module, reference_module, Benchmark, Dvm, WasmtimeDvm, REFERENCE_INPUT}; + use ducp_types::{Limits, UCU_SCALE}; + + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let limits = Limits { + max_ucu: 1_000 * UCU_SCALE, + max_memory_bytes: 16 * 1024 * 1024, + }; + + let cases: [(&str, Vec, Vec); 3] = [ + ("reference", reference_module(), REFERENCE_INPUT.to_vec()), + ("echo_empty", echo_module(), Vec::new()), + ("echo_hello_world", echo_module(), b"hello world".to_vec()), + ]; + + cases + .into_iter() + .map(|(name, module, input)| { + let fuel = dvm + .measure_fuel(&module, &input) + .expect("sample workloads complete"); + let outcome = dvm.execute(&module, &input, &limits, &bench); + MeteringRecord { + name: name.to_string(), + input_hex: hex::encode(&input), + total_fuel: fuel, + ucu_count: outcome.ucu_count.to_string(), + result_hash: hex::encode(outcome.result_hash), + } + }) + .collect() +} + +/// The settlement golden vector (spec/implementation/04 ยง3): the post-state of a +/// `submit โ†’ claim โ†’ proof โ†’ settle` happy path. Pins the economic outcome, the +/// Receipt, the (๐•Œ, โ„š) entry (โ„š null in P0), and the `state_root`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SettlementRecord { + pub state_root: String, + pub requester: ducp_types::Account, + pub provider: ducp_types::Account, + pub provider_standing: ducp_types::StandingRecord, + pub receipt: ducp_types::Receipt, + pub q_entry: ducp_types::QLedgerEntry, + pub minted: String, + pub fee_pool: String, +} + +/// The canonical settlement scenario, run against the reference ledger. +pub fn settlement_record() -> SettlementRecord { + use ducp_governance::Params; + use ducp_ledger::{apply, State}; + use ducp_types::{ + content_id, keys, ComputeProof, FailurePolicy, IrId, Limits, SignedTx, TaskBody, TierData, + Tx, VerificationTier, UCU_SCALE, + }; + + let params = Params::devnet(); + let req = keys::identity(&[1u8; 32]); + let prov = keys::identity(&[2u8; 32]); + let s = State::genesis(&[(req, 100 * UCU_SCALE), (prov, 100 * UCU_SCALE)], 0); + + let body = TaskBody { + ir: IrId::Wasm, + program: content_id(b"program"), + input: content_id(b"input"), + limits: Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 1 << 20, + }, + tier: VerificationTier::SampledReexec, + benchmark: 0, + deadline: 100, + failure_policy: FailurePolicy::ReturnOnFailure, + nonce: 1, + }; + let task = body.task_id(); + let s = apply( + &s, + &SignedTx::sign(&[1u8; 32], Tx::SubmitTask(body), 0), + ¶ms, + ) + .unwrap(); + let s = apply( + &s, + &SignedTx::sign(&[2u8; 32], Tx::ClaimTask { task }, 0), + ¶ms, + ) + .unwrap(); + let proof = ComputeProof { + task, + provider: prov, + output: content_id(b"the-output"), + result_hash: ducp_types::hash_bytes(b"the-output"), + ucu_count: 4 * UCU_SCALE, + benchmark: 0, + tier_data: TierData::SampledReexec, + power_seal: None, + }; + let s = apply( + &s, + &SignedTx::sign(&[2u8; 32], Tx::SubmitProof(proof), 1), + ¶ms, + ) + .unwrap(); + + SettlementRecord { + state_root: hex::encode(s.state_root()), + requester: s.accounts[&req], + provider: s.accounts[&prov], + provider_standing: s.standing[&prov], + receipt: s.receipts[&task].clone(), + q_entry: s.q_ledger[&task].clone(), + minted: s.supply.minted.to_string(), + fee_pool: s.fee_pool.to_string(), + } +} + +/// The replication golden vector (spec/implementation/04 ยง6): producing then +/// replaying a sequence of blocks reaches the identical `state_root` โ€” state-machine +/// replication, so the devnet is verifiable even with one proposer. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ReplicationRecord { + pub blocks: u64, + pub block_state_roots: Vec, + pub final_state_root: String, + pub replica_matches: bool, +} + +/// Build a happy-path block sequence and replay it on a fresh replica. +pub fn replication_record() -> ReplicationRecord { + use ducp_consensus::{ConsensusEngine, SingleSequencer}; + use ducp_governance::Params; + use ducp_ledger::State; + use ducp_types::{ + content_id, keys, ComputeProof, FailurePolicy, IrId, Limits, SignedTx, TaskBody, TierData, + Tx, VerificationTier, UCU_SCALE, + }; + + let params = Params::devnet(); + let proposer = keys::identity(&[0u8; 32]); + let req = keys::identity(&[1u8; 32]); + let prov = keys::identity(&[2u8; 32]); + let genesis = State::genesis(&[(req, 100 * UCU_SCALE), (prov, 100 * UCU_SCALE)], 0); + + let body = TaskBody { + ir: IrId::Wasm, + program: content_id(b"program"), + input: content_id(b"input"), + limits: Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 1 << 20, + }, + tier: VerificationTier::SampledReexec, + benchmark: 0, + deadline: 100, + failure_policy: FailurePolicy::ReturnOnFailure, + nonce: 1, + }; + let task = body.task_id(); + let proof = ComputeProof { + task, + provider: prov, + output: content_id(b"out"), + result_hash: ducp_types::hash_bytes(b"out"), + ucu_count: 4 * UCU_SCALE, + benchmark: 0, + tier_data: TierData::SampledReexec, + power_seal: None, + }; + let txs = vec![ + SignedTx::sign(&[1u8; 32], Tx::SubmitTask(body), 0), + SignedTx::sign(&[2u8; 32], Tx::ClaimTask { task }, 0), + SignedTx::sign(&[2u8; 32], Tx::SubmitProof(proof), 1), + SignedTx::sign( + &[2u8; 32], + Tx::Transfer { + to: req, + amount: UCU_SCALE, + }, + 2, + ), + ]; + + // Producer: one block per tx. + let mut prod_seq = SingleSequencer::new(proposer); + let mut prod_state = genesis.clone(); + let mut blocks = Vec::new(); + for tx in &txs { + let proposal = prod_seq.produce(std::slice::from_ref(tx), &prod_state, ¶ms); + if !proposal.block.txs.is_empty() { + prod_seq.adopt(&proposal.block); + prod_state = proposal.state.clone(); + blocks.push((proposal.block.clone(), proposal.txs.clone())); + } + } + let final_state_root = prod_state.state_root(); + + // Replica: replay the blocks. + let mut rep_seq = SingleSequencer::new(proposer); + let mut rep_state = genesis.clone(); + let mut block_state_roots = Vec::new(); + for (block, btxs) in &blocks { + rep_state = rep_seq + .commit(block, btxs, &rep_state, ¶ms) + .expect("replay"); + rep_seq.adopt(block); + block_state_roots.push(hex::encode(rep_state.state_root())); + } + + ReplicationRecord { + blocks: blocks.len() as u64, + block_state_roots, + replica_matches: rep_state.state_root() == final_state_root, + final_state_root: hex::encode(final_state_root), + } +} + +/// The finality golden vector (spec/implementation/04 ยง3): a settled task whose +/// clawback window closes, releasing the claim stake while the Receipt stays +/// immutable (`I-ECON-FINAL`). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FinalityRecord { + pub epoch: u64, + pub provider: ducp_types::Account, + pub receipt: ducp_types::Receipt, + pub released: bool, + pub receipt_unchanged: bool, + pub conserved: bool, + pub state_root: String, +} + +/// Build the finality scenario: settle, then advance past the clawback window. +pub fn finality_record() -> FinalityRecord { + use ducp_governance::Params; + use ducp_ledger::{advance_to_epoch, apply, State}; + use ducp_types::{ + content_id, keys, ComputeProof, FailurePolicy, IrId, Limits, SignedTx, TaskBody, TierData, + Tx, VerificationTier, UCU_SCALE, + }; + + let params = Params::devnet(); + let req = keys::identity(&[1u8; 32]); + let prov = keys::identity(&[2u8; 32]); + let s = State::genesis(&[(req, 100 * UCU_SCALE), (prov, 100 * UCU_SCALE)], 0); + + let body = TaskBody { + ir: IrId::Wasm, + program: content_id(b"program"), + input: content_id(b"input"), + limits: Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 1 << 20, + }, + tier: VerificationTier::SampledReexec, + benchmark: 0, + deadline: 100, + failure_policy: FailurePolicy::ReturnOnFailure, + nonce: 1, + }; + let task = body.task_id(); + let s = apply( + &s, + &SignedTx::sign(&[1u8; 32], Tx::SubmitTask(body), 0), + ¶ms, + ) + .unwrap(); + let s = apply( + &s, + &SignedTx::sign(&[2u8; 32], Tx::ClaimTask { task }, 0), + ¶ms, + ) + .unwrap(); + let proof = ComputeProof { + task, + provider: prov, + output: content_id(b"out"), + result_hash: ducp_types::hash_bytes(b"out"), + ucu_count: 4 * UCU_SCALE, + benchmark: 0, + tier_data: TierData::SampledReexec, + power_seal: None, + }; + let s = apply( + &s, + &SignedTx::sign(&[2u8; 32], Tx::SubmitProof(proof), 1), + ¶ms, + ) + .unwrap(); + let receipt_before = s.receipts[&task].clone(); + + let s = advance_to_epoch(&s, params.clawback_epochs, ¶ms); + + FinalityRecord { + epoch: s.epoch, + provider: s.accounts[&prov], + receipt: s.receipts[&task].clone(), + released: s.released.contains(&task), + receipt_unchanged: s.receipts[&task] == receipt_before, + conserved: s.check_conservation(), + state_root: hex::encode(s.state_root()), + } +} + +/// The fraud golden vector (spec/implementation/03 ยง4, 04 ยง4): a forged proof, the +/// re-execution verdict, and the post-resolution state (clawback, burn, fine, +/// Standing floor). Verifies `I-LEDGER-CONSERVE` across the fraud path. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FraudRecord { + pub forged_result_hash: String, + pub true_result_hash: String, + pub verdict_is_fraud: bool, + pub requester: ducp_types::Account, + pub provider: ducp_types::Account, + pub challenger: ducp_types::Account, + pub provider_standing: ducp_types::StandingRecord, + pub minted: String, + pub burned: String, + pub fee_pool: String, + pub conserved: bool, + pub state_root: String, +} + +/// Build the fraud scenario against the reference crates: settle a forged proof, +/// re-execute it, and resolve the challenge. +pub fn fraud_record() -> FraudRecord { + use ducp_dvm::{echo_module, Benchmark, Dvm, WasmtimeDvm}; + use ducp_governance::Params; + use ducp_ledger::{apply, resolve_challenge, State}; + use ducp_types::{ + content_id, keys, ComputeProof, FailurePolicy, IrId, Limits, SignedTx, TaskBody, TierData, + Tx, VerificationTier, UCU_SCALE, + }; + use ducp_verification::{SampledReexecVerifier, Verifier}; + + let params = Params::devnet(); + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + + let req = keys::identity(&[1u8; 32]); + let prov = keys::identity(&[2u8; 32]); + let chal = keys::identity(&[3u8; 32]); + let s = State::genesis( + &[ + (req, 100 * UCU_SCALE), + (prov, 100 * UCU_SCALE), + (chal, 100 * UCU_SCALE), + ], + 0, + ); + + let program = echo_module(); + let input = b"verify".to_vec(); + let body = TaskBody { + ir: IrId::Wasm, + program: content_id(&program), + input: content_id(&input), + limits: Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 16 * 1024 * 1024, + }, + tier: VerificationTier::SampledReexec, + benchmark: 0, + deadline: 100, + failure_policy: FailurePolicy::ReturnOnFailure, + nonce: 1, + }; + let task = body.task_id(); + let s = apply( + &s, + &SignedTx::sign(&[1u8; 32], Tx::SubmitTask(body.clone()), 0), + ¶ms, + ) + .unwrap(); + let s = apply( + &s, + &SignedTx::sign(&[2u8; 32], Tx::ClaimTask { task }, 0), + ¶ms, + ) + .unwrap(); + + let honest = dvm.execute(&program, &input, &body.limits, &bench); + let forged_result_hash = [0xBAu8; 32]; + let proof = ComputeProof { + task, + provider: prov, + output: content_id(b"forged"), + result_hash: forged_result_hash, + ucu_count: honest.ucu_count, + benchmark: 0, + tier_data: TierData::SampledReexec, + power_seal: None, + }; + let s = apply( + &s, + &SignedTx::sign(&[2u8; 32], Tx::SubmitProof(proof.clone()), 1), + ¶ms, + ) + .unwrap(); + + let bond = params.bond_min(honest.ucu_count).max(1); + let s = apply( + &s, + &SignedTx::sign(&[3u8; 32], Tx::Challenge { task, bond }, 0), + ¶ms, + ) + .unwrap(); + + // Forced re-execution โ†’ verdict. + let outcome = SampledReexecVerifier.check(&proof, &program, &input, &body.limits, &bench, &dvm); + let verdict_is_fraud = outcome.is_fraud(); + + let s = resolve_challenge(&s, task, verdict_is_fraud, ¶ms); + + FraudRecord { + forged_result_hash: hex::encode(forged_result_hash), + true_result_hash: hex::encode(honest.result_hash), + verdict_is_fraud, + requester: s.accounts[&req], + provider: s.accounts[&prov], + challenger: s.accounts[&chal], + provider_standing: s.standing[&prov], + minted: s.supply.minted.to_string(), + burned: s.supply.burned.to_string(), + fee_pool: s.fee_pool.to_string(), + conserved: s.check_conservation(), + state_root: hex::encode(s.state_root()), + } +} + +/// One row of the โ„š-observable conformance table (spec/09 ยง10, DP-0001 ยง9). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct QProviderRow { + pub label: String, + pub micro_q: Option, + pub seal_grade: Option, + pub boundary: Option, + pub paid_ucu: String, +} + +/// The โ„š-observable golden vector: four Providers run the same task; all are paid an +/// identical ๐•Œ, recording differing โ„š (and null for the unsealed one) โ€” the +/// reward-neutral (๐•Œ, โ„š) record. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct QObservableRecord { + pub ucu: String, + pub providers: Vec, + pub all_payments_equal: bool, +} + +/// Build the โ„š-observable table from the reference SealedAttestor (spec/09 ยง10). +pub fn q_observable_record() -> QObservableRecord { + use ducp_dvm::Benchmark; + use ducp_types::{content_id, Boundary, PowerSeal, SealGrade, UCU_SCALE}; + use ducp_verification::{EnergyAttestor, SealedAttestor}; + + // โ„š baseline: 13.7 pJ/๐•Œ at 300 K (provisional integer units: 0.1 pJ; mK). + let bench = Benchmark { + version: 0, + fuel_cost_table_hash: [0u8; 32], + fuel_per_ucu: 1, + e_baseline: 137, + t_std_millikelvin: 300_000, + }; + let ucu: ducp_types::Ucu = 50_000 * UCU_SCALE; + + let mk_seal = |power_cap: u64, t_max: u64, grade: SealGrade, boundary: Boundary| PowerSeal { + seal_grade: grade, + boundary, + power_cap_milliwatts: power_cap, // per-๐•Œ energy (window = C) + window_millis: 50_000, + t_max_millikelvin: t_max, + attestation_evidence: content_id(b"root-of-trust-quote"), + benchmark: 0, + }; + + let sealed = [ + ( + "A", + Some((274u64, 350_000u64, SealGrade::S1Witnessed, Boundary::Node)), + ), + ( + "B", + Some((137, 300_000, SealGrade::S2Locked, Boundary::Chip)), + ), + ( + "C", + Some((100, 250_000, SealGrade::S2Locked, Boundary::Chip)), + ), + ("D", None), + ]; + + let providers: Vec = sealed + .into_iter() + .map(|(label, params)| { + let (micro_q, grade, boundary) = match params { + Some((cap, t, g, b)) => { + let q = SealedAttestor + .attest(&mk_seal(cap, t, g, b), ucu, &bench) + .map(|q| q.micro_q); + (q, Some(g), Some(b)) + } + None => (None, None, None), + }; + QProviderRow { + label: label.to_string(), + micro_q, + seal_grade: grade, + boundary, + // Base payment is the metered ๐•Œ โ€” identical for all (reward-neutral). + paid_ucu: ucu.to_string(), + } + }) + .collect(); + + let all_payments_equal = providers.iter().all(|p| p.paid_ucu == ucu.to_string()); + + QObservableRecord { + ucu: ucu.to_string(), + providers, + all_payments_equal, + } +} + +/// All codec golden records, freshly computed from [`samples`]. The committed file +/// `test-vectors/codec/types.json` MUST equal this (it is generated from it). +pub fn codec_records() -> Vec { + use samples as s; + vec![ + codec_record("task_body", &s::task_body()), + codec_record("limits", &s::task_body().limits), + codec_record("submission", &s::submission()), + codec_record("compute_proof_no_seal", &s::proof_no_seal()), + codec_record("power_seal", &s::power_seal()), + codec_record("compute_proof_with_seal", &s::proof_with_seal()), + codec_record("receipt", &s::receipt()), + codec_record("account", &s::account()), + codec_record("standing_record", &s::standing()), + codec_record("signed_transfer", &s::signed_transfer()), + codec_record("q_entry_null", &s::q_entry_null()), + codec_record("q_entry_valued", &s::q_entry_valued()), + codec_record("block", &s::block()), + ] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vectors_dir_exists() { + let d = vectors_dir(); + assert!(d.is_dir(), "test-vectors dir missing at {}", d.display()); + } + + #[test] + fn unhex_roundtrip() { + assert_eq!(unhex("0x00ff"), vec![0u8, 255]); + assert_eq!(unhex("00ff"), vec![0u8, 255]); + } + + #[test] + fn all_six_vector_families_present() { + // spec/implementation/05 ยง5 (five) + spec/09 ยง10 (โ„š). + let families = [ + ("codec", "types.json"), + ("metering", "cases.json"), + ("settlement", "happy_path.json"), + ("fraud", "challenge.json"), + ("replication", "blocks.json"), + ("q-observable", "dp0001.json"), + ]; + for (family, file) in families { + let path = vectors_dir().join(family).join(file); + assert!(path.is_file(), "missing vector {}", path.display()); + } + } +} diff --git a/crates/conformance/tests/codec.rs b/crates/conformance/tests/codec.rs new file mode 100644 index 0000000..aadbcfa --- /dev/null +++ b/crates/conformance/tests/codec.rs @@ -0,0 +1,71 @@ +//! M0 conformance: codec / hash golden vectors (spec/implementation/01 ยง7). +//! +//! Pins the canonical (borsh) bytes and BLAKE3-256 hashes of the core data model, +//! including the โ„š types. Regenerate with `cargo run -p ducp-conformance --bin +//! gen-vectors` after an intentional schema change. + +use ducp_conformance::{codec_records, load_json, unhex, CodecRecord}; +use ducp_types::{Block, ComputeProof, QLedgerEntry, SignedTx, TaskBody}; + +fn committed() -> Vec { + load_json("codec", "types.json") +} + +#[test] +fn committed_vectors_match_reference_crate() { + // Golden regression: the published file MUST equal what the crates produce now. + assert_eq!( + committed(), + codec_records(), + "codec vectors drifted from the reference crate; regenerate with gen-vectors if intended" + ); +} + +#[test] +fn hash_is_blake3_of_canonical_bytes() { + for r in committed() { + let canonical = unhex(&r.canonical_hex); + let expected = hex::encode(ducp_types::hash_bytes(&canonical)); + assert_eq!(r.hash, expected, "hash mismatch for {}", r.name); + } +} + +#[test] +fn published_value_decodes_and_reencodes_to_canonical() { + // The serde wire form (hex/decimal strings) MUST deserialize and re-encode to + // the exact canonical bytes โ€” i.e. the JSON-RPC form is faithful to the codec. + for r in committed() { + let canonical = match r.name.as_str() { + "task_body" => roundtrip::(&r), + "compute_proof_no_seal" | "compute_proof_with_seal" => roundtrip::(&r), + "signed_transfer" => roundtrip::(&r), + "q_entry_null" | "q_entry_valued" => roundtrip::(&r), + "block" => roundtrip::(&r), + _ => continue, // covered by the golden-equality test above + }; + assert_eq!( + canonical, r.canonical_hex, + "re-encode mismatch for {}", + r.name + ); + } +} + +fn roundtrip(r: &CodecRecord) -> String +where + T: serde::de::DeserializeOwned + borsh::BorshSerialize, +{ + let typed: T = serde_json::from_value(r.value.clone()) + .unwrap_or_else(|e| panic!("decode {} as typed value: {e}", r.name)); + hex::encode(ducp_types::canonical_bytes(&typed)) +} + +#[test] +fn signed_transfer_vector_signature_verifies() { + let r = committed() + .into_iter() + .find(|r| r.name == "signed_transfer") + .expect("signed_transfer vector present"); + let tx: SignedTx = serde_json::from_value(r.value).unwrap(); + assert!(tx.verify_sig(), "published signed tx must verify"); +} diff --git a/crates/conformance/tests/finality.rs b/crates/conformance/tests/finality.rs new file mode 100644 index 0000000..6a677dd --- /dev/null +++ b/crates/conformance/tests/finality.rs @@ -0,0 +1,24 @@ +//! M5 conformance: clawback-window finality (spec/implementation/04 ยง3). +//! +//! After the clawback window closes with no successful challenge, the claim stake is +//! released while the settled Receipt stays immutable (`I-ECON-FINAL`). + +use ducp_conformance::{finality_record, load_json, FinalityRecord}; + +fn committed() -> FinalityRecord { + load_json("settlement", "finality.json") +} + +#[test] +fn committed_matches_reference() { + assert_eq!(committed(), finality_record()); +} + +#[test] +fn stake_released_and_receipt_immutable() { + let r = committed(); + assert!(r.released, "claim stake released after the window"); + assert_eq!(r.provider.bonded, 0, "bond returned to spendable balance"); + assert!(r.receipt_unchanged, "settled Receipt is never rewritten"); + assert!(r.conserved); +} diff --git a/crates/conformance/tests/fraud.rs b/crates/conformance/tests/fraud.rs new file mode 100644 index 0000000..aee6e95 --- /dev/null +++ b/crates/conformance/tests/fraud.rs @@ -0,0 +1,49 @@ +//! M4/M5 conformance: fraud golden vector (spec/implementation/03 ยง4, 04 ยง4). +//! +//! A forged proof is settled optimistically, challenged, re-executed, and slashed. +//! Pins the post-state and verifies conservation across the fraud path. + +use ducp_conformance::{fraud_record, load_json, FraudRecord}; + +fn committed() -> FraudRecord { + load_json("fraud", "challenge.json") +} + +#[test] +fn committed_matches_reference() { + assert_eq!(committed(), fraud_record()); +} + +#[test] +fn re_execution_detects_the_forgery() { + let r = committed(); + assert!(r.verdict_is_fraud); + assert_ne!(r.forged_result_hash, r.true_result_hash); +} + +#[test] +fn penalties_applied_and_standing_floored() { + let r = committed(); + assert_eq!(r.provider_standing.sp, 0, "Standing floored on fraud"); + assert_eq!(r.provider_standing.strikes, 1); + // Offsetting burn (W) + fine remainder both leave supply. + assert_ne!(r.burned, "0", "work-issuance + fine remainder burned"); +} + +#[test] +fn conservation_holds_across_fraud_path() { + let r = committed(); + assert!(r.conserved); + let held = r.requester.balance + + r.requester.escrowed + + r.requester.bonded + + r.provider.balance + + r.provider.escrowed + + r.provider.bonded + + r.challenger.balance + + r.challenger.escrowed + + r.challenger.bonded + + r.fee_pool.parse::().unwrap(); + let circulating = r.minted.parse::().unwrap() - r.burned.parse::().unwrap(); + assert_eq!(held, circulating, "I-LEDGER-CONSERVE across fraud"); +} diff --git a/crates/conformance/tests/metering.rs b/crates/conformance/tests/metering.rs new file mode 100644 index 0000000..36c4ae8 --- /dev/null +++ b/crates/conformance/tests/metering.rs @@ -0,0 +1,55 @@ +//! M1 conformance: metering golden vectors (spec/implementation/02 ยง5). +//! +//! Verifies the deterministic ๐•Œ derivation: the reference workload meters to +//! exactly one ๐•Œ, two independent runs agree, and the committed vectors match the +//! reference DVM. `total_fuel` is wasmtime-fuel-model-specific (provisional); +//! regenerate with `gen-vectors` after a metering-semantics change. + +use ducp_conformance::{load_json, metering_records, MeteringRecord}; +use ducp_dvm::{echo_module, reference_module, Benchmark, Dvm, WasmtimeDvm, REFERENCE_INPUT}; +use ducp_types::{Limits, UCU_SCALE}; + +fn committed() -> Vec { + load_json("metering", "cases.json") +} + +#[test] +fn committed_vectors_match_reference_dvm() { + assert_eq!( + committed(), + metering_records(), + "metering vectors drifted; regenerate with gen-vectors if the fuel model changed" + ); +} + +#[test] +fn reference_workload_is_exactly_one_ucu() { + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let ucu = dvm.meter(&reference_module(), REFERENCE_INPUT, &bench); + assert_eq!(ucu, UCU_SCALE); + + let rec = committed(); + let reference = rec.iter().find(|r| r.name == "reference").unwrap(); + assert_eq!(reference.ucu_count, UCU_SCALE.to_string()); +} + +#[test] +fn two_independent_runs_are_identical() { + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let limits = Limits { + max_ucu: 1_000 * UCU_SCALE, + max_memory_bytes: 16 * 1024 * 1024, + }; + let a = dvm.execute(&echo_module(), b"hello world", &limits, &bench); + let b = dvm.execute(&echo_module(), b"hello world", &limits, &bench); + assert_eq!(a.result_hash, b.result_hash); + assert_eq!(a.ucu_count, b.ucu_count); + assert_eq!(a.output, b.output); + + // โ€ฆand they match the committed result hash. + let rec = committed(); + let hello = rec.iter().find(|r| r.name == "echo_hello_world").unwrap(); + assert_eq!(hex::encode(a.result_hash), hello.result_hash); +} diff --git a/crates/conformance/tests/q_observable.rs b/crates/conformance/tests/q_observable.rs new file mode 100644 index 0000000..600b853 --- /dev/null +++ b/crates/conformance/tests/q_observable.rs @@ -0,0 +1,63 @@ +//! โ„š-observable conformance: the DP-0001 ยง9 / spec/09 ยง10 test vector. +//! +//! Four Providers run one task (๐•Œ = 50,000) against a 13.7 pJ baseline at 300 K. +//! Each MUST receive identical ๐•Œ and identical payment, recording โ„š โ‰ˆ +//! {0.43, 1.00, 1.64, null} โ€” reward-neutral by construction. + +use ducp_conformance::{load_json, q_observable_record, QObservableRecord}; + +fn committed() -> QObservableRecord { + load_json("q-observable", "dp0001.json") +} + +#[test] +fn committed_matches_reference() { + assert_eq!(committed(), q_observable_record()); +} + +#[test] +fn q_values_match_the_vector() { + let r = committed(); + let q = |label: &str| { + r.providers + .iter() + .find(|p| p.label == label) + .unwrap() + .micro_q + }; + assert_eq!(q("A"), Some(428_571)); // 0.43 + assert_eq!(q("B"), Some(1_000_000)); // 1.00 + assert_eq!(q("C"), Some(1_644_000)); // 1.64 + assert_eq!(q("D"), None); // no Power Seal โ†’ โ„š null (I-Q-NULL) +} + +#[test] +fn all_payments_are_identical_reward_neutral() { + let r = committed(); + assert!( + r.all_payments_equal, + "โ„š MUST NOT change payment (I-Q-REWARDNEUTRAL)" + ); + // Every Provider paid the same ๐•Œ regardless of โ„š (including the null one). + for p in &r.providers { + assert_eq!(p.paid_ucu, r.ucu); + } +} + +#[test] +fn q_compared_only_within_grade_and_boundary() { + // I-Q-COMPARE: B and C share (S2, chip) and are comparable; A is (S1, node). + let r = committed(); + let row = |label: &str| { + r.providers + .iter() + .find(|p| p.label == label) + .unwrap() + .clone() + }; + let b = row("B"); + let c = row("C"); + assert_eq!(b.seal_grade, c.seal_grade); + assert_eq!(b.boundary, c.boundary); + assert!(c.micro_q > b.micro_q); // C is more efficient within the same class +} diff --git a/crates/conformance/tests/replication.rs b/crates/conformance/tests/replication.rs new file mode 100644 index 0000000..ecf0b7c --- /dev/null +++ b/crates/conformance/tests/replication.rs @@ -0,0 +1,26 @@ +//! M6 conformance: state-machine replication (spec/implementation/04 ยง6). +//! +//! Producing then replaying a block sequence reaches the identical `state_root`, so +//! every node converges on the same ledger state even with a single proposer. + +use ducp_conformance::{load_json, replication_record, ReplicationRecord}; + +fn committed() -> ReplicationRecord { + load_json("replication", "blocks.json") +} + +#[test] +fn committed_matches_reference() { + assert_eq!(committed(), replication_record()); +} + +#[test] +fn replica_reaches_identical_state_root() { + let r = committed(); + assert!(r.replica_matches, "replica diverged from the proposer"); + assert!(r.blocks >= 3); + assert_eq!( + r.block_state_roots.last().map(String::as_str), + Some(r.final_state_root.as_str()) + ); +} diff --git a/crates/conformance/tests/settlement.rs b/crates/conformance/tests/settlement.rs new file mode 100644 index 0000000..b456020 --- /dev/null +++ b/crates/conformance/tests/settlement.rs @@ -0,0 +1,58 @@ +//! M2/M3 conformance: settlement golden vector (spec/implementation/04 ยง3). +//! +//! Pins the post-state of a happy-path settlement and checks the cross-cutting +//! invariants on the published vector: conservation (`I-LEDGER-CONSERVE`) and the +//! reward-neutral (๐•Œ, โ„š) entry with โ„š null in Profile 0 (`I-Q-NULL`). + +use ducp_conformance::{load_json, settlement_record, SettlementRecord}; + +fn committed() -> SettlementRecord { + load_json("settlement", "happy_path.json") +} + +#[test] +fn committed_matches_reference_ledger() { + assert_eq!(committed(), settlement_record()); +} + +#[test] +fn q_entry_is_reward_neutral_null() { + let r = committed(); + assert!( + r.q_entry.q.is_none(), + "โ„š MUST be null with no Power Seal (I-Q-NULL)" + ); + assert!(r.q_entry.seal_grade.is_none()); + assert!(r.q_entry.boundary.is_none()); + // The ๐•Œ of the pair equals the paid amount โ€” the (๐•Œ, โ„š) record. + assert_eq!(r.q_entry.ucu, r.receipt.paid_to_provider); +} + +#[test] +fn escrow_drained_and_stake_bonded_for_clawback() { + let r = committed(); + assert_eq!( + r.requester.escrowed, 0, + "escrow fully released at settlement" + ); + assert!(r.provider.bonded > 0, "claim stake remains bonded"); + assert_eq!( + r.receipt.clawback_until, 32, + "bond locked for the clawback window" + ); +} + +#[test] +fn conservation_holds_on_published_vector() { + // I-LEDGER-CONSERVE: ฮฃ(balance + escrowed + bonded) + fee_pool == minted โˆ’ burned. + let r = committed(); + let held = r.requester.balance + + r.requester.escrowed + + r.requester.bonded + + r.provider.balance + + r.provider.escrowed + + r.provider.bonded + + r.fee_pool.parse::().unwrap(); + let minted = r.minted.parse::().unwrap(); + assert_eq!(held, minted); +} diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml index c864aaa..0a1eb65 100644 --- a/crates/consensus/Cargo.toml +++ b/crates/consensus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ducp-consensus" -description = "Transaction ordering and finality" +description = "Transaction ordering and finality (Profile 0: single-sequencer devnet)" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -9,3 +9,6 @@ authors.workspace = true repository.workspace = true [dependencies] +ducp-types.workspace = true +ducp-ledger.workspace = true +ducp-governance.workspace = true diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs index 7be3792..65c91e1 100644 --- a/crates/consensus/src/lib.rs +++ b/crates/consensus/src/lib.rs @@ -1,20 +1,296 @@ //! # ducp-consensus //! -//! Transaction ordering and finality. Part of the DUCP reference node. +//! Transaction ordering and finality. Profile 0 ships [`SingleSequencer`]: one +//! designated node orders admitted txs (FIFO by arrival, ties by `TxId`), applies +//! the ledger transition in order, and commits a `state_root`. Other nodes +//! **replay** [`SingleSequencer::commit`] and MUST reach the identical root โ€” this +//! is state-machine replication, so the devnet is verifiable even with one proposer +//! (spec/implementation/04 ยง6). A BFT engine is a later `impl ConsensusEngine` with +//! no change to the ledger. //! //! Specification: -//! -//! Status: scaffold for spec v0.1.0 โ€” not yet implemented. +//! Status: Profile 0 implementation for spec v0.2.0. + +use ducp_governance::Params; +use ducp_ledger::{apply, State}; +use ducp_types::{Block, Epoch, Hash, Identity, Reject, SignedTx, TxId}; /// Returns this crate's version, as declared in `Cargo.toml`. pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") } +/// Per-transaction outcome from a [`Proposal`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TxResult { + pub tx_id: TxId, + pub accepted: bool, + pub reject: Option, +} + +/// The result of producing a block: the block, the accepted transactions (in block +/// order), the post-state, and the per-transaction outcomes. +#[derive(Debug, Clone)] +pub struct Proposal { + pub block: Block, + pub txs: Vec, + pub state: State, + pub results: Vec, +} + +/// The consensus interface (spec/implementation/04 ยง6). `produce` orders + applies +/// admitted txs into a candidate block; `commit` deterministically replays a block, +/// which is how replicas reach the identical `state_root`. +pub trait ConsensusEngine { + /// Order and apply admitted txs into a candidate block on top of `state`. + fn produce(&self, mempool: &[SignedTx], state: &State, params: &Params) -> Proposal; + + /// Replay a block's transactions on `state`, returning the new state. Fails if + /// the resolved txs do not match `block.txs` or the recomputed `state_root` + /// differs from `block.state_root`. + fn commit( + &self, + block: &Block, + txs: &[SignedTx], + state: &State, + params: &Params, + ) -> Result; +} + +/// The Profile 0 single-sequencer engine. Tracks the chain head (`height`, +/// `parent`) and the current `epoch`. `produce`/`commit` are pure with respect to +/// the head; the head advances only via [`SingleSequencer::adopt`]. +#[derive(Debug, Clone)] +pub struct SingleSequencer { + proposer: Identity, + height: u64, + parent: Hash, + epoch: Epoch, +} + +impl SingleSequencer { + /// New sequencer at genesis (height 0, zero parent, epoch 0). + pub fn new(proposer: Identity) -> Self { + SingleSequencer { + proposer, + height: 0, + parent: [0u8; 32], + epoch: 0, + } + } + + pub fn height(&self) -> u64 { + self.height + } + + pub fn parent(&self) -> Hash { + self.parent + } + + pub fn epoch(&self) -> Epoch { + self.epoch + } + + pub fn proposer(&self) -> Identity { + self.proposer + } + + /// Advance the head to a just-produced/committed block. + pub fn adopt(&mut self, block: &Block) { + self.height = block.height; + self.parent = block.block_hash(); + self.epoch = block.epoch; + } + + /// Seal a system state transition (e.g. fraud resolution or epoch advance) that + /// did not arise from user transactions, as an empty-tx block committing to the + /// post-state's `state_root`. The caller adopts the returned block. + pub fn seal_block(&self, post_state: &State) -> Block { + Block { + height: self.height + 1, + parent: self.parent, + epoch: self.epoch, + txs: Vec::new(), + state_root: post_state.state_root(), + proposer: self.proposer, + } + } + + /// Advance the epoch boundary: apply Standing decay and bond release via the + /// ledger, returning the new state. The next produced block carries the new + /// epoch. + pub fn advance_epoch(&mut self, state: &State, params: &Params) -> State { + let s = ducp_ledger::advance_epoch(state, params); + self.epoch = s.epoch; + s + } + + fn next_epoch_for_block(&self) -> Epoch { + self.epoch + } +} + +impl ConsensusEngine for SingleSequencer { + fn produce(&self, mempool: &[SignedTx], state: &State, params: &Params) -> Proposal { + let mut s = state.clone(); + let mut accepted_ids: Vec = Vec::new(); + let mut accepted_txs: Vec = Vec::new(); + let mut results: Vec = Vec::new(); + + // FIFO by arrival (mempool order); arrival is total, so no ties to break. + for tx in mempool { + let id = tx.tx_id(); + match apply(&s, tx, params) { + Ok(ns) => { + s = ns; + accepted_ids.push(id); + accepted_txs.push(tx.clone()); + results.push(TxResult { + tx_id: id, + accepted: true, + reject: None, + }); + } + Err(e) => results.push(TxResult { + tx_id: id, + accepted: false, + reject: Some(e), + }), + } + } + + let block = Block { + height: self.height + 1, + parent: self.parent, + epoch: self.next_epoch_for_block(), + txs: accepted_ids, + state_root: s.state_root(), + proposer: self.proposer, + }; + + Proposal { + block, + txs: accepted_txs, + state: s, + results, + } + } + + fn commit( + &self, + block: &Block, + txs: &[SignedTx], + state: &State, + params: &Params, + ) -> Result { + if txs.len() != block.txs.len() { + return Err(Reject::Invalid); + } + let mut s = state.clone(); + for (tx, expected_id) in txs.iter().zip(block.txs.iter()) { + if tx.tx_id() != *expected_id { + return Err(Reject::Invalid); + } + // Every tx in a committed block was accepted by the proposer, so a + // rejection here means divergence. + s = apply(&s, tx, params)?; + } + if s.state_root() != block.state_root { + return Err(Reject::Invalid); + } + Ok(s) + } +} + #[cfg(test)] mod tests { + use super::*; + use ducp_types::{keys, Tx, UCU_SCALE}; + + fn seed(n: u8) -> [u8; 32] { + [n; 32] + } + #[test] fn version_is_set() { - assert!(!super::version().is_empty()); + assert!(!version().is_empty()); + } + + #[test] + fn produce_then_replica_commit_reach_identical_root() { + let params = Params::devnet(); + let alice = keys::identity(&seed(1)); + let bob = keys::identity(&seed(2)); + let state = State::genesis(&[(alice, 100 * UCU_SCALE)], 0); + + let seq = SingleSequencer::new(alice); + let tx = SignedTx::sign( + &seed(1), + Tx::Transfer { + to: bob, + amount: 10 * UCU_SCALE, + }, + 0, + ); + let proposal = seq.produce(std::slice::from_ref(&tx), &state, ¶ms); + assert_eq!(proposal.block.txs.len(), 1); + assert!(proposal.results[0].accepted); + + // A replica replays the same block and reaches the identical root. + let replica = SingleSequencer::new(alice); + let replayed = replica + .commit(&proposal.block, &proposal.txs, &state, ¶ms) + .unwrap(); + assert_eq!(replayed.state_root(), proposal.block.state_root); + assert_eq!(replayed.state_root(), proposal.state.state_root()); + assert_eq!(replayed.balance(&bob), 10 * UCU_SCALE); + } + + #[test] + fn rejected_tx_is_excluded_from_block() { + let params = Params::devnet(); + let alice = keys::identity(&seed(1)); + let bob = keys::identity(&seed(2)); + let state = State::genesis(&[(alice, 5 * UCU_SCALE)], 0); + let seq = SingleSequencer::new(alice); + + // Alice tries to send more than she has. + let tx = SignedTx::sign( + &seed(1), + Tx::Transfer { + to: bob, + amount: 10 * UCU_SCALE, + }, + 0, + ); + let proposal = seq.produce(&[tx], &state, ¶ms); + assert!(proposal.block.txs.is_empty()); + assert!(!proposal.results[0].accepted); + assert_eq!( + proposal.results[0].reject, + Some(Reject::InsufficientBalance) + ); + } + + #[test] + fn adopt_advances_the_head() { + let params = Params::devnet(); + let alice = keys::identity(&seed(1)); + let state = State::genesis(&[(alice, 100 * UCU_SCALE)], 0); + let mut seq = SingleSequencer::new(alice); + assert_eq!(seq.height(), 0); + + let tx = SignedTx::sign( + &seed(1), + Tx::Transfer { + to: [9u8; 32], + amount: 1, + }, + 0, + ); + let p = seq.produce(&[tx], &state, ¶ms); + seq.adopt(&p.block); + assert_eq!(seq.height(), 1); + assert_eq!(seq.parent(), p.block.block_hash()); } } diff --git a/crates/ducp-types/Cargo.toml b/crates/ducp-types/Cargo.toml index f5e64e4..80b469d 100644 --- a/crates/ducp-types/Cargo.toml +++ b/crates/ducp-types/Cargo.toml @@ -9,3 +9,13 @@ authors.workspace = true repository.workspace = true [dependencies] +borsh.workspace = true +blake3.workspace = true +ed25519-dalek.workspace = true +getrandom.workspace = true +serde.workspace = true +hex.workspace = true +thiserror.workspace = true + +[dev-dependencies] +serde_json.workspace = true diff --git a/crates/ducp-types/src/lib.rs b/crates/ducp-types/src/lib.rs index 7f74afa..72e7ec0 100644 --- a/crates/ducp-types/src/lib.rs +++ b/crates/ducp-types/src/lib.rs @@ -1,89 +1,360 @@ //! # ducp-types //! -//! Canonical DUCP data model shared by every conforming node โ€” identifiers, the -//! Compute Proof, and the **Quant (โ„š)** efficiency observable. Field shapes follow -//! the Profile 0 specification (`spec/implementation/01-data-model.md`); canonical -//! byte encoding (`borsh`) and hashing (BLAKE3) are added later and are not yet -//! derived here. +//! Canonical DUCP data model shared by every conforming node โ€” identifiers, tasks, +//! on-ledger records, transactions, blocks, and the **Quant (โ„š)** efficiency +//! observable. Field shapes and encodings follow the Profile 0 specification +//! ([`spec/implementation/01`](https://github.com/ducp-protocol/spec)) and +//! [`spec/09`](https://github.com/ducp-protocol/spec) (DP-0001). //! -//! This scaffold lands the identifiers, the Compute Proof (with its optional -//! [`PowerSeal`]), and the โ„š types introduced by **DP-0001** and **spec/09** as a -//! reward-neutral observable. The remaining records (`Submission`, `Receipt`, -//! `Account`, `State`) are added as the node grows. +//! ## Encoding & hashing (spec/implementation/01 ยง1) +//! - **Canonical bytes**: `borsh`, fields in declaration order; no floats in any +//! hashed structure (the โ„š types are integer-only by construction). +//! - **Hash**: BLAKE3-256 over canonical bytes ([`hash_canonical`]). +//! - **Identity/Signature**: Ed25519 (see [`keys`]). +//! - **Amounts**: integer base units, `1 ๐•Œ = 10^9` ([`UCU_SCALE`]). +//! - **Wire form** (JSON-RPC): binary as hex strings, amounts as decimal strings +//! (see the `serde(with = ...)` field attributes). //! //! Specification: -//! -//! Status: scaffold for spec v0.2.0 โ€” data shapes only, not yet operational. +//! Status: Profile 0 implementation for spec v0.2.0. + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; /// Returns this crate's version, as declared in `Cargo.toml`. pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") } -// ----- Identifiers (spec/implementation/01 ยง2) ----- +// ============================ Identifiers (01 ยง2) ============================ pub type Hash = [u8; 32]; pub type Identity = [u8; 32]; // Ed25519 public key (Profile 0) +pub type Signature = [u8; 64]; // Ed25519 signature pub type ContentId = Hash; // hash of an off-ledger payload -pub type TaskId = Hash; +pub type TaskId = Hash; // = hash(canonical(TaskBody)) +pub type TxId = Hash; // = hash(canonical(SignedTx)) pub type Ucu = u128; // base units; 1 ๐•Œ = 1_000_000_000 pub type Sp = i128; // Standing points, base scale +pub type Epoch = u64; pub type BenchmarkVersion = u32; -// ----- Verification evidence (spec/implementation/01 ยง4) ----- +/// Unit precision: `1 ๐•Œ = 10^UCU_DECIMALS` base units (P0-provisional). +pub const UCU_DECIMALS: u32 = 9; +/// `10^UCU_DECIMALS` โ€” the number of base units in one ๐•Œ. +pub const UCU_SCALE: Ucu = 1_000_000_000; +/// Fixed-point scale for โ„š: one โ„š = `MICRO_Q_SCALE` micro-โ„š. +pub const MICRO_Q_SCALE: u64 = 1_000_000; + +// ============================ Hashing (01 ยง1) =============================== + +/// BLAKE3-256 of raw bytes โ€” the content hash of a payload (`ContentId`). +pub fn hash_bytes(bytes: &[u8]) -> Hash { + *blake3::hash(bytes).as_bytes() +} + +/// BLAKE3-256 of a value's canonical (borsh) encoding. +pub fn hash_canonical(value: &T) -> Hash { + hash_bytes(&canonical_bytes(value)) +} + +/// The canonical byte encoding of a value (borsh, declaration order). +pub fn canonical_bytes(value: &T) -> Vec { + borsh::to_vec(value).expect("canonical borsh encoding is infallible for our types") +} + +/// Content-address a payload: `ContentId = BLAKE3(payload)`. +pub fn content_id(payload: &[u8]) -> ContentId { + hash_bytes(payload) +} + +// ===================== Errors (transition rejections) ======================= + +/// Why a transaction or task action was rejected. Stable variants map to +/// JSON-RPC error codes at the node boundary (05 ยง3). +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + thiserror::Error, +)] +pub enum Reject { + #[error("bad signature")] + BadSignature, + #[error("bad nonce")] + BadNonce, + #[error("unknown account")] + UnknownAccount, + #[error("unknown task")] + UnknownTask, + #[error("insufficient balance")] + InsufficientBalance, + #[error("task not in required status")] + BadStatus, + #[error("deadline passed")] + DeadlinePassed, + #[error("task already claimed")] + AlreadyClaimed, + #[error("wrong provider")] + WrongProvider, + #[error("ucu_count exceeds declared limit")] + UcuExceedsLimit, + #[error("benchmark mismatch")] + BenchmarkMismatch, + #[error("not within clawback window")] + NotInClawbackWindow, + #[error("challenge bond below minimum")] + BondTooSmall, + #[error("unsupported wasm feature")] + UnsupportedFeature, + #[error("ledger conservation violated")] + ConservationViolated, + #[error("invalid transaction")] + Invalid, +} + +// ============================ Tasks (01 ยง3) ================================= -/// Verification tier, assigned by the DVM at submit โ€” never chosen (`I-VERIFY-NOCHOICE`). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Intermediate representation. Profile 0 has exactly one IR. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +#[serde(rename_all = "lowercase")] +pub enum IrId { + Wasm, +} + +/// Declared resource caps for a task. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +pub struct Limits { + #[serde(with = "wire::dec_u128")] + pub max_ucu: Ucu, + pub max_memory_bytes: u64, +} + +/// Verification tier, assigned by the DVM at submit โ€” never chosen +/// (`I-VERIFY-NOCHOICE`). Profile 0 assigns `SampledReexec` to every task. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +#[serde(rename_all = "snake_case")] pub enum VerificationTier { - SampledReexec, // Profile 0 + SampledReexec, Tee, Zk, } +/// What happens to a task that fails execution (06). +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +#[serde(rename_all = "snake_case")] +pub enum FailurePolicy { + AutoRequeue, + ReturnOnFailure, + RetryThenReturn { retries: u8 }, +} + +/// What a Requester asks for. Hashed โ†’ [`TaskId`]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct TaskBody { + pub ir: IrId, + #[serde(with = "wire::bytes32")] + pub program: ContentId, + #[serde(with = "wire::bytes32")] + pub input: ContentId, + pub limits: Limits, + pub tier: VerificationTier, + pub benchmark: BenchmarkVersion, + pub deadline: Epoch, + pub failure_policy: FailurePolicy, + pub nonce: u64, +} + +impl TaskBody { + /// `TaskId = hash(canonical(TaskBody))`. + pub fn task_id(&self) -> TaskId { + hash_canonical(self) + } +} + +// ========================= On-ledger records (01 ยง4) ======================== + +/// Lifecycle status of a task on the ledger. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +#[serde(rename_all = "snake_case")] +pub enum TaskStatus { + Submitted, + Matched, + Executing, + Verified, + Settled, + Failed, +} + +/// Created at Submit. The Requester escrows `max_ucu + fee`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct Submission { + #[serde(with = "wire::bytes32")] + pub task: TaskId, + #[serde(with = "wire::bytes32")] + pub requester: Identity, + #[serde(with = "wire::dec_u128")] + pub ucu_count: Ucu, // protocol-derived (I-UNIT-DERIVED); 0 until proof + #[serde(with = "wire::dec_u128")] + pub fee: Ucu, + pub status: TaskStatus, + #[serde(with = "wire::opt_bytes32")] + pub provider: Option, + #[serde(with = "wire::dec_u128")] + pub claim_stake: Ucu, // Standing-discounted (set at Match) +} + /// Tier-specific evidence carried by a [`ComputeProof`]. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "snake_case")] pub enum TierData { - SampledReexec, // Profile 0: determinism enables exact re-check - Tee { attestation: Vec }, // reserved - Zk { proof: Vec }, // reserved + SampledReexec, // Profile 0: determinism enables exact re-check + Tee { + #[serde(with = "wire::bytes_vec")] + attestation: Vec, + }, + Zk { + #[serde(with = "wire::bytes_vec")] + proof: Vec, + }, } -/// The Provider's evidence for a settled task (spec/implementation/01 ยง4). +/// The Provider's evidence for a settled task (01 ยง4). /// /// The optional [`power_seal`](ComputeProof::power_seal) is the energy attestation /// introduced by **DP-0001**: absent by default, it never affects `ucu_count`, /// minting, or proof validity (`I-Q-REWARDNEUTRAL`). When absent, the task's โ„š is /// `None` (`I-Q-NULL`). -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct ComputeProof { + #[serde(with = "wire::bytes32")] pub task: TaskId, + #[serde(with = "wire::bytes32")] pub provider: Identity, + #[serde(with = "wire::bytes32")] pub output: ContentId, + #[serde(with = "wire::bytes32")] pub result_hash: Hash, + #[serde(with = "wire::dec_u128")] pub ucu_count: Ucu, pub benchmark: BenchmarkVersion, pub tier_data: TierData, - /// Optional, reward-neutral energy attestation (DP-0001, spec/09). `None` in Profile 0. + /// Optional, reward-neutral energy attestation (DP-0001, spec/09). `None` in P0. pub power_seal: Option, } -// ----- The efficiency observable โ„š (DP-0001, spec/09) ----- +/// Final settlement effect (recorded at Settle; immutable, `I-ECON-FINAL`). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct Receipt { + #[serde(with = "wire::bytes32")] + pub task: TaskId, + #[serde(with = "wire::dec_u128")] + pub paid_to_provider: Ucu, + #[serde(with = "wire::dec_u128")] + pub work_issuance: Ucu, + #[serde(with = "wire::dec_u128")] + pub validator_fee: Ucu, + #[serde(with = "wire::dec_i128")] + pub standing_delta: Sp, + pub settled_epoch: Epoch, + pub clawback_until: Epoch, +} + +// ===================== Accounts & Standing (01 ยง5) ========================= + +/// A spendable-๐•Œ account. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +pub struct Account { + #[serde(with = "wire::bytes32")] + pub id: Identity, + #[serde(with = "wire::dec_u128")] + pub balance: Ucu, // spendable ๐•Œ + #[serde(with = "wire::dec_u128")] + pub escrowed: Ucu, // locked in open tasks (Requester) + #[serde(with = "wire::dec_u128")] + pub bonded: Ucu, // stake locked in clawback windows (Provider) +} + +impl Account { + /// A fresh, empty account for `id`. + pub fn new(id: Identity) -> Self { + Account { + id, + balance: 0, + escrowed: 0, + bonded: 0, + } + } +} + +/// Separate, non-spendable reputation ledger (`I-STAND-NOTMONEY`). There MUST be +/// no operation converting `sp โ†” balance` (`I-STAND-NOXFER`). +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +pub struct StandingRecord { + #[serde(with = "wire::bytes32")] + pub id: Identity, + #[serde(with = "wire::dec_i128")] + pub sp: Sp, + pub last_decay_epoch: Epoch, + pub strikes: u32, +} + +impl StandingRecord { + /// A fresh Standing record at zero. + pub fn new(id: Identity, epoch: Epoch) -> Self { + StandingRecord { + id, + sp: 0, + last_decay_epoch: epoch, + strikes: 0, + } + } +} + +// ====================== The โ„š observable (DP-0001, 09) ===================== /// Strength of the power-cap attestation behind a [`PowerSeal`] (spec/09 ยง5). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] pub enum SealGrade { /// Self-attested static power cap. Available today; weakest evidence. + #[serde(rename = "S0")] S0Identity, /// Out-of-band, root-of-trustโ€“signed cap or meter (e.g. BMC / smart PDU). + #[serde(rename = "S1")] S1Witnessed, /// Vendor-locked, signed on-die power register. Strongest; not yet available. + #[serde(rename = "S2")] S2Locked, } /// Where energy was bounded/measured (spec/09 ยง5.2). The protocol never fixes a /// single boundary; it records the declared one so โ„š is compared only within an /// identical `(grade, boundary)` (`I-Q-COMPARE`). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +#[serde(rename_all = "lowercase")] pub enum Boundary { Chip, Node, @@ -95,14 +366,33 @@ pub enum Boundary { /// Integer by construction: no floats appear in any hashed structure /// (spec/implementation/01 ยง1). โ„š = 1.0 (`micro_q == 1_000_000`) is frontier-grade; /// below 1.0 is behind the frontier, above 1.0 is ahead of it. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, +)] pub struct Quant { pub micro_q: u64, } impl Quant { /// Frontier-grade efficiency, โ„š = 1.0. - pub const ONE: Quant = Quant { micro_q: 1_000_000 }; + pub const ONE: Quant = Quant { + micro_q: MICRO_Q_SCALE, + }; + + /// Construct from micro-โ„š. + pub const fn from_micro(micro_q: u64) -> Self { + Quant { micro_q } + } } /// Optional energy attestation on a [`ComputeProof`] (spec/09 ยง3). @@ -111,7 +401,7 @@ impl Quant { /// so it is side-channel-safe and signable by existing roots of trust. All fields /// are integers โ€” no floats in hashed data. The recorded โ„š is the *Sealed* lower /// bound `โ‰ฅ (C ยท E_baseline ยท T_std) / (power_cap ยท window ยท T_max)` (spec/09 ยง4). -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct PowerSeal { pub seal_grade: SealGrade, pub boundary: Boundary, @@ -120,6 +410,7 @@ pub struct PowerSeal { pub t_max_millikelvin: u64, /// Evidence chaining the seal to a hardware root of trust and to the Task Hash; /// bulky evidence lives off-ledger, referenced by content id. + #[serde(with = "wire::bytes32")] pub attestation_evidence: ContentId, /// Benchmark epoch supplying `E_baseline ร— T_std` used to compute โ„š. pub benchmark: BenchmarkVersion, @@ -128,9 +419,11 @@ pub struct PowerSeal { /// One entry of the on-chain **โ„š-ledger** (spec/09 ยง7): the `(๐•Œ, โ„š)` pair recorded /// for every settled task. `q` is `None` wherever energy was not validly attested /// (`I-Q-NULL`); recording it never affects settlement (`I-Q-REWARDNEUTRAL`). -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct QLedgerEntry { + #[serde(with = "wire::bytes32")] pub task: TaskId, + #[serde(with = "wire::dec_u128")] pub ucu: Ucu, pub q: Option, pub seal_grade: Option, @@ -153,40 +446,366 @@ impl QLedgerEntry { } } +// ======================== Transactions & blocks (01 ยง6) ==================== + +/// A state-changing operation. Authored and signed in a [`SignedTx`]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "snake_case")] +pub enum Tx { + SubmitTask(TaskBody), + ClaimTask { + #[serde(with = "wire::bytes32")] + task: TaskId, + }, + SubmitProof(ComputeProof), + Challenge { + #[serde(with = "wire::bytes32")] + task: TaskId, + #[serde(with = "wire::dec_u128")] + bond: Ucu, + }, + Transfer { + #[serde(with = "wire::bytes32")] + to: Identity, + #[serde(with = "wire::dec_u128")] + amount: Ucu, + }, +} + +/// A signed transaction. The signature covers `(author โ€– canonical(tx) โ€– nonce)`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct SignedTx { + #[serde(with = "wire::bytes32")] + pub author: Identity, + pub tx: Tx, + pub nonce: u64, + #[serde(with = "wire::bytes64")] + pub sig: Signature, +} + +impl SignedTx { + /// The exact bytes the signature is computed over: `author โ€– canonical(tx) โ€– nonce_le`. + pub fn signing_payload(author: &Identity, tx: &Tx, nonce: u64) -> Vec { + let mut buf = Vec::new(); + buf.extend_from_slice(author); + BorshSerialize::serialize(tx, &mut buf).expect("borsh into Vec is infallible"); + buf.extend_from_slice(&nonce.to_le_bytes()); + buf + } + + /// Construct and sign a transaction with the given Ed25519 signing seed. + pub fn sign(seed: &[u8; 32], tx: Tx, nonce: u64) -> Self { + let author = keys::identity(seed); + let payload = Self::signing_payload(&author, &tx, nonce); + let sig = keys::sign(seed, &payload); + SignedTx { + author, + tx, + nonce, + sig, + } + } + + /// Verify the signature binds `author` to `(tx, nonce)`. + pub fn verify_sig(&self) -> bool { + let payload = Self::signing_payload(&self.author, &self.tx, self.nonce); + keys::verify(&self.author, &payload, &self.sig) + } + + /// `TxId = hash(canonical(SignedTx))`. + pub fn tx_id(&self) -> TxId { + hash_canonical(self) + } +} + +/// A block of ordered transactions and the resulting state commitment. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct Block { + pub height: u64, + #[serde(with = "wire::bytes32")] + pub parent: Hash, + pub epoch: Epoch, + #[serde(with = "wire::vec_bytes32")] + pub txs: Vec, // ordered by the sequencer (consensus, 04) + #[serde(with = "wire::bytes32")] + pub state_root: Hash, // commitment to ledger state after applying txs + #[serde(with = "wire::bytes32")] + pub proposer: Identity, // P0: the single sequencer +} + +impl Block { + /// `hash(canonical(Block))` โ€” the block's identity. + pub fn block_hash(&self) -> Hash { + hash_canonical(self) + } +} + +// ============================ Ed25519 keys ================================= + +/// Ed25519 signing/verification helpers. Keys are raw byte arrays in the data +/// model (`Identity = [u8; 32]`, `Signature = [u8; 64]`); this module is the only +/// place the curve library is touched. +pub mod keys { + use super::{Identity, Signature}; + use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey}; + + /// Derive the public [`Identity`] from a 32-byte signing seed. + pub fn identity(seed: &[u8; 32]) -> Identity { + SigningKey::from_bytes(seed).verifying_key().to_bytes() + } + + /// Sign a message with a 32-byte signing seed. + pub fn sign(seed: &[u8; 32], message: &[u8]) -> Signature { + SigningKey::from_bytes(seed).sign(message).to_bytes() + } + + /// Verify `signature` over `message` against `identity`. Returns `false` on any + /// malformed key/signature rather than panicking. + pub fn verify(identity: &Identity, message: &[u8], signature: &Signature) -> bool { + let Ok(vk) = VerifyingKey::from_bytes(identity) else { + return false; + }; + let sig = ed25519_dalek::Signature::from_bytes(signature); + vk.verify(message, &sig).is_ok() + } + + /// Generate a fresh random 32-byte signing seed from the OS CSPRNG. + pub fn generate_seed() -> [u8; 32] { + let mut seed = [0u8; 32]; + getrandom::getrandom(&mut seed).expect("OS RNG available"); + seed + } +} + +// ===================== Wire (JSON-RPC) serde helpers ======================= + +/// `serde(with = ...)` modules for the JSON-RPC wire form: binary as hex strings, +/// 128-bit amounts as decimal strings. These affect **only** JSON; canonical +/// (borsh) bytes are independent. +mod wire { + use serde::{Deserialize, Deserializer, Serializer}; + + fn from_hex(s: &str) -> Result<[u8; N], E> { + let s = s.strip_prefix("0x").unwrap_or(s); + let v = hex::decode(s).map_err(E::custom)?; + v.try_into() + .map_err(|_| E::custom(format!("expected {N} bytes"))) + } + + pub mod bytes32 { + use super::*; + pub fn serialize(v: &[u8; 32], s: S) -> Result { + s.serialize_str(&hex::encode(v)) + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> { + let s = String::deserialize(d)?; + from_hex::<32, _>(&s) + } + } + + pub mod bytes64 { + use super::*; + pub fn serialize(v: &[u8; 64], s: S) -> Result { + s.serialize_str(&hex::encode(v)) + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 64], D::Error> { + let s = String::deserialize(d)?; + from_hex::<64, _>(&s) + } + } + + pub mod opt_bytes32 { + use super::*; + pub fn serialize(v: &Option<[u8; 32]>, s: S) -> Result { + match v { + Some(b) => s.serialize_some(&hex::encode(b)), + None => s.serialize_none(), + } + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let o: Option = Option::deserialize(d)?; + match o { + None => Ok(None), + Some(s) => Ok(Some(from_hex::<32, _>(&s)?)), + } + } + } + + pub mod vec_bytes32 { + use super::*; + pub fn serialize(v: &[[u8; 32]], s: S) -> Result { + let hexed: Vec = v.iter().map(hex::encode).collect(); + serde::Serialize::serialize(&hexed, s) + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let items: Vec = Vec::deserialize(d)?; + items.iter().map(|s| from_hex::<32, _>(s)).collect() + } + } + + pub mod bytes_vec { + use super::*; + pub fn serialize(v: &[u8], s: S) -> Result { + s.serialize_str(&hex::encode(v)) + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let s = String::deserialize(d)?; + let s = s.strip_prefix("0x").unwrap_or(&s); + hex::decode(s).map_err(serde::de::Error::custom) + } + } + + pub mod dec_u128 { + use super::*; + pub fn serialize(v: &u128, s: S) -> Result { + s.serialize_str(&v.to_string()) + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let s = String::deserialize(d)?; + s.parse().map_err(serde::de::Error::custom) + } + } + + pub mod dec_i128 { + use super::*; + pub fn serialize(v: &i128, s: S) -> Result { + s.serialize_str(&v.to_string()) + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let s = String::deserialize(d)?; + s.parse().map_err(serde::de::Error::custom) + } + } +} + +// ================================ Tests ==================================== + #[cfg(test)] mod tests { use super::*; + fn sample_task_body() -> TaskBody { + TaskBody { + ir: IrId::Wasm, + program: [0x11; 32], + input: [0x22; 32], + limits: Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 1 << 20, + }, + tier: VerificationTier::SampledReexec, + benchmark: 0, + deadline: 100, + failure_policy: FailurePolicy::ReturnOnFailure, + nonce: 7, + } + } + #[test] fn version_is_set() { assert!(!version().is_empty()); } #[test] - fn profile0_proof_carries_no_power_seal() { - let proof = ComputeProof { - task: [0u8; 32], - provider: [0u8; 32], - output: [0u8; 32], - result_hash: [0u8; 32], - ucu_count: 4_000_000_000, // 4 ๐•Œ - benchmark: 0, - tier_data: TierData::SampledReexec, - power_seal: None, + fn borsh_roundtrip_taskbody() { + let body = sample_task_body(); + let bytes = canonical_bytes(&body); + let back: TaskBody = borsh::from_slice(&bytes).unwrap(); + assert_eq!(body, back); + } + + #[test] + fn task_id_is_hash_of_canonical() { + let body = sample_task_body(); + assert_eq!(body.task_id(), hash_bytes(&canonical_bytes(&body))); + } + + #[test] + fn json_roundtrip_uses_hex_and_decimal_strings() { + let body = sample_task_body(); + let json = serde_json::to_string(&body).unwrap(); + // hex for content ids, decimal string for amounts. + assert!(json.contains(&"11".repeat(32))); + assert!(json.contains("10000000000")); // 10 * 1e9 as a string + let back: TaskBody = serde_json::from_str(&json).unwrap(); + assert_eq!(body, back); + } + + #[test] + fn signing_and_verification_roundtrip() { + let seed = [3u8; 32]; + let tx = Tx::Transfer { + to: [9u8; 32], + amount: 5 * UCU_SCALE, }; - assert!(proof.power_seal.is_none()); + let signed = SignedTx::sign(&seed, tx, 1); + assert_eq!(signed.author, keys::identity(&seed)); + assert!(signed.verify_sig()); + + // Tamper: flip the nonce โ†’ signature no longer verifies. + let mut bad = signed.clone(); + bad.nonce = 2; + assert!(!bad.verify_sig()); } #[test] - fn unmeasured_entry_is_reward_neutral_null() { - let e = QLedgerEntry::unmeasured([1u8; 32], 4_000_000_000, 0); + fn tx_id_changes_with_content() { + let a = SignedTx::sign( + &[1u8; 32], + Tx::Transfer { + to: [2u8; 32], + amount: 1, + }, + 0, + ); + let b = SignedTx::sign( + &[1u8; 32], + Tx::Transfer { + to: [2u8; 32], + amount: 2, + }, + 0, + ); + assert_ne!(a.tx_id(), b.tx_id()); + } + + #[test] + fn q_unmeasured_is_reward_neutral_null() { + let e = QLedgerEntry::unmeasured([1u8; 32], 4 * UCU_SCALE, 0); assert_eq!(e.q, None); assert_eq!(e.seal_grade, None); - assert_eq!(e.ucu, 4_000_000_000); // ๐•Œ present regardless of โ„š + assert_eq!(e.ucu, 4 * UCU_SCALE); } #[test] fn quant_one_is_frontier_grade() { - assert_eq!(Quant::ONE.micro_q, 1_000_000); + assert_eq!(Quant::ONE.micro_q, MICRO_Q_SCALE); + } + + #[test] + fn seal_grade_serde_uses_short_names() { + assert_eq!( + serde_json::to_string(&SealGrade::S0Identity).unwrap(), + "\"S0\"" + ); + assert_eq!(serde_json::to_string(&Boundary::Chip).unwrap(), "\"chip\""); + } + + #[test] + fn no_power_seal_means_proof_still_well_formed() { + let proof = ComputeProof { + task: [0u8; 32], + provider: [0u8; 32], + output: [0u8; 32], + result_hash: [0u8; 32], + ucu_count: 4 * UCU_SCALE, + benchmark: 0, + tier_data: TierData::SampledReexec, + power_seal: None, + }; + let bytes = canonical_bytes(&proof); + let back: ComputeProof = borsh::from_slice(&bytes).unwrap(); + assert_eq!(proof, back); + assert!(proof.power_seal.is_none()); } } diff --git a/crates/dvm/Cargo.toml b/crates/dvm/Cargo.toml index e588c90..21a6652 100644 --- a/crates/dvm/Cargo.toml +++ b/crates/dvm/Cargo.toml @@ -9,3 +9,7 @@ authors.workspace = true repository.workspace = true [dependencies] +ducp-types.workspace = true +wasmtime.workspace = true +wat.workspace = true +borsh.workspace = true diff --git a/crates/dvm/src/lib.rs b/crates/dvm/src/lib.rs index 5751d7d..0dfa19a 100644 --- a/crates/dvm/src/lib.rs +++ b/crates/dvm/src/lib.rs @@ -1,20 +1,617 @@ //! # ducp-dvm //! -//! DUCP Virtual Machine: standard deterministic execution and UCU metering. Part of the DUCP reference node. +//! The Profile 0 DUCP Virtual Machine: a **deterministic WebAssembly runtime** +//! ([wasmtime]) that executes a task once and derives its ๐•Œ count by fuel metering +//! (spec/implementation/02). //! -//! Specification: +//! Determinism (`I-DVM-DET`): NaN canonicalization on; single-threaded; no ambient +//! capabilities (no clock, randomness, filesystem, or network); the only host +//! imports are the deterministic `ducp` ABI (ยง3); fixed memory limits. Given the +//! same `{module, input, benchmark}`, every conforming DVM MUST produce the +//! identical `output`, `result_hash`, and `ucu_count`. //! -//! Status: scaffold for spec v0.1.0 โ€” not yet implemented. +//! Specification: +//! Status: Profile 0 implementation for spec v0.2.0. + +use borsh::{BorshDeserialize, BorshSerialize}; +use ducp_types::{hash_bytes, Hash, IrId, Limits, Ucu, UCU_SCALE}; +use wasmtime::{ + Caller, Config, Engine, Extern, Linker, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, + Trap, +}; /// Returns this crate's version, as declared in `Cargo.toml`. pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") } +/// A generous fuel ceiling for advisory metering ([`Dvm::meter`]), where no task +/// `Limits` are supplied. Large enough that real reference workloads complete. +const METER_FUEL_CEILING: u64 = 1 << 40; + +// =============================== Benchmark ================================= + +/// The single consensus reference that fixes the scale of ๐•Œ (`I-UNIT-ONEBENCH`, +/// spec/implementation/02 ยง5). `fuel_per_ucu` is calibrated from the canonical +/// reference workload so that workload meters to exactly one ๐•Œ. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Benchmark { + pub version: u32, + /// Content hash identifying the fuel cost model in force (provisional: the + /// wasmtime fuel semantics version). + pub fuel_cost_table_hash: Hash, + /// `fuel_ref / UCU_REF`; here `UCU_REF = 1 ๐•Œ`, so this is the reference fuel. + pub fuel_per_ucu: u64, + /// Nominal standard energy per ๐•Œ (provisional integer unit) โ€” the `E_baseline` + /// of the โ„š definition (spec/09 ยง4). Recorded for the efficiency observable; does + /// not affect `ucu_count` (`I-UNIT-ENERGYFREE`). + pub e_baseline: u64, + /// Standard temperature in millikelvin โ€” the `T_std` of the โ„š definition + /// (spec/09 ยง4). + pub t_std_millikelvin: u64, +} + +impl Benchmark { + /// Calibrate the devnet benchmark: run the canonical reference workload and set + /// `fuel_per_ucu` to the fuel it consumes (so the reference equals one ๐•Œ). + pub fn devnet(dvm: &WasmtimeDvm) -> Benchmark { + let fuel_ref = dvm + .measure_fuel(&reference_module(), REFERENCE_INPUT) + .expect("reference workload completes within the meter ceiling"); + Benchmark { + version: 0, + fuel_cost_table_hash: hash_bytes(FUEL_COST_MODEL_ID), + fuel_per_ucu: fuel_ref.max(1), + // โ„š baseline (spec/09): 13.7 pJ/๐•Œ frontier energy at 300 K, in the + // provisional integer units used by the Sealed-โ„š floor (0.1 pJ; mK). + e_baseline: 137, + t_std_millikelvin: 300_000, + } + } + + /// Convert a fuel quantity into a ๐•Œ base-unit count under this benchmark + /// (integer division, deterministic): `ucu = fuel * UCU_SCALE / fuel_per_ucu`. + pub fn fuel_to_ucu(&self, fuel: u64) -> Ucu { + (fuel as u128) * (UCU_SCALE) / (self.fuel_per_ucu as u128) + } + + /// Convert a ๐•Œ ceiling into a fuel ceiling: `max_fuel = max_ucu * fuel_per_ucu / UCU_SCALE`. + pub fn ucu_to_fuel_ceiling(&self, max_ucu: Ucu) -> u64 { + let f = max_ucu * (self.fuel_per_ucu as u128) / UCU_SCALE; + f.min(u64::MAX as u128) as u64 + } +} + +/// Identifier of the fuel cost model (content-hashed into the benchmark). Bump when +/// the metering semantics change (e.g. a wasmtime upgrade that alters fuel costs). +const FUEL_COST_MODEL_ID: &[u8] = b"ducp.fuel.wasmtime.v46.profile0"; + +// ============================ Execution outcome =========================== + +/// Why a task execution did not complete successfully. Deterministic and part of +/// the canonical `result_hash` for failed runs (spec/implementation/02 ยง6). +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub enum FailureKind { + /// Fuel ceiling (from `Limits.max_ucu`) exhausted before completion. + OutOfFuel, + /// Guest called `ducp.fail(code)`. + UserAbort(i32), + /// A wasm trap (illegal instruction, OOB access, unreachable, โ€ฆ). + Trap, + /// The module failed to validate/compile under the Profile 0 feature set. + InvalidModule, + /// The module could not be instantiated (e.g. an unsatisfiable import โ€” no + /// ambient capabilities are provided). + Instantiation, + /// No `run`/`_start` entry point was exported. + NoEntryPoint, +} + +/// Terminal status of an execution. +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub enum ExecStatus { + Ok, + Failure(FailureKind), +} + +/// The deterministic result of running a task in the DVM. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExecOutcome { + /// `hash(output)` on success; `hash(domain โ€– canonical(FailureKind))` on failure. + pub result_hash: Hash, + /// The output payload (empty on failure). Content-addressed by the caller. + pub output: Vec, + /// Metered work in ๐•Œ base units, derived from fuel and the benchmark. + pub ucu_count: Ucu, + pub status: ExecStatus, +} + +/// Domain separator for hashing a failure outcome's `result_hash`. +const FAIL_DOMAIN: &[u8] = b"ducp.fail.v0"; + +fn failure_result_hash(kind: &FailureKind) -> Hash { + let mut buf = FAIL_DOMAIN.to_vec(); + buf.extend_from_slice(&borsh::to_vec(kind).expect("borsh kind")); + hash_bytes(&buf) +} + +// ================================ Dvm trait =============================== + +/// The DUCP Virtual Machine interface (spec/implementation/02 ยง7). `execute` is +/// called once by the Provider; `execute`/`meter` are called by re-executors during +/// sampling/challenge. Because both are deterministic, comparison is exact. +pub trait Dvm { + /// Validate a module against the Profile 0 feature set. A module using a + /// forbidden feature MUST be rejected at submit (`Reject::UnsupportedFeature`) + /// and never reach metering. + fn validate(&self, module: &[u8]) -> Result<(), ducp_types::Reject>; + + /// Execute deterministically: identical `(module, input, benchmark)` โ†’ + /// identical [`ExecOutcome`] on any host. + fn execute( + &self, + module: &[u8], + input: &[u8], + limits: &Limits, + benchmark: &Benchmark, + ) -> ExecOutcome; + + /// Re-derive only the ๐•Œ count (advisory; used at submit and by verifiers). + fn meter(&self, module: &[u8], input: &[u8], benchmark: &Benchmark) -> Ucu; +} + +// ============================== Host state ================================ + +struct HostState { + input: Vec, + output: Vec, + fail_code: Option, + limits: StoreLimits, +} + +impl HostState { + fn new(input: Vec, max_memory_bytes: u64) -> Self { + let limits = StoreLimitsBuilder::new() + .memory_size(max_memory_bytes as usize) + .build(); + HostState { + input, + output: Vec::new(), + fail_code: None, + limits, + } + } +} + +fn caller_memory(caller: &mut Caller<'_, HostState>) -> Option { + match caller.get_export("memory") { + Some(Extern::Memory(m)) => Some(m), + _ => None, + } +} + +// ============================== The runtime =============================== + +/// A deterministic wasmtime-backed DVM. The [`Engine`] (and its `Config`) is built +/// once and reused; it carries the determinism settings. +pub struct WasmtimeDvm { + engine: Engine, +} + +impl Default for WasmtimeDvm { + fn default() -> Self { + Self::new() + } +} + +impl WasmtimeDvm { + /// Build a DVM with the Profile 0 determinism configuration. + pub fn new() -> Self { + let mut config = Config::new(); + config.consume_fuel(true); + config.cranelift_nan_canonicalization(true); + // No nondeterministic / out-of-scope proposals (spec/implementation/02 ยง1). + config.wasm_simd(false); + config.wasm_relaxed_simd(false); + config.wasm_threads(false); + config.wasm_reference_types(false); + config.wasm_function_references(false); + config.wasm_gc(false); + // Allowed Profile 0 features. + config.wasm_bulk_memory(true); + config.wasm_multi_value(true); + let engine = Engine::new(&config).expect("valid deterministic wasmtime config"); + WasmtimeDvm { engine } + } + + fn define_abi(&self, linker: &mut Linker) -> wasmtime::Result<()> { + linker.func_wrap( + "ducp", + "input_len", + |caller: Caller<'_, HostState>| -> i32 { caller.data().input.len() as i32 }, + )?; + linker.func_wrap( + "ducp", + "input_read", + |mut caller: Caller<'_, HostState>, dst: i32, offset: i32, len: i32| -> i32 { + let Some(mem) = caller_memory(&mut caller) else { + return 0; + }; + let (data, state) = mem.data_and_store_mut(&mut caller); + let (dst, offset, len) = (dst as usize, offset as usize, len as usize); + let avail = state.input.len().saturating_sub(offset); + let n = len.min(avail); + match dst.checked_add(n) { + Some(end) if end <= data.len() => { + data[dst..end].copy_from_slice(&state.input[offset..offset + n]); + n as i32 + } + _ => 0, + } + }, + )?; + linker.func_wrap( + "ducp", + "output_write", + |mut caller: Caller<'_, HostState>, src: i32, len: i32| { + let Some(mem) = caller_memory(&mut caller) else { + return; + }; + let (data, state) = mem.data_and_store_mut(&mut caller); + let (src, len) = (src as usize, len as usize); + if let Some(end) = src.checked_add(len) { + if end <= data.len() { + state.output.extend_from_slice(&data[src..end]); + } + } + }, + )?; + linker.func_wrap( + "ducp", + "fail", + |mut caller: Caller<'_, HostState>, code: i32| -> wasmtime::Result<()> { + caller.data_mut().fail_code = Some(code); + Err(wasmtime::Error::msg("ducp.fail")) + }, + )?; + Ok(()) + } + + /// Core deterministic run. Returns `(status, output, fuel_consumed)`. + fn run( + &self, + module_bytes: &[u8], + input: &[u8], + fuel_ceiling: u64, + max_memory_bytes: u64, + ) -> (ExecStatus, Vec, u64) { + let module = match Module::new(&self.engine, module_bytes) { + Ok(m) => m, + Err(_) => { + return ( + ExecStatus::Failure(FailureKind::InvalidModule), + Vec::new(), + 0, + ) + } + }; + + let mut store = Store::new( + &self.engine, + HostState::new(input.to_vec(), max_memory_bytes), + ); + store.limiter(|s| &mut s.limits); + // Fuel is enabled in Config, so set_fuel succeeds. + let _ = store.set_fuel(fuel_ceiling); + + let mut linker = Linker::new(&self.engine); + if self.define_abi(&mut linker).is_err() { + return ( + ExecStatus::Failure(FailureKind::Instantiation), + Vec::new(), + 0, + ); + } + + let instance = match linker.instantiate(&mut store, &module) { + Ok(i) => i, + Err(_) => { + let fuel = consumed(&store, fuel_ceiling); + return ( + ExecStatus::Failure(FailureKind::Instantiation), + Vec::new(), + fuel, + ); + } + }; + + let entry = instance + .get_typed_func::<(), ()>(&mut store, "run") + .or_else(|_| instance.get_typed_func::<(), ()>(&mut store, "_start")); + let entry = match entry { + Ok(f) => f, + Err(_) => { + let fuel = consumed(&store, fuel_ceiling); + return ( + ExecStatus::Failure(FailureKind::NoEntryPoint), + Vec::new(), + fuel, + ); + } + }; + + let call = entry.call(&mut store, ()); + let fuel = consumed(&store, fuel_ceiling); + match call { + Ok(()) => { + let output = std::mem::take(&mut store.data_mut().output); + (ExecStatus::Ok, output, fuel) + } + Err(err) => { + let kind = if let Some(code) = store.data().fail_code { + FailureKind::UserAbort(code) + } else if matches!(err.downcast_ref::(), Some(Trap::OutOfFuel)) { + FailureKind::OutOfFuel + } else { + FailureKind::Trap + }; + (ExecStatus::Failure(kind), Vec::new(), fuel) + } + } + } + + /// Measure the raw fuel a module consumes (benchmark-independent; used to + /// calibrate [`Benchmark::devnet`]). `None` if it does not complete. + pub fn measure_fuel(&self, module_bytes: &[u8], input: &[u8]) -> Option { + let (status, _out, fuel) = + self.run(module_bytes, input, METER_FUEL_CEILING, DEFAULT_MAX_MEM); + match status { + ExecStatus::Ok => Some(fuel), + _ => None, + } + } +} + +/// Registry mapping an IR to its deterministic executor. Profile 0 has exactly one +/// IR (WebAssembly); RISC-V / tensor IRs are added as additional entries here with +/// no change to the task lifecycle (the `Ir` registry seam, spec/implementation +/// README). +pub struct IrRegistry { + wasm: WasmtimeDvm, +} + +impl Default for IrRegistry { + fn default() -> Self { + Self::new() + } +} + +impl IrRegistry { + pub fn new() -> Self { + IrRegistry { + wasm: WasmtimeDvm::new(), + } + } + + /// The executor for an IR, or `None` if the IR is unsupported in this profile. + pub fn executor(&self, ir: IrId) -> Option<&dyn Dvm> { + match ir { + IrId::Wasm => Some(&self.wasm), + } + } +} + +/// Default memory ceiling for advisory metering / calibration (16 MiB). +const DEFAULT_MAX_MEM: u64 = 16 * 1024 * 1024; + +fn consumed(store: &Store, ceiling: u64) -> u64 { + ceiling.saturating_sub(store.get_fuel().unwrap_or(0)) +} + +impl Dvm for WasmtimeDvm { + fn validate(&self, module: &[u8]) -> Result<(), ducp_types::Reject> { + Module::new(&self.engine, module) + .map(|_| ()) + .map_err(|_| ducp_types::Reject::UnsupportedFeature) + } + + fn execute( + &self, + module: &[u8], + input: &[u8], + limits: &Limits, + benchmark: &Benchmark, + ) -> ExecOutcome { + let ceiling = benchmark.ucu_to_fuel_ceiling(limits.max_ucu); + let (status, output, fuel) = self.run(module, input, ceiling, limits.max_memory_bytes); + let (result_hash, ucu_count) = match &status { + ExecStatus::Ok => (hash_bytes(&output), benchmark.fuel_to_ucu(fuel)), + ExecStatus::Failure(FailureKind::OutOfFuel) => { + // Consumed the whole ceiling โ†’ the declared max_ucu (02 ยง6). + (failure_result_hash(&FailureKind::OutOfFuel), limits.max_ucu) + } + ExecStatus::Failure(kind) => (failure_result_hash(kind), benchmark.fuel_to_ucu(fuel)), + }; + ExecOutcome { + result_hash, + output, + ucu_count, + status, + } + } + + fn meter(&self, module: &[u8], input: &[u8], benchmark: &Benchmark) -> Ucu { + let (status, _output, fuel) = self.run(module, input, METER_FUEL_CEILING, DEFAULT_MAX_MEM); + match status { + ExecStatus::Ok => benchmark.fuel_to_ucu(fuel), + _ => benchmark.fuel_to_ucu(fuel), + } + } +} + +// ===================== Canonical reference & sample modules ================ + +/// The canonical reference workload (WAT). A fixed, deterministic compute loop; +/// its fuel cost calibrates `fuel_per_ucu` so it equals exactly one ๐•Œ. +pub const REFERENCE_WAT: &str = r#" +(module + (memory (export "memory") 1) + (func (export "run") + (local $i i32) (local $sum i64) + (local.set $i (i32.const 0)) + (local.set $sum (i64.const 0)) + (block $done + (loop $loop + (br_if $done (i32.ge_u (local.get $i) (i32.const 100000))) + (local.set $sum (i64.add (local.get $sum) (i64.extend_i32_u (local.get $i)))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop))) + (i64.store (i32.const 0) (local.get $sum)) + (call $noop) + ) + (func $noop) +) +"#; + +/// Input for the reference workload (empty). +pub const REFERENCE_INPUT: &[u8] = &[]; + +/// Compile the canonical reference workload to wasm bytes. +pub fn reference_module() -> Vec { + wat::parse_str(REFERENCE_WAT).expect("reference WAT is valid") +} + +/// An echo workload (WAT): copies the input payload to the output verbatim. Used in +/// the metering vectors and ABI tests. +pub const ECHO_WAT: &str = r#" +(module + (import "ducp" "input_len" (func $len (result i32))) + (import "ducp" "input_read" (func $read (param i32 i32 i32) (result i32))) + (import "ducp" "output_write" (func $write (param i32 i32))) + (memory (export "memory") 1) + (func (export "run") + (local $n i32) + (local.set $n (call $len)) + (drop (call $read (i32.const 0) (i32.const 0) (local.get $n))) + (call $write (i32.const 0) (local.get $n)) + ) +) +"#; + +/// Compile the echo workload to wasm bytes. +pub fn echo_module() -> Vec { + wat::parse_str(ECHO_WAT).expect("echo WAT is valid") +} + +// ================================= Tests ================================== + #[cfg(test)] mod tests { + use super::*; + + fn limits(max_ucu: Ucu) -> Limits { + Limits { + max_ucu, + max_memory_bytes: DEFAULT_MAX_MEM, + } + } + #[test] fn version_is_set() { - assert!(!super::version().is_empty()); + assert!(!version().is_empty()); + } + + #[test] + fn reference_meters_to_exactly_one_ucu() { + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let ucu = dvm.meter(&reference_module(), REFERENCE_INPUT, &bench); + assert_eq!(ucu, UCU_SCALE, "reference workload defines 1 ๐•Œ"); + } + + #[test] + fn execution_is_deterministic() { + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let m = echo_module(); + let a = dvm.execute(&m, b"hello world", &limits(UCU_SCALE), &bench); + let b = dvm.execute(&m, b"hello world", &limits(UCU_SCALE), &bench); + assert_eq!(a.result_hash, b.result_hash); + assert_eq!(a.ucu_count, b.ucu_count); + assert_eq!(a.output, b.output); + } + + #[test] + fn echo_output_and_result_hash() { + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let out = dvm.execute(&echo_module(), b"abc", &limits(UCU_SCALE), &bench); + assert_eq!(out.status, ExecStatus::Ok); + assert_eq!(out.output, b"abc"); + assert_eq!(out.result_hash, hash_bytes(b"abc")); + } + + #[test] + fn user_abort_is_deterministic_failure() { + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let m = wat::parse_str( + r#"(module (import "ducp" "fail" (func $f (param i32))) + (func (export "run") (call $f (i32.const 7))))"#, + ) + .unwrap(); + let out = dvm.execute(&m, b"", &limits(UCU_SCALE), &bench); + assert_eq!(out.status, ExecStatus::Failure(FailureKind::UserAbort(7))); + assert_eq!(out.output, Vec::::new()); + assert_eq!( + out.result_hash, + failure_result_hash(&FailureKind::UserAbort(7)) + ); + } + + #[test] + fn infinite_loop_runs_out_of_fuel_at_max_ucu() { + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let m = wat::parse_str(r#"(module (func (export "run") (loop $l (br $l))))"#).unwrap(); + let cap = UCU_SCALE / 1000; // small ceiling + let out = dvm.execute(&m, b"", &limits(cap), &bench); + assert_eq!(out.status, ExecStatus::Failure(FailureKind::OutOfFuel)); + assert_eq!(out.ucu_count, cap, "OOF meters to the declared ceiling"); + } + + #[test] + fn forbidden_simd_feature_is_rejected() { + let dvm = WasmtimeDvm::new(); + let m = wat::parse_str( + r#"(module (memory 1) (func (export "run") (drop (v128.load (i32.const 0)))))"#, + ) + .unwrap(); + assert_eq!( + dvm.validate(&m), + Err(ducp_types::Reject::UnsupportedFeature) + ); + } + + #[test] + fn valid_module_passes_validation() { + let dvm = WasmtimeDvm::new(); + assert!(dvm.validate(&echo_module()).is_ok()); + assert!(dvm.validate(&reference_module()).is_ok()); + } + + #[test] + fn ir_registry_resolves_wasm() { + let reg = IrRegistry::new(); + assert!(reg.executor(IrId::Wasm).is_some()); + } + + #[test] + fn missing_entry_point_is_failure() { + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let m = wat::parse_str(r#"(module (memory (export "memory") 1))"#).unwrap(); + let out = dvm.execute(&m, b"", &limits(UCU_SCALE), &bench); + assert_eq!(out.status, ExecStatus::Failure(FailureKind::NoEntryPoint)); } } diff --git a/crates/governance/Cargo.toml b/crates/governance/Cargo.toml index ac221cd..5271647 100644 --- a/crates/governance/Cargo.toml +++ b/crates/governance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ducp-governance" -description = "Reputation-weighted, role-chamber governance" +description = "Reputation-weighted, role-chamber governance (Profile 0: static parameter set)" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -9,3 +9,5 @@ authors.workspace = true repository.workspace = true [dependencies] +ducp-types.workspace = true +serde.workspace = true diff --git a/crates/governance/src/lib.rs b/crates/governance/src/lib.rs index ec92bcf..d7256d3 100644 --- a/crates/governance/src/lib.rs +++ b/crates/governance/src/lib.rs @@ -1,20 +1,221 @@ //! # ducp-governance //! -//! Reputation-weighted, role-chamber governance. Part of the DUCP reference node. +//! Profile 0 governance is a **static parameter set** ([`Params`]) held as config +//! and set by the maintainer (spec/implementation/05 ยง4). At v1.0 these become +//! on-chain, role-chamber governance parameters; here they fix the economic +//! constants the ledger reads. They are **parameters, not invariants**. //! -//! Specification: +//! All rates are expressed in **parts-per-million** (ppm) so every computation is +//! exact integer arithmetic โ€” no floats anywhere on a consensus path. //! -//! Status: scaffold for spec v0.1.0 โ€” not yet implemented. +//! Specification: +//! Status: Profile 0 implementation for spec v0.2.0. + +use ducp_types::{Sp, Ucu, UCU_SCALE}; +use serde::{Deserialize, Serialize}; /// Returns this crate's version, as declared in `Cargo.toml`. pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") } +/// One million โ€” the ppm denominator. +pub const PPM: u128 = 1_000_000; + +/// The devnet parameter set (spec/implementation/05 ยง4). Values are provisional and +/// tuned on devnet; they are config, not consensus invariants. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Params { + /// Work-issuance rate (ppm of metered ๐•Œ). Default 1% โ€” below real resource cost + /// (`I-SEC-WASHBOUND`). + pub issuance_rate_ppm: u128, + /// Per-task validator fee (ppm of `max_ucu`). Default 0.1%. + pub fee_ppm: u128, + /// Standing accrual rate (ppm of metered ๐•Œ). Default 1 SP per ๐•Œ (1:1 at base scale). + pub sp_rate_ppm: u128, + /// Efficiency multiplier on Standing accrual (ppm). Profile 0 = 1.0 (no energy + /// measured); โ„š is recorded but inert (`I-Q-REWARDNEUTRAL`). + pub efficiency_mult_ppm: u128, + /// Standing decay per epoch (ppm). Default 2%. + pub decay_rate_ppm: u128, + /// Claim-stake base (ppm of `max_ucu`). Default 50%. + pub stake_base_ppm: u128, + /// Maximum stake discount (ppm reduction). Default 80%. + pub discount_max_ppm: u128, + /// Standing at which the discount reaches its cap. + pub sp_ref: Sp, + /// Clawback window length in epochs. Default 32. + pub clawback_epochs: u64, + /// Audit sampling probability (ppm). Default 10%. + pub audit_prob_ppm: u128, + /// Fraud fine (ppm of payment `P`). Default 200% (2ยทP). + pub fine_mult_ppm: u128, + /// Challenger reward (ppm of fine `F`). Default 50% (F/2). + pub challenger_reward_ppm: u128, + /// Minimum challenge bond (ppm of payment `P`). Default 25%. + pub bond_min_ppm: u128, +} + +impl Default for Params { + fn default() -> Self { + Self::devnet() + } +} + +impl Params { + /// The Profile 0 devnet defaults (spec/implementation/05 ยง4). + pub const fn devnet() -> Self { + Params { + issuance_rate_ppm: 10_000, // 1% + fee_ppm: 1_000, // 0.1% of max_ucu + sp_rate_ppm: 1_000_000, // 1 SP per ๐•Œ + efficiency_mult_ppm: 1_000_000, // 1.0 (P0) + decay_rate_ppm: 20_000, // 2% + stake_base_ppm: 500_000, // 0.5 ยท max_ucu + discount_max_ppm: 800_000, // 0.8 cap + sp_ref: 1_000 * UCU_SCALE as Sp, // provisional reference SP + clawback_epochs: 32, + audit_prob_ppm: 100_000, // 0.10 + fine_mult_ppm: 2_000_000, // 2ยทP + challenger_reward_ppm: 500_000, // F/2 + bond_min_ppm: 250_000, // 0.25ยทP + } + } + + /// Validator fee for a task with the given `max_ucu`. + pub fn fee(&self, max_ucu: Ucu) -> Ucu { + max_ucu * self.fee_ppm / PPM + } + + /// Work-issuance minted for metered work `u`: `โŒŠissuance_rate ยท uโŒ‹`. + pub fn issuance(&self, u: Ucu) -> Ucu { + u * self.issuance_rate_ppm / PPM + } + + /// Standing accrual for metered work `u` under an efficiency multiplier (ppm): + /// `โŒŠsp_rate ยท u ยท efficiency_multโŒ‹`. Profile 0 passes `efficiency_mult = PPM`. + pub fn standing_accrual(&self, u: Ucu, efficiency_mult_ppm: u128) -> Sp { + (u * self.sp_rate_ppm / PPM * efficiency_mult_ppm / PPM) as Sp + } + + /// Claim-stake base for a task with the given `max_ucu`. + pub fn stake_base(&self, max_ucu: Ucu) -> Ucu { + max_ucu * self.stake_base_ppm / PPM + } + + /// The Standing-discounted claim stake a Provider must post. + pub fn claim_stake(&self, max_ucu: Ucu, provider_sp: Sp) -> Ucu { + let base = self.stake_base(max_ucu); + let reduction = self.discount_reduction_ppm(provider_sp); + base * (PPM - reduction) / PPM + } + + /// The stake discount, as a ppm reduction, for a Provider's Standing: + /// `min(discount_max, sp / sp_ref)`. + pub fn discount_reduction_ppm(&self, provider_sp: Sp) -> u128 { + if self.sp_ref <= 0 { + return 0; + } + let sp_pos = provider_sp.max(0) as u128; + (sp_pos * PPM / self.sp_ref as u128).min(self.discount_max_ppm) + } + + /// Fraud fine for a payment `p`: `โŒŠfine_mult ยท pโŒ‹` (default 2ยทp). + pub fn fine(&self, p: Ucu) -> Ucu { + p * self.fine_mult_ppm / PPM + } + + /// Challenger reward out of a fine `f`: `โŒŠchallenger_reward ยท fโŒ‹` (default f/2). + pub fn challenger_reward(&self, f: Ucu) -> Ucu { + f * self.challenger_reward_ppm / PPM + } + + /// Minimum acceptable challenge bond for a payment `p`: `โŒŠbond_min ยท pโŒ‹`. + pub fn bond_min(&self, p: Ucu) -> Ucu { + p * self.bond_min_ppm / PPM + } + + /// Decay Standing by one epoch: `โŒŠsp ยท (1 - decay_rate)โŒ‹`. + pub fn decay(&self, sp: Sp) -> Sp { + let ppm_i = PPM as i128; + sp * (ppm_i - self.decay_rate_ppm as i128) / ppm_i + } +} + +/// Source of protocol parameters. Profile 0 uses [`StaticParams`] (a fixed set the +/// maintainer configures); at v1.0 an on-chain, role-chamber governance engine is a +/// later `impl ParamSource` with no change to the ledger (spec 07). This is the +/// governance seam. +pub trait ParamSource { + /// The parameters in force for the current epoch. + fn params(&self) -> Params; +} + +/// The Profile 0 static parameter source. +#[derive(Debug, Clone, Copy, Default)] +pub struct StaticParams(pub Params); + +impl ParamSource for StaticParams { + fn params(&self) -> Params { + self.0 + } +} + #[cfg(test)] mod tests { + use super::*; + #[test] fn version_is_set() { - assert!(!super::version().is_empty()); + assert!(!version().is_empty()); + } + + #[test] + fn static_param_source_returns_devnet() { + let src = StaticParams::default(); + assert_eq!(src.params(), Params::devnet()); + } + + #[test] + fn devnet_defaults_match_spec() { + let p = Params::devnet(); + // 1% issuance on 100 ๐•Œ โ†’ 1 ๐•Œ. + assert_eq!(p.issuance(100 * UCU_SCALE), UCU_SCALE); + // fee 0.1% of 1000 ๐•Œ โ†’ 1 ๐•Œ. + assert_eq!(p.fee(1_000 * UCU_SCALE), UCU_SCALE); + // 1 SP per ๐•Œ at base scale, efficiency 1.0. + assert_eq!(p.standing_accrual(5 * UCU_SCALE, PPM), 5 * UCU_SCALE as Sp); + // stake base 0.5 ยท max_ucu. + assert_eq!(p.stake_base(10 * UCU_SCALE), 5 * UCU_SCALE); + // fine 2ยทP, reward F/2, bond 0.25ยทP. + let f = p.fine(8 * UCU_SCALE); + assert_eq!(f, 16 * UCU_SCALE); + assert_eq!(p.challenger_reward(f), 8 * UCU_SCALE); + assert_eq!(p.bond_min(8 * UCU_SCALE), 2 * UCU_SCALE); + } + + #[test] + fn efficiency_multiplier_is_one_in_p0() { + let p = Params::devnet(); + assert_eq!(p.efficiency_mult_ppm, PPM); + // โ„š-inertness at the accrual level: same ๐•Œ โ†’ same accrual regardless of any + // (hypothetical) efficiency value, because P0 always passes PPM. + let u = 4 * UCU_SCALE; + assert_eq!(p.standing_accrual(u, PPM), p.standing_accrual(u, PPM)); + } + + #[test] + fn discount_is_capped() { + let p = Params::devnet(); + // Very high Standing โ†’ capped at discount_max (0.8) โ†’ stake = 0.2 ยท base. + let stake = p.claim_stake(10 * UCU_SCALE, 1_000_000 * UCU_SCALE as Sp); + let base = p.stake_base(10 * UCU_SCALE); + assert_eq!(stake, base * 200_000 / PPM); + } + + #[test] + fn decay_reduces_standing() { + let p = Params::devnet(); + assert_eq!(p.decay(100 * UCU_SCALE as Sp), 98 * UCU_SCALE as Sp); } } diff --git a/crates/ledger/Cargo.toml b/crates/ledger/Cargo.toml index 4f5af3f..3835abb 100644 --- a/crates/ledger/Cargo.toml +++ b/crates/ledger/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ducp-ledger" -description = "Settlement of UCU and the Standing reputation ledger" +description = "Settlement of UCU, the Standing reputation ledger, and the โ„š-ledger" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -9,4 +9,6 @@ authors.workspace = true repository.workspace = true [dependencies] -ducp-types = { path = "../ducp-types" } +ducp-types.workspace = true +ducp-governance.workspace = true +borsh.workspace = true diff --git a/crates/ledger/src/lib.rs b/crates/ledger/src/lib.rs index cfaea23..3e62d35 100644 --- a/crates/ledger/src/lib.rs +++ b/crates/ledger/src/lib.rs @@ -1,73 +1,956 @@ //! # ducp-ledger //! -//! Settlement of UCU and the Standing reputation ledger. Part of the DUCP reference node. +//! The deterministic ledger state machine: accounts and ๐•Œ, the separate Standing +//! reputation ledger, and the on-chain **โ„š-ledger** (spec/implementation/04, +//! spec/09 ยง7). [`apply`] is a pure transition `State ร— SignedTx โ†’ State`. //! -//! Specification: +//! Base reward is strictly **๐•Œ-proportional**. The efficiency multiplier (DP-0001, +//! spec/09) is the only place โ„š could touch accrual โ€” and in Profile 0 it is fixed +//! at 1.0, so โ„š is recorded but inert (`I-Q-REWARDNEUTRAL`). Every settled task +//! records a `(๐•Œ, โ„š)` entry from genesis, with โ„š null in Profile 0 (`I-Q-NULL`). //! -//! Status: scaffold for spec v0.1.0 โ€” not yet implemented. +//! Conservation (`I-LEDGER-CONSERVE`): after every transition, +//! `ฮฃ(balance + escrowed + bonded) + fee_pool == minted โˆ’ burned`. //! -//! Settlement keeps base reward strictly ๐•Œ-proportional. The efficiency multiplier -//! (DP-0001, spec/09) is the only place โ„š could touch accrual โ€” and in Profile 0 it -//! is fixed at 1.0, so โ„š is recorded but inert (`I-Q-REWARDNEUTRAL`). +//! Specification: +//! Status: Profile 0 implementation for spec v0.2.0. + +use borsh::{BorshDeserialize, BorshSerialize}; +use ducp_governance::Params; +use ducp_types::{ + hash_canonical, Account, BenchmarkVersion, ComputeProof, Epoch, Hash, Identity, IrId, + QLedgerEntry, Receipt, Reject, SignedTx, Sp, StandingRecord, Submission, TaskBody, TaskId, + TaskStatus, Tx, Ucu, VerificationTier, +}; +use std::collections::{BTreeMap, BTreeSet}; /// Returns this crate's version, as declared in `Cargo.toml`. pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") } -/// Profile 0 efficiency multiplier on Standing accrual, as an exact integer ratio -/// `num/den` (no floats in ledger math). P0 measures no energy, so it is -/// **1/1 = 1.0** (spec/implementation/04 ยง3.5). โ„š never scales ๐•Œ minting or -/// settlement โ€” only, once measurement exists, Standing accrual and routing. -pub const EFFICIENCY_MULT_NUM: u64 = 1; -pub const EFFICIENCY_MULT_DEN: u64 = 1; - -/// Standing accrual from `u` ๐•Œ base units: `โŒŠsp_rate ยท u ยท efficiency_multโŒ‹` -/// (spec/implementation/04 ยง3.5). In Profile 0 `efficiency_mult == 1`. -pub fn standing_accrual(sp_rate: u64, u: ducp_types::Ucu) -> ducp_types::Sp { - let scaled = - u * sp_rate as u128 * EFFICIENCY_MULT_NUM as u128 / EFFICIENCY_MULT_DEN as u128; - scaled as ducp_types::Sp +/// Total minted / burned ๐•Œ (audit). Circulating = `minted โˆ’ burned`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, BorshSerialize, BorshDeserialize)] +pub struct Supply { + pub minted: Ucu, + pub burned: Ucu, +} + +/// An open challenge against a settled task: the challenger and their posted bond +/// (spec/implementation/03 ยง3). Resolved by re-execution within the clawback window. +#[derive(Debug, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct ChallengeRecord { + pub challenger: Identity, + pub bond: Ucu, +} + +/// The full ledger state. All maps are sorted (`BTreeMap`) so the canonical +/// encoding โ€” and thus [`State::state_root`] โ€” is deterministic. +#[derive(Debug, Clone, PartialEq, Eq, Default, BorshSerialize, BorshDeserialize)] +pub struct State { + pub accounts: BTreeMap, + pub standing: BTreeMap, + /// Task bodies, kept so claim/proof can read limits, deadline, and benchmark. + pub bodies: BTreeMap, + pub tasks: BTreeMap, + pub proofs: BTreeMap, + pub receipts: BTreeMap, + /// The on-chain โ„š-ledger: `(๐•Œ, โ„š)` per settled task (spec/09 ยง7). + pub q_ledger: BTreeMap, + /// Open challenges awaiting resolution (keyed by task). + pub pending_challenges: BTreeMap, + /// Tasks already slashed for fraud (idempotence / no double-slash). + pub slashed: BTreeSet, + /// Tasks whose claim stake has been released after the clawback window. + pub released: BTreeSet, + /// Per-account transaction nonce (replay protection). + pub nonces: BTreeMap, + pub supply: Supply, + /// Accumulated validator fees (claimable by the sequencer/validators). + pub fee_pool: Ucu, + pub epoch: Epoch, +} + +impl State { + /// Build a genesis state that mints initial balances to the given accounts. + pub fn genesis(allocations: &[(Identity, Ucu)], epoch: Epoch) -> State { + let mut s = State { + epoch, + ..Default::default() + }; + let mut minted: Ucu = 0; + for (id, amount) in allocations { + let a = s.acct_mut(*id); + a.balance += *amount; + minted += *amount; + } + s.supply.minted = minted; + debug_assert!(s.check_conservation()); + s + } + + /// The ๐•Œ commitment to the whole state: `BLAKE3(canonical(State))` (provisional; + /// a Merkle commitment replaces it later โ€” spec/implementation/04 ยง6). + pub fn state_root(&self) -> Hash { + hash_canonical(self) + } + + /// Spendable balance of an account (0 if unknown). + pub fn balance(&self, id: &Identity) -> Ucu { + self.accounts.get(id).map(|a| a.balance).unwrap_or(0) + } + + /// Current Standing of an identity (0 if unknown). + pub fn standing_of(&self, id: &Identity) -> Sp { + self.standing.get(id).map(|s| s.sp).unwrap_or(0) + } + + /// `I-LEDGER-CONSERVE`: total held ๐•Œ equals circulating supply. + pub fn check_conservation(&self) -> bool { + let mut held: u128 = self.fee_pool; + for a in self.accounts.values() { + held += a.balance + a.escrowed + a.bonded; + } + held == self.supply.minted.saturating_sub(self.supply.burned) + } + + // ---- internal mutators (operate on the working copy inside `apply`) ---- + + fn acct_mut(&mut self, id: Identity) -> &mut Account { + self.accounts.entry(id).or_insert_with(|| Account::new(id)) + } + + fn standing_mut(&mut self, id: Identity) -> &mut StandingRecord { + let epoch = self.epoch; + self.standing + .entry(id) + .or_insert_with(|| StandingRecord::new(id, epoch)) + } + + fn check_and_bump_nonce(&mut self, author: Identity, nonce: u64) -> Result<(), Reject> { + let expected = self.nonces.get(&author).copied().unwrap_or(0); + if nonce != expected { + return Err(Reject::BadNonce); + } + self.nonces.insert(author, expected + 1); + Ok(()) + } + + fn submit_task( + &mut self, + requester: Identity, + body: &TaskBody, + params: &Params, + ) -> Result<(), Reject> { + if body.ir != IrId::Wasm || body.tier != VerificationTier::SampledReexec { + return Err(Reject::Invalid); + } + let task = body.task_id(); + if self.tasks.contains_key(&task) { + return Err(Reject::Invalid); // duplicate submission + } + let max_ucu = body.limits.max_ucu; + let fee = params.fee(max_ucu); + let need = max_ucu.checked_add(fee).ok_or(Reject::Invalid)?; + { + let acct = self.acct_mut(requester); + if acct.balance < need { + return Err(Reject::InsufficientBalance); + } + acct.balance -= need; + acct.escrowed += need; + } + self.bodies.insert(task, body.clone()); + self.tasks.insert( + task, + Submission { + task, + requester, + ucu_count: 0, + fee, + status: TaskStatus::Submitted, + provider: None, + claim_stake: 0, + }, + ); + Ok(()) + } + + fn claim_task( + &mut self, + provider: Identity, + task: TaskId, + params: &Params, + ) -> Result<(), Reject> { + let body = self.bodies.get(&task).ok_or(Reject::UnknownTask)?.clone(); + let status = self.tasks.get(&task).ok_or(Reject::UnknownTask)?.status; + if status != TaskStatus::Submitted { + return Err(Reject::BadStatus); + } + if self.epoch > body.deadline { + return Err(Reject::DeadlinePassed); + } + let stake = params.claim_stake(body.limits.max_ucu, self.standing_of(&provider)); + { + let acct = self.acct_mut(provider); + if acct.balance < stake { + return Err(Reject::InsufficientBalance); + } + acct.balance -= stake; + acct.bonded += stake; + } + let sub = self.tasks.get_mut(&task).expect("checked above"); + sub.provider = Some(provider); + sub.status = TaskStatus::Executing; + sub.claim_stake = stake; + Ok(()) + } + + fn submit_proof( + &mut self, + author: Identity, + proof: &ComputeProof, + params: &Params, + ) -> Result<(), Reject> { + let body = self + .bodies + .get(&proof.task) + .ok_or(Reject::UnknownTask)? + .clone(); + let sub = self + .tasks + .get(&proof.task) + .ok_or(Reject::UnknownTask)? + .clone(); + if sub.status != TaskStatus::Executing { + return Err(Reject::BadStatus); + } + if sub.provider != Some(author) || proof.provider != author { + return Err(Reject::WrongProvider); + } + if proof.ucu_count > body.limits.max_ucu { + return Err(Reject::UcuExceedsLimit); + } + if proof.benchmark != body.benchmark { + return Err(Reject::BenchmarkMismatch); + } + + // Record the proof and mark verified (no re-execution here โ€” 03 ยง1). + self.proofs.insert(proof.task, proof.clone()); + { + let s = self.tasks.get_mut(&proof.task).expect("present"); + s.status = TaskStatus::Verified; + s.ucu_count = proof.ucu_count; + } + + // Settle atomically (04 ยง3). + self.settle(&body, &sub, proof, params); + Ok(()) + } + + /// Atomic settlement on `Verified` (04 ยง3). All effects apply together. + fn settle(&mut self, body: &TaskBody, sub: &Submission, proof: &ComputeProof, params: &Params) { + let requester = sub.requester; + let provider = proof.provider; + let fee = sub.fee; + let max_ucu = body.limits.max_ucu; + let u = proof.ucu_count; + + // 1โ€“3: drain the requester's escrow (payment + refund + fee). + { + let r = self.acct_mut(requester); + r.escrowed -= max_ucu + fee; + r.balance += max_ucu - u; // refund the unused ceiling + } + self.fee_pool += fee; // 3: validator fee + + // 1: payment transfer (not burned/reminted โ€” I-ECON-TRANSFER). + { + let p = self.acct_mut(provider); + p.balance += u; + } + + // 4: work-issuance (the only mint โ€” I-ECON-ONEMINT). + let w = params.issuance(u); + { + let p = self.acct_mut(provider); + p.balance += w; + } + self.supply.minted += w; + + // 5: Standing accrual (efficiency_mult = 1.0 in P0). + let delta = params.standing_accrual(u, params.efficiency_mult_ppm); + { + let st = self.standing_mut(provider); + st.sp += delta; + } + + // 6: bond stays locked (already in provider.bonded) until clawback_until. + let clawback_until = self.epoch + params.clawback_epochs; + + // 7: write the immutable Receipt and finalize status. + self.receipts.insert( + proof.task, + Receipt { + task: proof.task, + paid_to_provider: u, + work_issuance: w, + validator_fee: fee, + standing_delta: delta, + settled_epoch: self.epoch, + clawback_until, + }, + ); + { + let s = self.tasks.get_mut(&proof.task).expect("present"); + s.status = TaskStatus::Settled; + } + + // โ„š-ledger genesis MUST (spec/09 ยง7.1): record (๐•Œ, โ„š). In Profile 0 the + // EnergyAttestor is Null, so โ„š is null regardless of any `power_seal` + // (I-Q-NULL, I-Q-REWARDNEUTRAL). + self.q_ledger.insert( + proof.task, + QLedgerEntry::unmeasured(proof.task, u, body.benchmark), + ); + } + + /// Open a challenge against a settled task within its clawback window + /// (spec/implementation/03 ยง3): lock the challenger's bond and record it. + fn open_challenge( + &mut self, + challenger: Identity, + task: TaskId, + bond: Ucu, + params: &Params, + ) -> Result<(), Reject> { + let receipt = self.receipts.get(&task).ok_or(Reject::UnknownTask)?.clone(); + let status = self.tasks.get(&task).ok_or(Reject::UnknownTask)?.status; + if status != TaskStatus::Settled { + return Err(Reject::BadStatus); + } + if self.epoch > receipt.clawback_until { + return Err(Reject::NotInClawbackWindow); + } + if self.slashed.contains(&task) || self.pending_challenges.contains_key(&task) { + return Err(Reject::Invalid); + } + if bond < params.bond_min(receipt.paid_to_provider) { + return Err(Reject::BondTooSmall); + } + { + let ca = self.acct_mut(challenger); + if ca.balance < bond { + return Err(Reject::InsufficientBalance); + } + ca.balance -= bond; + ca.bonded += bond; + } + self.pending_challenges + .insert(task, ChallengeRecord { challenger, bond }); + Ok(()) + } + + fn transfer(&mut self, from: Identity, to: Identity, amount: Ucu) -> Result<(), Reject> { + { + let f = self.acct_mut(from); + if f.balance < amount { + return Err(Reject::InsufficientBalance); + } + f.balance -= amount; + } + self.acct_mut(to).balance += amount; + Ok(()) + } +} + +/// Apply a signed transaction deterministically, returning the new state. Pure: on +/// `Err`, the input state is unchanged (the working copy is discarded). +/// Signature and nonce are checked first (04 ยง2). +pub fn apply(state: &State, stx: &SignedTx, params: &Params) -> Result { + if !stx.verify_sig() { + return Err(Reject::BadSignature); + } + let mut s = state.clone(); + s.check_and_bump_nonce(stx.author, stx.nonce)?; + match &stx.tx { + Tx::SubmitTask(body) => s.submit_task(stx.author, body, params)?, + Tx::ClaimTask { task } => s.claim_task(stx.author, *task, params)?, + Tx::SubmitProof(proof) => s.submit_proof(stx.author, proof, params)?, + Tx::Transfer { to, amount } => s.transfer(stx.author, *to, *amount)?, + Tx::Challenge { task, bond } => s.open_challenge(stx.author, *task, *bond, params)?, + } + debug_assert!( + s.check_conservation(), + "I-LEDGER-CONSERVE violated by transition" + ); + Ok(s) } -/// Build the reward-neutral โ„š-ledger entry for a settled task in Profile 0: -/// ๐•Œ is recorded; โ„š is null (no energy measured) โ€” `I-Q-REWARDNEUTRAL`, `I-Q-NULL`. -pub fn q_ledger_entry_p0( - task: ducp_types::TaskId, - ucu: ducp_types::Ucu, - benchmark: ducp_types::BenchmarkVersion, -) -> ducp_types::QLedgerEntry { - ducp_types::QLedgerEntry::unmeasured(task, ucu, benchmark) +/// Advance the epoch boundary: apply Standing decay deterministically to every +/// identity (`I-STAND-DECAY`), then release any claim stake whose clawback window +/// has closed without a successful challenge (spec/implementation/04 ยง3). The +/// settled Receipt is never rewritten โ€” release is a stake movement, not a reversal +/// (`I-ECON-FINAL`). +pub fn advance_epoch(state: &State, params: &Params) -> State { + let mut s = state.clone(); + s.epoch += 1; + + for st in s.standing.values_mut() { + st.sp = params.decay(st.sp); + st.last_decay_epoch = s.epoch; + } + + // Release matured bonds. Collect first to avoid borrowing `s` while mutating. + let mut to_release: Vec<(TaskId, Identity, Ucu)> = Vec::new(); + for (task, receipt) in &s.receipts { + if receipt.clawback_until <= s.epoch + && !s.released.contains(task) + && !s.slashed.contains(task) + { + if let Some(sub) = s.tasks.get(task) { + if let Some(provider) = sub.provider { + to_release.push((*task, provider, sub.claim_stake)); + } + } + } + } + for (task, provider, stake) in to_release { + let amt = { + let pa = s.acct_mut(provider); + let a = stake.min(pa.bonded); + pa.bonded -= a; + pa.balance += a; + a + }; + let _ = amt; + s.released.insert(task); + } + + debug_assert!(s.check_conservation()); + s +} + +/// Advance the epoch repeatedly until `target` (convenience for tests and the node). +pub fn advance_to_epoch(state: &State, target: Epoch, params: &Params) -> State { + let mut s = state.clone(); + while s.epoch < target { + s = advance_epoch(&s, params); + } + s +} + +/// Convenience: build the Profile 0 reward-neutral โ„š-ledger entry for a settled task +/// (๐•Œ recorded, โ„š null โ€” `I-Q-NULL`). +pub fn q_ledger_entry_p0(task: TaskId, ucu: Ucu, benchmark: BenchmarkVersion) -> QLedgerEntry { + QLedgerEntry::unmeasured(task, ucu, benchmark) +} + +/// Apply the penalties for proven fraud on a settled task (spec/implementation/04 +/// ยง4): clawback the payment from bonded stake, burn the work-issuance, slash a +/// fine (rewarding the auditor), and floor the offender's Standing. Economic +/// reversal is via **stake**, never by rewriting the settled tx (`I-ECON-FINAL`). +/// `reward_to` is the auditor/Challenger; `None` routes the reward to the fee pool +/// (sampling audits). Idempotent: a task is slashed at most once. All amounts are +/// clamped to what is available so conservation holds exactly. +pub fn resolve_fraud( + state: &State, + task: TaskId, + reward_to: Option, + params: &Params, +) -> State { + let mut s = state.clone(); + if s.slashed.contains(&task) { + return s; + } + let receipt = match s.receipts.get(&task) { + Some(r) => r.clone(), + None => return s, + }; + let provider = match s.tasks.get(&task).and_then(|t| t.provider) { + Some(p) => p, + None => return s, + }; + let requester = s.tasks[&task].requester; + let p = receipt.paid_to_provider; + let w = receipt.work_issuance; + + // 1. Clawback P from the Provider's bonded stake โ†’ Requester. + let recovered = { + let pa = s.acct_mut(provider); + let r = p.min(pa.bonded); + pa.bonded -= r; + r + }; + s.acct_mut(requester).balance += recovered; + + // 2. Offsetting burn of the work-issuance W (`I-ECON-BACKED`). + let burn_w = { + let pa = s.acct_mut(provider); + let b = w.min(pa.balance); + pa.balance -= b; + b + }; + s.supply.burned += burn_w; + + // 3. Fine F from bonded; reward R โ‰ค F to the auditor; remainder burned. + let f = params.fine(p); + let fine_avail = { + let pa = s.acct_mut(provider); + let fa = f.min(pa.bonded); + pa.bonded -= fa; + fa + }; + let reward = params.challenger_reward(f).min(fine_avail); + match reward_to { + Some(id) => s.acct_mut(id).balance += reward, + None => s.fee_pool += reward, + } + s.supply.burned += fine_avail - reward; + + // 4. Standing floored + strike (escalating). + { + let st = s.standing_mut(provider); + st.sp = 0; + st.strikes += 1; + } + + s.slashed.insert(task); + if let Some(t) = s.tasks.get_mut(&task) { + t.status = TaskStatus::Failed; + } + debug_assert!(s.check_conservation(), "fraud resolution must conserve ๐•Œ"); + s +} + +/// Resolve an open challenge given the re-execution verdict (`fraud`). On fraud: +/// apply penalties (rewarding the challenger) and return the challenger's bond. On a +/// failed challenge: the challenger forfeits the bond (burned, anti-spam, +/// spec/implementation/03 ยง3). +pub fn resolve_challenge(state: &State, task: TaskId, fraud: bool, params: &Params) -> State { + let mut s = state.clone(); + let pc = match s.pending_challenges.remove(&task) { + Some(p) => p, + None => return s, + }; + if fraud { + s = resolve_fraud(&s, task, Some(pc.challenger), params); + // Return the challenger's bond. + let ca = s.acct_mut(pc.challenger); + let b = pc.bond.min(ca.bonded); + ca.bonded -= b; + ca.balance += b; + } else { + // Forfeit the bond. + let ca = s.acct_mut(pc.challenger); + let b = pc.bond.min(ca.bonded); + ca.bonded -= b; + s.supply.burned += b; + } + debug_assert!(s.check_conservation()); + s } #[cfg(test)] mod tests { - use ducp_types::Quant; + use super::*; + use ducp_types::{ + content_id, hash_bytes, keys, Boundary, Limits, PowerSeal, SealGrade, UCU_SCALE, + }; + + fn seed(n: u8) -> [u8; 32] { + [n; 32] + } + + fn make_task_body(nonce: u64) -> TaskBody { + TaskBody { + ir: IrId::Wasm, + program: content_id(b"program"), + input: content_id(b"input"), + limits: Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 1 << 20, + }, + tier: VerificationTier::SampledReexec, + benchmark: 0, + deadline: 100, + failure_policy: ducp_types::FailurePolicy::ReturnOnFailure, + nonce, + } + } + + /// Drive submit โ†’ claim โ†’ proof and return the settled state. + fn happy_path(power_seal: Option) -> (State, TaskId, Identity, Identity) { + let params = Params::devnet(); + let req = keys::identity(&seed(1)); + let prov = keys::identity(&seed(2)); + let s = State::genesis(&[(req, 100 * UCU_SCALE), (prov, 100 * UCU_SCALE)], 0); + + let body = make_task_body(1); + let task = body.task_id(); + let s = apply( + &s, + &SignedTx::sign(&seed(1), Tx::SubmitTask(body.clone()), 0), + ¶ms, + ) + .unwrap(); + let s = apply( + &s, + &SignedTx::sign(&seed(2), Tx::ClaimTask { task }, 0), + ¶ms, + ) + .unwrap(); + + let result = hash_bytes(b"the-output"); + let proof = ComputeProof { + task, + provider: prov, + output: content_id(b"the-output"), + result_hash: result, + ucu_count: 4 * UCU_SCALE, + benchmark: 0, + tier_data: ducp_types::TierData::SampledReexec, + power_seal, + }; + let s = apply( + &s, + &SignedTx::sign(&seed(2), Tx::SubmitProof(proof), 1), + ¶ms, + ) + .unwrap(); + (s, task, req, prov) + } #[test] fn version_is_set() { - assert!(!super::version().is_empty()); + assert!(!version().is_empty()); } #[test] - fn p0_efficiency_mult_is_one() { - assert_eq!(super::EFFICIENCY_MULT_NUM, super::EFFICIENCY_MULT_DEN); // 1.0 + fn genesis_conserves() { + let s = State::genesis(&[([1; 32], 50), ([2; 32], 70)], 0); + assert!(s.check_conservation()); + assert_eq!(s.supply.minted, 120); } #[test] - fn q_does_not_enter_accrual_in_p0() { - // Same ๐•Œ โ†’ same accrual whether โ„š is null or a value (reward-neutral). - let u: ducp_types::Ucu = 4_000_000_000; // 4 ๐•Œ - let null_entry = super::q_ledger_entry_p0([0u8; 32], u, 0); - let mut valued = super::q_ledger_entry_p0([0u8; 32], u, 0); - valued.q = Some(Quant::ONE); - assert_eq!(null_entry.q, None); - assert_eq!(valued.q, Some(Quant::ONE)); - // Accrual depends on ๐•Œ only: + fn happy_path_settles_with_transfer_mint_and_standing() { + let params = Params::devnet(); + let (s, task, req, prov) = happy_path(None); + + let sub = &s.tasks[&task]; + assert_eq!(sub.status, TaskStatus::Settled); + + let u = 4 * UCU_SCALE; + let max_ucu = 10 * UCU_SCALE; + let fee = params.fee(max_ucu); + let w = params.issuance(u); + let stake = params.claim_stake(max_ucu, 0); + + // Provider: paid u + minted w, minus the still-bonded stake. + let prov_acct = &s.accounts[&prov]; + assert_eq!(prov_acct.balance, 100 * UCU_SCALE - stake + u + w); + assert_eq!(prov_acct.bonded, stake); + + // Requester: escrow fully drained; refunded the unused ceiling. + let req_acct = &s.accounts[&req]; + assert_eq!(req_acct.escrowed, 0); assert_eq!( - super::standing_accrual(1, null_entry.ucu), - super::standing_accrual(1, valued.ucu) + req_acct.balance, + 100 * UCU_SCALE - (max_ucu + fee) + (max_ucu - u) + ); + + // Mint + fee accounted. + assert_eq!(s.supply.minted, 200 * UCU_SCALE + w); + assert_eq!(s.fee_pool, fee); + + // Standing accrued 1:1 with ๐•Œ. + assert_eq!(s.standing_of(&prov), u as Sp); + + // Receipt recorded. + let r = &s.receipts[&task]; + assert_eq!(r.paid_to_provider, u); + assert_eq!(r.work_issuance, w); + assert_eq!(r.clawback_until, params.clawback_epochs); + + assert!(s.check_conservation()); + } + + #[test] + fn q_ledger_records_null_pair_at_genesis() { + let (s, task, _, _) = happy_path(None); + let e = &s.q_ledger[&task]; + assert_eq!(e.ucu, 4 * UCU_SCALE); + assert_eq!(e.q, None); // I-Q-NULL + assert_eq!(e.seal_grade, None); + } + + #[test] + fn reward_neutral_a_power_seal_does_not_change_settlement() { + // I-Q-REWARDNEUTRAL: settlement is identical whether or not a power seal + // rides the proof (the P0 ledger ignores it; โ„š stays null either way). + let (no_seal, task_a, _, _) = happy_path(None); + let seal = PowerSeal { + seal_grade: SealGrade::S2Locked, + boundary: Boundary::Chip, + power_cap_milliwatts: 100_000, + window_millis: 500, + t_max_millikelvin: 300_000, + attestation_evidence: content_id(b"evidence"), + benchmark: 0, + }; + let (with_seal, task_b, _, _) = happy_path(Some(seal)); + + // Same task body โ†’ same task id; the economic effects (balances, supply, + // Standing) and the โ„š entry are all identical. (The stored proof differs by + // design โ€” it carries the seal โ€” so state_root differs; that is not a + // settlement effect.) + assert_eq!(task_a, task_b); + assert_eq!(no_seal.accounts, with_seal.accounts); + assert_eq!(no_seal.supply, with_seal.supply); + assert_eq!(no_seal.fee_pool, with_seal.fee_pool); + assert_eq!(no_seal.standing, with_seal.standing); + assert_eq!(no_seal.q_ledger, with_seal.q_ledger); + } + + #[test] + fn bad_nonce_is_rejected_without_mutation() { + let params = Params::devnet(); + let req = keys::identity(&seed(1)); + let s = State::genesis(&[(req, 100 * UCU_SCALE)], 0); + let body = make_task_body(1); + // Wrong nonce (expected 0). + let bad = SignedTx::sign(&seed(1), Tx::SubmitTask(body), 5); + assert_eq!(apply(&s, &bad, ¶ms), Err(Reject::BadNonce)); + } + + #[test] + fn double_claim_is_rejected() { + let params = Params::devnet(); + let req = keys::identity(&seed(1)); + let prov1 = keys::identity(&seed(2)); + let prov2 = keys::identity(&seed(3)); + let s = State::genesis( + &[ + (req, 100 * UCU_SCALE), + (prov1, 100 * UCU_SCALE), + (prov2, 100 * UCU_SCALE), + ], + 0, + ); + let body = make_task_body(1); + let task = body.task_id(); + let s = apply( + &s, + &SignedTx::sign(&seed(1), Tx::SubmitTask(body), 0), + ¶ms, + ) + .unwrap(); + let s = apply( + &s, + &SignedTx::sign(&seed(2), Tx::ClaimTask { task }, 0), + ¶ms, + ) + .unwrap(); + let second = apply( + &s, + &SignedTx::sign(&seed(3), Tx::ClaimTask { task }, 0), + ¶ms, + ); + assert_eq!(second, Err(Reject::BadStatus)); + } + + #[test] + fn decay_applies_at_epoch_boundary() { + let params = Params::devnet(); + let (s, _, _, prov) = happy_path(None); + let before = s.standing_of(&prov); + let s2 = advance_epoch(&s, ¶ms); + assert_eq!(s2.standing_of(&prov), params.decay(before)); + assert_eq!(s2.epoch, 1); + } + + /// Settle a task and open a challenge against it from a funded challenger. + fn settled_with_open_challenge() -> (State, TaskId, Identity, Identity, Identity) { + let params = Params::devnet(); + let req = keys::identity(&seed(1)); + let prov = keys::identity(&seed(2)); + let chal = keys::identity(&seed(3)); + let s = State::genesis( + &[ + (req, 100 * UCU_SCALE), + (prov, 100 * UCU_SCALE), + (chal, 100 * UCU_SCALE), + ], + 0, + ); + let body = make_task_body(1); + let task = body.task_id(); + let s = apply( + &s, + &SignedTx::sign(&seed(1), Tx::SubmitTask(body), 0), + ¶ms, + ) + .unwrap(); + let s = apply( + &s, + &SignedTx::sign(&seed(2), Tx::ClaimTask { task }, 0), + ¶ms, + ) + .unwrap(); + let proof = ComputeProof { + task, + provider: prov, + output: content_id(b"out"), + result_hash: hash_bytes(b"out"), + ucu_count: 4 * UCU_SCALE, + benchmark: 0, + tier_data: ducp_types::TierData::SampledReexec, + power_seal: None, + }; + let s = apply( + &s, + &SignedTx::sign(&seed(2), Tx::SubmitProof(proof), 1), + ¶ms, + ) + .unwrap(); + let bond = params.bond_min(4 * UCU_SCALE); + let s = apply( + &s, + &SignedTx::sign(&seed(3), Tx::Challenge { task, bond }, 0), + ¶ms, + ) + .unwrap(); + (s, task, req, prov, chal) + } + + #[test] + fn resolve_fraud_claws_back_burns_and_floors_standing() { + let params = Params::devnet(); + let (s, task, req, prov) = happy_path(None); + let req_balance_before = s.accounts[&req].balance; + let burned_before = s.supply.burned; + + let s = resolve_fraud(&s, task, None, ¶ms); + + // Payment clawed back to the requester (from bonded stake). + assert!(s.accounts[&req].balance > req_balance_before); + // Work-issuance burned (supply backed โ€” I-ECON-BACKED). + assert!(s.supply.burned > burned_before); + // Standing floored + strike. + assert_eq!(s.standing_of(&prov), 0); + assert_eq!(s.standing[&prov].strikes, 1); + // Task marked failed; idempotent. + assert_eq!(s.tasks[&task].status, TaskStatus::Failed); + assert!(s.slashed.contains(&task)); + assert!(s.check_conservation()); + + // Re-resolving is a no-op (no double slash). + let again = resolve_fraud(&s, task, None, ¶ms); + assert_eq!(again.supply.burned, s.supply.burned); + } + + #[test] + fn challenge_fraud_rewards_challenger_and_returns_bond() { + let params = Params::devnet(); + let (s, task, _req, prov, chal) = settled_with_open_challenge(); + let bond = params.bond_min(4 * UCU_SCALE); + assert_eq!(s.accounts[&chal].bonded, bond); + + let s = resolve_challenge(&s, task, true, ¶ms); + + // Provider slashed; challenger made whole on bond and net-positive on reward. + assert_eq!(s.standing_of(&prov), 0); + assert_eq!(s.accounts[&chal].bonded, 0); + assert!(s.accounts[&chal].balance > 100 * UCU_SCALE - bond); + assert!(!s.pending_challenges.contains_key(&task)); + assert!(s.check_conservation()); + } + + #[test] + fn failed_challenge_forfeits_bond() { + let params = Params::devnet(); + let (s, task, _req, prov, chal) = settled_with_open_challenge(); + let bond = params.bond_min(4 * UCU_SCALE); + let burned_before = s.supply.burned; + + let s = resolve_challenge(&s, task, false, ¶ms); + + // Bond forfeited (burned); provider untouched. + assert_eq!(s.accounts[&chal].bonded, 0); + assert_eq!(s.accounts[&chal].balance, 100 * UCU_SCALE - bond); + assert_eq!(s.supply.burned, burned_before + bond); + assert_ne!(s.standing_of(&prov), 0); // not slashed + assert!(s.check_conservation()); + } + + #[test] + fn bond_releases_after_clawback_window() { + let params = Params::devnet(); + let (s, task, _req, prov) = happy_path(None); + let stake = s.accounts[&prov].bonded; + assert!(stake > 0); + let balance_before = s.accounts[&prov].balance; + let receipt_before = s.receipts[&task].clone(); + + // Advance to the end of the clawback window. + let s = advance_to_epoch(&s, params.clawback_epochs, ¶ms); + + // Stake returned to spendable balance; recorded as released. + assert_eq!(s.accounts[&prov].bonded, 0); + assert_eq!(s.accounts[&prov].balance, balance_before + stake); + assert!(s.released.contains(&task)); + // Finality: the settled Receipt is never rewritten (I-ECON-FINAL). + assert_eq!(s.receipts[&task], receipt_before); + assert_eq!(s.tasks[&task].status, TaskStatus::Settled); + assert!(s.check_conservation()); + } + + #[test] + fn slashed_task_does_not_release_bond() { + let params = Params::devnet(); + let (s, task, _req, _prov) = happy_path(None); + let s = resolve_fraud(&s, task, None, ¶ms); + // Far past the window; a slashed task's stake is consumed, not released. + let s = advance_to_epoch(&s, params.clawback_epochs + 1, ¶ms); + assert!(!s.released.contains(&task)); + assert!(s.check_conservation()); + } + + #[test] + fn settled_receipt_is_immutable_through_reversal() { + let params = Params::devnet(); + let (s, task, _req, _prov) = happy_path(None); + let receipt_before = s.receipts[&task].clone(); + // Economic reversal via stake marks the task Failed but never edits the Receipt. + let s = resolve_fraud(&s, task, None, ¶ms); + assert_eq!(s.receipts[&task], receipt_before); + assert_eq!(s.tasks[&task].status, TaskStatus::Failed); + } + + #[test] + fn challenge_outside_window_is_rejected() { + let params = Params::devnet(); + let (mut s, task, _, _) = happy_path(None); + // Push the epoch past the clawback window. + s.epoch = params.clawback_epochs + 1; + let chal = keys::identity(&seed(3)); + s.accounts.insert( + chal, + Account { + id: chal, + balance: 100 * UCU_SCALE, + escrowed: 0, + bonded: 0, + }, + ); + s.supply.minted += 100 * UCU_SCALE; + let bond = params.bond_min(4 * UCU_SCALE); + let res = apply( + &s, + &SignedTx::sign(&seed(3), Tx::Challenge { task, bond }, 0), + ¶ms, ); - assert_eq!(super::standing_accrual(1, u), 4_000_000_000); + assert_eq!(res, Err(Reject::NotInClawbackWindow)); } } diff --git a/crates/verification/Cargo.toml b/crates/verification/Cargo.toml index f85c1d9..c06ef7a 100644 --- a/crates/verification/Cargo.toml +++ b/crates/verification/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ducp-verification" -description = "Layered verification: TEE attestation, ZK proofs, and sampled re-execution" +description = "Layered verification: sampled re-execution + challenge (TEE/ZK reserved)" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -9,4 +9,5 @@ authors.workspace = true repository.workspace = true [dependencies] -ducp-types = { path = "../ducp-types" } +ducp-types.workspace = true +ducp-dvm.workspace = true diff --git a/crates/verification/src/lib.rs b/crates/verification/src/lib.rs index d762621..22ae8fb 100644 --- a/crates/verification/src/lib.rs +++ b/crates/verification/src/lib.rs @@ -1,63 +1,445 @@ //! # ducp-verification //! -//! Layered verification: TEE attestation, ZK proofs, and sampled re-execution. Part of the DUCP reference node. +//! Profile 0 verification: the universal **sampled re-execution** floor plus open +//! challenge (spec/implementation/03). Because the DVM is deterministic, a check is +//! exact โ€” re-run on the same `{module, input, benchmark}` and compare the +//! `result_hash` and `ucu_count` byte-for-byte. TEE/ZK are reserved tiers. //! -//! Specification: -//! -//! Status: scaffold for spec v0.1.0 โ€” not yet implemented. +//! The optional [`EnergyAttestor`] seam (DP-0001, spec/09) validates a Power Seal +//! into a recorded โ„š. On the live path Profile 0 wires [`NullAttestor`], so โ„š stays +//! a reward-neutral, unmeasured observable; a real `impl EnergyAttestor` lands later +//! with no change to the proof path. Verifiers validate attestations as evidence โ€” +//! they never re-measure energy (`I-VERIFY-RUNONCE`). //! -//! The [`EnergyAttestor`] seam (DP-0001, spec/09) validates the optional Power Seal -//! into a recorded โ„š. Profile 0 measures no energy, so [`NullAttestor`] always -//! returns `None` โ€” โ„š stays a reward-neutral, unmeasured observable. +//! Specification: +//! Status: Profile 0 implementation for spec v0.2.0. + +use ducp_dvm::{Benchmark, Dvm}; +use ducp_types::{ + hash_bytes, ComputeProof, Hash, Identity, Limits, PowerSeal, Quant, TaskId, Ucu, + VerificationTier, MICRO_Q_SCALE, UCU_SCALE, +}; /// Returns this crate's version, as declared in `Cargo.toml`. pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") } +// ============================ Re-execution ================================= + +/// The verdict of re-executing a Provider's proof. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VerifyOutcome { + /// The re-execution matched the proof. + Accept, + /// Mismatch โ€” fraud. Carries the protocol's re-derived `result_hash` + /// (`expected`) and the Provider's claimed one (`got`). + Fraud { expected: Hash, got: Hash }, +} + +impl VerifyOutcome { + pub fn is_fraud(&self) -> bool { + matches!(self, VerifyOutcome::Fraud { .. }) + } +} + +/// A verification tier. Adding a tier is an additional `impl Verifier` plus a +/// tier-assignment rule, with no change to the lifecycle or ledger +/// (spec/implementation/03 ยง5). +pub trait Verifier { + /// The tier this verifier implements. + fn tier(&self) -> VerificationTier; + + /// Re-derive and compare. For sampled re-execution this re-runs the DVM and + /// compares the `result_hash` **and** `ucu_count` exactly (spec/implementation/03 + /// ยง2.2). + fn check( + &self, + proof: &ComputeProof, + program: &[u8], + input: &[u8], + limits: &Limits, + benchmark: &Benchmark, + dvm: &dyn Dvm, + ) -> VerifyOutcome; +} + +/// The Profile 0 verifier: exact re-execution. +pub struct SampledReexecVerifier; + +impl Verifier for SampledReexecVerifier { + fn tier(&self) -> VerificationTier { + VerificationTier::SampledReexec + } + + fn check( + &self, + proof: &ComputeProof, + program: &[u8], + input: &[u8], + limits: &Limits, + benchmark: &Benchmark, + dvm: &dyn Dvm, + ) -> VerifyOutcome { + let reexec = dvm.execute(program, input, limits, benchmark); + if reexec.result_hash == proof.result_hash && reexec.ucu_count == proof.ucu_count { + VerifyOutcome::Accept + } else { + VerifyOutcome::Fraud { + expected: reexec.result_hash, + got: proof.result_hash, + } + } + } +} + +/// **Reserved** TEE-attestation verifier (spec/implementation/03; out of scope for +/// Profile 0). Present as a seam: a later profile implements `check` to validate a +/// hardware attestation cheaply. Profile 0 never assigns this tier, so `check` is +/// unreachable here. +pub struct TeeVerifier; + +impl Verifier for TeeVerifier { + fn tier(&self) -> VerificationTier { + VerificationTier::Tee + } + fn check( + &self, + _proof: &ComputeProof, + _program: &[u8], + _input: &[u8], + _limits: &Limits, + _benchmark: &Benchmark, + _dvm: &dyn Dvm, + ) -> VerifyOutcome { + unimplemented!( + "TEE tier is reserved; not implemented in Profile 0 (spec/implementation/03)" + ) + } +} + +/// **Reserved** ZK-proof verifier (spec/implementation/03; out of scope for Profile +/// 0). Present as a seam; a later profile implements `check` to verify a succinct +/// proof. Profile 0 never assigns this tier. +pub struct ZkVerifier; + +impl Verifier for ZkVerifier { + fn tier(&self) -> VerificationTier { + VerificationTier::Zk + } + fn check( + &self, + _proof: &ComputeProof, + _program: &[u8], + _input: &[u8], + _limits: &Limits, + _benchmark: &Benchmark, + _dvm: &dyn Dvm, + ) -> VerifyOutcome { + unimplemented!("ZK tier is reserved; not implemented in Profile 0 (spec/implementation/03)") + } +} + +// ============================== Sampling =================================== + +const PPM: u128 = 1_000_000; + +/// Deterministic audit draw (spec/implementation/03 ยง2.1): +/// `selected = blake3(block_hash โ€– task_id)[0..8] < p ยท 2^64`. Reproducible and not +/// gameable by the Provider. +pub fn is_sampled(block_hash: &Hash, task: &TaskId, audit_prob_ppm: u128) -> bool { + let mut buf = Vec::with_capacity(64); + buf.extend_from_slice(block_hash); + buf.extend_from_slice(task); + let h = hash_bytes(&buf); + let draw = u64::from_le_bytes(h[0..8].try_into().expect("8 bytes")); + let threshold = audit_prob_ppm.saturating_mul(1u128 << 64) / PPM; + (draw as u128) < threshold +} + +/// Deterministically choose a re-executor from the eligible set, excluding the +/// original Provider (spec/implementation/03 ยง2.2), by the same seed. `None` if no +/// eligible worker remains. +pub fn select_reexecutor( + block_hash: &Hash, + task: &TaskId, + eligible: &[Identity], + exclude: &Identity, +) -> Option { + let filtered: Vec = eligible.iter().copied().filter(|e| e != exclude).collect(); + if filtered.is_empty() { + return None; + } + let mut buf = Vec::with_capacity(64); + buf.extend_from_slice(block_hash); + buf.extend_from_slice(task); + let h = hash_bytes(&buf); + let draw = u64::from_le_bytes(h[8..16].try_into().expect("8 bytes")); + let idx = (draw as usize) % filtered.len(); + Some(filtered[idx]) +} + +// ===================== The โ„š energy-attestation seam ======================= + /// Validates an optional Power Seal and, on success, returns the recorded โ„š lower /// bound (spec/09 ยง4). โ„š is a **reward-neutral observable**: a failed or absent /// attestation yields `None` and MUST NOT affect ๐•Œ minting, settlement, or proof -/// validity (`I-Q-REWARDNEUTRAL`, `I-Q-NULL`). Verifiers validate the attestation; -/// they never re-measure energy (`I-VERIFY-RUNONCE`). +/// validity (`I-Q-REWARDNEUTRAL`, `I-Q-NULL`). โ„š is protocol-derived from the seal +/// and the benchmark โ€” never self-reported (`I-Q-DERIVED`). pub trait EnergyAttestor { - fn attest(&self, seal: &ducp_types::PowerSeal) -> Option; + fn attest(&self, seal: &PowerSeal, ucu_count: Ucu, benchmark: &Benchmark) -> Option; } -/// Profile 0 attestor: no energy is measured, so โ„š is always `None` -/// (`efficiency_mult = 1.0`; see `ducp-ledger`). Real attestation โ€” a TEE-carried -/// reading, a signed meter, or a locked register โ€” lands later as additional -/// `impl EnergyAttestor`s, with no change to the proof path. +/// Profile 0 **live** attestor: no energy is measured, so โ„š is always `None` +/// (`efficiency_mult = 1.0`). This is what the devnet wires, keeping base settlement +/// strictly ๐•Œ-proportional. pub struct NullAttestor; impl EnergyAttestor for NullAttestor { - fn attest(&self, _seal: &ducp_types::PowerSeal) -> Option { + fn attest(&self, _seal: &PowerSeal, _ucu_count: Ucu, _benchmark: &Benchmark) -> Option { None } } +/// The **Sealed Power Proof** attestor (spec/09 ยง3โ€“6, DP-0001): runs the three gated +/// checks on a present Power Seal and, if all pass, records the provable โ„š **lower +/// bound** (the *Sealed โ„š floor*, ยง4.2). Exists to satisfy the spec/09 ยง10 +/// conformance vector and to prove the seam; it is **not** wired into base +/// settlement (which stays ๐•Œ-proportional โ€” `I-Q-REWARDNEUTRAL`). +pub struct SealedAttestor; + +impl EnergyAttestor for SealedAttestor { + fn attest(&self, seal: &PowerSeal, ucu_count: Ucu, benchmark: &Benchmark) -> Option { + if !evidence_valid(seal) || !plausible(seal) || !well_formed(seal, benchmark) { + return None; + } + Some(sealed_q_floor(seal, ucu_count, benchmark)) + } +} + +/// (a) Evidence validity (spec/09 ยง6.1): the attestation must be present (chains to a +/// root of trust and binds the Task Hash). Profile 0 checks the evidence reference is +/// non-empty; real chain validation lands with the TEE tier. +fn evidence_valid(seal: &PowerSeal) -> bool { + seal.attestation_evidence != [0u8; 32] +} + +/// (b) Plausibility (spec/09 ยง6.1): the implied energy/temperature are physically +/// possible โ€” positive, and not below the Landauer floor for the resolved work. +fn plausible(seal: &PowerSeal) -> bool { + seal.power_cap_milliwatts > 0 && seal.window_millis > 0 && seal.t_max_millikelvin > 0 +} + +/// (c) Well-formedness (spec/09 ยง6.1): the declaration is consistent with the +/// benchmark the seal was produced under. +fn well_formed(seal: &PowerSeal, benchmark: &Benchmark) -> bool { + seal.benchmark == benchmark.version +} + +/// The Sealed โ„š floor (spec/09 ยง4.2), in fixed-point micro-โ„š, by exact integer math: +/// +/// ```text +/// โ„š โ‰ฅ (C ยท E_baseline ยท T_std) / (E_consumed ยท T_max) +/// ``` +/// +/// where `C` is the metered work in whole ๐•Œ, `E_consumed = power_cap ยท window` +/// bounds the energy, and `T_max` bounds temperature. +pub fn sealed_q_floor(seal: &PowerSeal, ucu_count: Ucu, benchmark: &Benchmark) -> Quant { + let c_ucu = (ucu_count / UCU_SCALE).max(1); + let numerator = c_ucu + * (benchmark.e_baseline as u128) + * (benchmark.t_std_millikelvin as u128) + * (MICRO_Q_SCALE as u128); + let e_consumed = (seal.power_cap_milliwatts as u128) * (seal.window_millis as u128); + let denom = e_consumed * (seal.t_max_millikelvin as u128); + let micro = numerator.checked_div(denom).unwrap_or(0) as u64; + Quant { micro_q: micro } +} + #[cfg(test)] mod tests { - use super::{EnergyAttestor, NullAttestor}; - use ducp_types::{Boundary, PowerSeal, SealGrade}; + use super::*; + use ducp_dvm::{echo_module, WasmtimeDvm}; + use ducp_types::{content_id, Boundary, PowerSeal, SealGrade, TierData, UCU_SCALE}; + + fn limits() -> Limits { + Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 16 * 1024 * 1024, + } + } + + fn honest_proof(dvm: &WasmtimeDvm, bench: &Benchmark, input: &[u8]) -> ComputeProof { + let out = dvm.execute(&echo_module(), input, &limits(), bench); + ComputeProof { + task: [1u8; 32], + provider: [2u8; 32], + output: content_id(&out.output), + result_hash: out.result_hash, + ucu_count: out.ucu_count, + benchmark: 0, + tier_data: TierData::SampledReexec, + power_seal: None, + } + } #[test] fn version_is_set() { - assert!(!super::version().is_empty()); + assert!(!version().is_empty()); } #[test] - fn null_attestor_records_no_q() { - let seal = PowerSeal { - seal_grade: SealGrade::S0Identity, - boundary: Boundary::Chip, - power_cap_milliwatts: 300_000, - window_millis: 1_000, - t_max_millikelvin: 350_000, - attestation_evidence: [0u8; 32], + fn verifier_tiers_are_distinct() { + assert_eq!( + SampledReexecVerifier.tier(), + VerificationTier::SampledReexec + ); + assert_eq!(TeeVerifier.tier(), VerificationTier::Tee); + assert_eq!(ZkVerifier.tier(), VerificationTier::Zk); + } + + #[test] + fn honest_proof_is_accepted() { + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let proof = honest_proof(&dvm, &bench, b"data"); + let v = SampledReexecVerifier; + assert_eq!( + v.check(&proof, &echo_module(), b"data", &limits(), &bench, &dvm), + VerifyOutcome::Accept + ); + } + + #[test] + fn forged_result_hash_is_fraud() { + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let mut proof = honest_proof(&dvm, &bench, b"data"); + proof.result_hash = [0xAB; 32]; // forged + let v = SampledReexecVerifier; + let outcome = v.check(&proof, &echo_module(), b"data", &limits(), &bench, &dvm); + assert!(outcome.is_fraud()); + } + + #[test] + fn forged_ucu_count_is_fraud() { + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let mut proof = honest_proof(&dvm, &bench, b"data"); + proof.ucu_count += 1; // forged + let v = SampledReexecVerifier; + assert!(v + .check(&proof, &echo_module(), b"data", &limits(), &bench, &dvm) + .is_fraud()); + } + + #[test] + fn sampling_is_deterministic_and_bounded() { + let block = [7u8; 32]; + let task = [9u8; 32]; + // p = 0 never samples; p = 100% always samples. + assert!(!is_sampled(&block, &task, 0)); + assert!(is_sampled(&block, &task, PPM)); + // Deterministic for a fixed seed. + assert_eq!( + is_sampled(&block, &task, 500_000), + is_sampled(&block, &task, 500_000) + ); + } + + #[test] + fn reexecutor_excludes_provider_and_is_deterministic() { + let block = [1u8; 32]; + let task = [2u8; 32]; + let provider = [10u8; 32]; + let eligible = [[10u8; 32], [11u8; 32], [12u8; 32]]; + let pick = select_reexecutor(&block, &task, &eligible, &provider).unwrap(); + assert_ne!(pick, provider); + assert_eq!( + select_reexecutor(&block, &task, &eligible, &provider), + Some(pick) + ); + // Only the provider is eligible โ†’ nobody to re-execute. + assert_eq!( + select_reexecutor(&block, &task, &[provider], &provider), + None + ); + } + + fn q_benchmark() -> Benchmark { + Benchmark { + version: 0, + fuel_cost_table_hash: [0u8; 32], + fuel_per_ucu: 1, + e_baseline: 137, // 13.7 pJ/๐•Œ (0.1 pJ units) + t_std_millikelvin: 300_000, + } + } + + fn seal(power_cap: u64, t_max_mk: u64, grade: SealGrade, boundary: Boundary) -> PowerSeal { + PowerSeal { + seal_grade: grade, + boundary, + power_cap_milliwatts: power_cap, + window_millis: 50_000, // = C (whole ๐•Œ), so power_cap encodes per-๐•Œ energy + t_max_millikelvin: t_max_mk, + attestation_evidence: content_id(b"root-of-trust-quote"), benchmark: 0, - }; - // Even a well-formed seal yields no โ„š in Profile 0 โ€” reward-neutral and inert. - assert_eq!(NullAttestor.attest(&seal), None); + } + } + + #[test] + fn null_attestor_records_no_q() { + let bench = q_benchmark(); + let s = seal(137, 300_000, SealGrade::S0Identity, Boundary::Chip); + assert_eq!(NullAttestor.attest(&s, 50_000 * UCU_SCALE, &bench), None); + } + + #[test] + fn sealed_attestor_reproduces_dp0001_vector() { + // spec/09 ยง10 / DP-0001 ยง9: ๐•Œ = 50,000; baseline 13.7 pJ at 300 K. + let bench = q_benchmark(); + let ucu = 50_000 * UCU_SCALE; + let a = SealedAttestor + .attest( + &seal(274, 350_000, SealGrade::S1Witnessed, Boundary::Node), + ucu, + &bench, + ) + .unwrap(); + let b = SealedAttestor + .attest( + &seal(137, 300_000, SealGrade::S2Locked, Boundary::Chip), + ucu, + &bench, + ) + .unwrap(); + let c = SealedAttestor + .attest( + &seal(100, 250_000, SealGrade::S2Locked, Boundary::Chip), + ucu, + &bench, + ) + .unwrap(); + // โ„š โ‰ˆ {0.43, 1.00, 1.64} in micro-โ„š. + assert_eq!(a.micro_q, 428_571); // 0.428571 โ‰ˆ 0.43 + assert_eq!(b.micro_q, 1_000_000); // 1.00 + assert_eq!(c.micro_q, 1_644_000); // 1.644 โ‰ˆ 1.64 + } + + #[test] + fn sealed_attestor_rejects_missing_evidence() { + let bench = q_benchmark(); + let mut s = seal(137, 300_000, SealGrade::S0Identity, Boundary::Chip); + s.attestation_evidence = [0u8; 32]; // no evidence + assert_eq!(SealedAttestor.attest(&s, 50_000 * UCU_SCALE, &bench), None); + } + + #[test] + fn sealed_attestor_rejects_benchmark_mismatch() { + let bench = q_benchmark(); + let mut s = seal(137, 300_000, SealGrade::S0Identity, Boundary::Chip); + s.benchmark = 99; // not the benchmark in force + assert_eq!(SealedAttestor.attest(&s, 50_000 * UCU_SCALE, &bench), None); } } diff --git a/node/Cargo.toml b/node/Cargo.toml index d00274c..ce956f4 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -8,13 +8,30 @@ license.workspace = true authors.workspace = true repository.workspace = true +[lib] +name = "ducp_node" +path = "src/lib.rs" + [[bin]] name = "ducp-node" path = "src/main.rs" [dependencies] -ducp-dvm = { path = "../crates/dvm" } -ducp-verification = { path = "../crates/verification" } -ducp-ledger = { path = "../crates/ledger" } -ducp-consensus = { path = "../crates/consensus" } -ducp-governance = { path = "../crates/governance" } +ducp-types.workspace = true +ducp-dvm.workspace = true +ducp-verification.workspace = true +ducp-ledger.workspace = true +ducp-consensus.workspace = true +ducp-governance.workspace = true +jsonrpsee.workspace = true +tokio.workspace = true +serde.workspace = true +serde_json.workspace = true +hex.workspace = true +clap.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +anyhow.workspace = true + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/node/src/bin/ducp-worker.rs b/node/src/bin/ducp-worker.rs new file mode 100644 index 0000000..6232029 --- /dev/null +++ b/node/src/bin/ducp-worker.rs @@ -0,0 +1,151 @@ +//! DUCP devnet worker โ€” a beachhead load driver. +//! +//! Connects to a running sequencer over JSON-RPC and repeatedly runs the full task +//! lifecycle (submit โ†’ claim โ†’ execute โ†’ proof โ†’ settle) against a deterministic +//! WebAssembly workload, printing the settled ๐•Œ and the (๐•Œ, โ„š) record for each. +//! +//! Acts as both Requester and Provider for the demo, using two dev keys that the +//! default `ducp-node` genesis funds (seeds `01..` and `02..`). + +use clap::Parser; +use ducp_dvm::{echo_module, Benchmark, Dvm, WasmtimeDvm}; +use ducp_node::DucpApiClient; +use ducp_types::{ + content_id, keys, ComputeProof, FailurePolicy, IrId, Limits, SignedTx, TaskBody, TierData, Tx, + VerificationTier, UCU_SCALE, +}; +use jsonrpsee::http_client::HttpClientBuilder; + +#[derive(Parser, Debug)] +#[command(name = "ducp-worker", version, about)] +struct Cli { + /// Sequencer JSON-RPC URL. + #[arg(long, default_value = "http://127.0.0.1:8645")] + sequencer: String, + + /// Number of beachhead tasks to run. + #[arg(long, default_value_t = 5)] + tasks: u64, + + /// Hex seed for the Requester key (funded at genesis). + #[arg( + long, + default_value = "0101010101010101010101010101010101010101010101010101010101010101" + )] + requester_seed: String, + + /// Hex seed for the Provider key (funded at genesis). + #[arg( + long, + default_value = "0202020202020202020202020202020202020202020202020202020202020202" + )] + provider_seed: String, +} + +fn seed(hex_str: &str) -> [u8; 32] { + hex::decode(hex_str).unwrap().try_into().unwrap() +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + let req_seed = seed(&cli.requester_seed); + let prov_seed = seed(&cli.provider_seed); + let prov = keys::identity(&prov_seed); + + let client = HttpClientBuilder::default().build(&cli.sequencer)?; + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + + // Upload the program once. + let program = echo_module(); + let prog_id = client.put_blob(hex::encode(&program)).await?.content_id; + + let head = client.get_head().await?; + println!( + "connected to {} โ€” head height {} epoch {}", + cli.sequencer, head.height, head.epoch + ); + + // Local nonces (fresh devnet starts at 0). The requester submits once per task, + // so its nonce is the task index `i`; the provider sends two txs per task. + let mut prov_nonce = 0u64; + + let mut total_ucu: u128 = 0; + for i in 0..cli.tasks { + let input = format!("beachhead-task-{i}").into_bytes(); + let in_id = client.put_blob(hex::encode(&input)).await?.content_id; + + let body = TaskBody { + ir: IrId::Wasm, + program: hex::decode(&prog_id)?.try_into().unwrap(), + input: hex::decode(&in_id)?.try_into().unwrap(), + limits: Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 16 * 1024 * 1024, + }, + tier: VerificationTier::SampledReexec, + benchmark: 0, + deadline: u64::MAX, + failure_policy: FailurePolicy::ReturnOnFailure, + nonce: i, + }; + let task = body.task_id(); + + client + .submit_task(SignedTx::sign(&req_seed, Tx::SubmitTask(body.clone()), i)) + .await?; + + client + .claim_task(SignedTx::sign( + &prov_seed, + Tx::ClaimTask { task }, + prov_nonce, + )) + .await?; + prov_nonce += 1; + + let outcome = dvm.execute(&program, &input, &body.limits, &bench); + let proof = ComputeProof { + task, + provider: prov, + output: content_id(&outcome.output), + result_hash: outcome.result_hash, + ucu_count: outcome.ucu_count, + benchmark: 0, + tier_data: TierData::SampledReexec, + power_seal: None, + }; + client + .submit_proof(SignedTx::sign( + &prov_seed, + Tx::SubmitProof(proof), + prov_nonce, + )) + .await?; + prov_nonce += 1; + + let q = client.get_q_entry(hex::encode(task)).await?; + let q_str = match q.q { + Some(v) => format!("{}.{:06}", v.micro_q / 1_000_000, v.micro_q % 1_000_000), + None => "null".to_string(), + }; + total_ucu += outcome.ucu_count; + println!( + "task {i}: settled ๐•Œ={} โ„š={} (task {})", + outcome.ucu_count, + q_str, + &hex::encode(task)[..16] + ); + } + + let head = client.get_head().await?; + println!( + "done โ€” {} tasks, total ๐•Œ={}, head height {} state_root {}", + cli.tasks, + total_ucu, + head.height, + &head.state_root[..16] + ); + Ok(()) +} diff --git a/node/src/lib.rs b/node/src/lib.rs new file mode 100644 index 0000000..34448f1 --- /dev/null +++ b/node/src/lib.rs @@ -0,0 +1,680 @@ +//! # ducp_node +//! +//! Profile 0 reference node: wires the DVM, ledger, single-sequencer consensus, and +//! governance parameters behind a JSON-RPC server (spec/implementation/05). +//! +//! The node accepts signed transactions, orders them into single-sequencer blocks, +//! applies the deterministic ledger transition, and exposes read queries โ€” including +//! the **(๐•Œ, โ„š)** pair recorded for every settled task (spec/09 ยง8). +//! +//! "node" means a *network participant*; it is unrelated to Node.js. +//! +//! Specification: +//! Status: Profile 0 implementation for spec v0.2.0. + +use std::collections::BTreeMap; +use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; + +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; +use jsonrpsee::server::{Server, ServerHandle}; +use jsonrpsee::types::ErrorObjectOwned; +use serde::{Deserialize, Serialize}; + +use ducp_consensus::{ConsensusEngine, SingleSequencer}; +use ducp_dvm::{Benchmark, Dvm, WasmtimeDvm}; +use ducp_governance::Params; +use ducp_ledger::{resolve_challenge, State}; +use ducp_types::{ + content_id, Account, Block, ContentId, Hash, Identity, QLedgerEntry, Receipt, Reject, SignedTx, + Submission, TaskId, Tx, TxId, Ucu, +}; +use ducp_verification::{SampledReexecVerifier, Verifier}; + +/// Returns this crate's version, as declared in `Cargo.toml`. +pub fn version() -> &'static str { + env!("CARGO_PKG_VERSION") +} + +// ============================ Node state =================================== + +/// Persistence seam for ledger state (spec/implementation/04: the state commitment +/// scheme is provisional). Profile 0 ships [`InMemoryStorage`]; a disk/Merkle-backed +/// store is a later `impl Storage` with no change to the node. +pub trait Storage: Send + Sync { + /// Persist the latest committed state snapshot. + fn save(&self, state: &State); + /// Load the most recent snapshot, if any. + fn load(&self) -> Option; +} + +/// The Profile 0 in-memory state store (no durability). +#[derive(Default)] +pub struct InMemoryStorage { + last: Mutex>, +} + +impl Storage for InMemoryStorage { + fn save(&self, state: &State) { + *self.last.lock().expect("storage mutex") = Some(state.clone()); + } + fn load(&self) -> Option { + self.last.lock().expect("storage mutex").clone() + } +} + +/// Mutable node state guarded by a single mutex. +struct NodeInner { + state: State, + sequencer: SingleSequencer, + mempool: Vec, + blobs: BTreeMap>, + blocks: Vec, + block_txs: Vec>, +} + +/// A running node: immutable config + engines, plus the mutex-guarded ledger state. +pub struct NodeHandle { + pub params: Params, + pub benchmark: Benchmark, + pub dvm: WasmtimeDvm, + storage: Box, + inner: Mutex, +} + +impl NodeHandle { + /// Build a node with the given sequencer identity and genesis ๐•Œ allocations, + /// backed by in-memory storage. + pub fn new(proposer: Identity, allocations: &[(Identity, Ucu)]) -> Arc { + Self::with_storage(proposer, allocations, Box::::default()) + } + + /// Build a node with a custom [`Storage`] backend. + pub fn with_storage( + proposer: Identity, + allocations: &[(Identity, Ucu)], + storage: Box, + ) -> Arc { + let dvm = WasmtimeDvm::new(); + let benchmark = Benchmark::devnet(&dvm); + let state = State::genesis(allocations, 0); + storage.save(&state); + let sequencer = SingleSequencer::new(proposer); + Arc::new(NodeHandle { + params: Params::devnet(), + benchmark, + dvm, + storage, + inner: Mutex::new(NodeInner { + state, + sequencer, + mempool: Vec::new(), + blobs: BTreeMap::new(), + blocks: Vec::new(), + block_txs: Vec::new(), + }), + }) + } + + /// Admit a signed transaction and produce a single-sequencer block immediately + /// (Profile 0 devnet finality). Returns the `TxId` on acceptance, or the ledger + /// `Reject` reason. The block carries any settlement, mint, Standing update, and + /// the (๐•Œ, โ„š) entry that the transition produced. + pub fn submit(&self, stx: SignedTx) -> Result { + let txid = stx.tx_id(); + let mut inner = self.inner.lock().expect("node mutex"); + inner.mempool.push(stx); + let mempool = std::mem::take(&mut inner.mempool); + let proposal = inner + .sequencer + .produce(&mempool, &inner.state, &self.params); + + let outcome = proposal.results.iter().find(|r| r.tx_id == txid).cloned(); + + if !proposal.block.txs.is_empty() { + inner.sequencer.adopt(&proposal.block); + inner.state = proposal.state; + inner.blocks.push(proposal.block); + inner.block_txs.push(proposal.txs); + self.storage.save(&inner.state); + } + + match outcome { + Some(r) if r.accepted => Ok(txid), + Some(r) => Err(r.reject.unwrap_or(Reject::Invalid)), + None => Err(Reject::Invalid), + } + } + + /// Resolve an open challenge against `task` by re-executing the proof and + /// applying the verdict on-chain (spec/implementation/03 ยง3). Returns whether + /// fraud was found. The bond must already be locked (via a Challenge tx). The + /// resolution is committed as a system block so the head and `state_root` + /// advance. + pub fn resolve_pending_challenge(&self, task: TaskId) -> Result { + // Gather re-execution inputs (brief lock). + let (body, proof, program, input) = { + let inner = self.inner.lock().expect("node mutex"); + let body = inner + .state + .bodies + .get(&task) + .ok_or(Reject::UnknownTask)? + .clone(); + let proof = inner + .state + .proofs + .get(&task) + .ok_or(Reject::UnknownTask)? + .clone(); + let program = inner.blobs.get(&body.program).cloned(); + let input = inner.blobs.get(&body.input).cloned(); + (body, proof, program, input) + }; + let program = program.ok_or(Reject::UnknownTask)?; + let input = input.ok_or(Reject::UnknownTask)?; + + // Re-execute outside the lock (CPU-bound, deterministic). + let outcome = SampledReexecVerifier.check( + &proof, + &program, + &input, + &body.limits, + &self.benchmark, + &self.dvm, + ); + let fraud = outcome.is_fraud(); + + // Apply the verdict and seal it as a system block. + let mut guard = self.inner.lock().expect("node mutex"); + let inner = &mut *guard; + let resolved = resolve_challenge(&inner.state, task, fraud, &self.params); + let block = inner.sequencer.seal_block(&resolved); + inner.sequencer.adopt(&block); + inner.state = resolved; + inner.blocks.push(block); + inner.block_txs.push(Vec::new()); + self.storage.save(&inner.state); + Ok(fraud) + } + + /// Advance the epoch boundary (Standing decay + clawback bond release), sealing + /// the result as a system block. + pub fn advance_epoch(&self) { + let mut guard = self.inner.lock().expect("node mutex"); + let inner = &mut *guard; // split disjoint fields past the MutexGuard Deref + let new_state = inner.sequencer.advance_epoch(&inner.state, &self.params); + let block = inner.sequencer.seal_block(&new_state); + inner.sequencer.adopt(&block); + inner.state = new_state; + inner.blocks.push(block); + inner.block_txs.push(Vec::new()); + self.storage.save(&inner.state); + } + + fn account_view(&self, id: &Identity) -> AccountView { + let inner = self.inner.lock().expect("node mutex"); + let a = inner + .state + .accounts + .get(id) + .copied() + .unwrap_or_else(|| Account::new(*id)); + AccountView { + balance: a.balance.to_string(), + escrowed: a.escrowed.to_string(), + bonded: a.bonded.to_string(), + } + } + + fn standing_view(&self, id: &Identity) -> StandingView { + let inner = self.inner.lock().expect("node mutex"); + match inner.state.standing.get(id) { + Some(s) => StandingView { + sp: s.sp.to_string(), + strikes: s.strikes, + }, + None => StandingView { + sp: "0".to_string(), + strikes: 0, + }, + } + } + + fn head(&self) -> HeadView { + let inner = self.inner.lock().expect("node mutex"); + HeadView { + height: inner.sequencer.height(), + state_root: hex::encode(inner.state.state_root()), + epoch: inner.sequencer.epoch(), + } + } + + fn task_view(&self, task: &TaskId) -> Option { + let inner = self.inner.lock().expect("node mutex"); + let submission = inner.state.tasks.get(task).cloned()?; + let status = format!("{:?}", submission.status); + Some(TaskView { + submission, + status, + receipt: inner.state.receipts.get(task).cloned(), + q_entry: inner.state.q_ledger.get(task).cloned(), + }) + } + + fn q_entry(&self, task: &TaskId) -> Option { + let inner = self.inner.lock().expect("node mutex"); + inner.state.q_ledger.get(task).cloned() + } + + fn task_claim_stake(&self, task: &TaskId) -> Option { + let inner = self.inner.lock().expect("node mutex"); + inner.state.tasks.get(task).map(|s| s.claim_stake) + } + + fn block_at(&self, height: u64) -> Option { + if height == 0 { + return None; + } + let inner = self.inner.lock().expect("node mutex"); + inner.blocks.get((height - 1) as usize).cloned() + } + + fn block_txs_at(&self, height: u64) -> Option> { + if height == 0 { + return None; + } + let inner = self.inner.lock().expect("node mutex"); + inner.block_txs.get((height - 1) as usize).cloned() + } + + fn put_blob(&self, bytes: Vec) -> ContentId { + let id = content_id(&bytes); + let mut inner = self.inner.lock().expect("node mutex"); + inner.blobs.insert(id, bytes); + id + } + + fn get_blob(&self, id: &ContentId) -> Option> { + let inner = self.inner.lock().expect("node mutex"); + inner.blobs.get(id).cloned() + } + + /// Advisory local metering for a content-addressed `(program, input)` pair. + fn estimate(&self, program: &ContentId, input: &ContentId) -> Result { + let (prog, inp) = { + let inner = self.inner.lock().expect("node mutex"); + ( + inner.blobs.get(program).cloned(), + inner.blobs.get(input).cloned(), + ) + }; + let prog = prog.ok_or(EstimateError::MissingProgram)?; + let inp = inp.ok_or(EstimateError::MissingInput)?; + Ok(self.dvm.meter(&prog, &inp, &self.benchmark)) + } +} + +enum EstimateError { + MissingProgram, + MissingInput, +} + +// ============================ RPC views ==================================== + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SubmitTaskResp { + pub task_id: String, + pub escrowed: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClaimResp { + pub ok: bool, + pub claim_stake: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OkResp { + pub ok: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AccountView { + pub balance: String, + pub escrowed: String, + pub bonded: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StandingView { + pub sp: String, + pub strikes: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HeadView { + pub height: u64, + pub state_root: String, + pub epoch: u64, +} + +/// `getTask` response: the Submission, its current status, the Receipt (if settled), +/// and the (๐•Œ, โ„š) ledger entry (spec/09 ยง8). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskView { + pub submission: Submission, + pub status: String, + pub receipt: Option, + pub q_entry: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EstimateResp { + pub ucu: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PutBlobResp { + pub content_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetBlobResp { + pub data: String, +} + +// ============================ RPC API ====================================== + +/// The Profile 0 JSON-RPC API (spec/implementation/05 ยง3). State-changing methods +/// take a single positional `SignedTx`; read methods take positional scalar args. +#[rpc(server, client, namespace_separator = "_")] +pub trait DucpApi { + #[method(name = "ducp_submitTask")] + fn submit_task(&self, tx: SignedTx) -> RpcResult; + + #[method(name = "ducp_claimTask")] + fn claim_task(&self, tx: SignedTx) -> RpcResult; + + #[method(name = "ducp_submitProof")] + fn submit_proof(&self, tx: SignedTx) -> RpcResult; + + #[method(name = "ducp_transfer")] + fn transfer(&self, tx: SignedTx) -> RpcResult; + + #[method(name = "ducp_challenge")] + fn challenge(&self, tx: SignedTx) -> RpcResult; + + #[method(name = "ducp_getTask")] + fn get_task(&self, task_id: String) -> RpcResult; + + #[method(name = "ducp_getAccount")] + fn get_account(&self, id: String) -> RpcResult; + + #[method(name = "ducp_getStanding")] + fn get_standing(&self, id: String) -> RpcResult; + + #[method(name = "ducp_getHead")] + fn get_head(&self) -> RpcResult; + + #[method(name = "ducp_getBlock")] + fn get_block(&self, height: u64) -> RpcResult; + + #[method(name = "ducp_getBlockTxs")] + fn get_block_txs(&self, height: u64) -> RpcResult>; + + #[method(name = "ducp_getQEntry")] + fn get_q_entry(&self, task_id: String) -> RpcResult; + + #[method(name = "ducp_estimateUcu")] + fn estimate_ucu( + &self, + program: String, + input: String, + benchmark: u32, + ) -> RpcResult; + + #[method(name = "ducp_putBlob")] + fn put_blob(&self, data: String) -> RpcResult; + + #[method(name = "ducp_getBlob")] + fn get_blob(&self, content_id: String) -> RpcResult; +} + +/// The server implementation backed by a [`NodeHandle`]. +pub struct RpcServerImpl { + handle: Arc, +} + +impl RpcServerImpl { + pub fn new(handle: Arc) -> Self { + RpcServerImpl { handle } + } +} + +impl DucpApiServer for RpcServerImpl { + fn submit_task(&self, tx: SignedTx) -> RpcResult { + let body = match &tx.tx { + Tx::SubmitTask(b) => b.clone(), + _ => return Err(invalid("expected a SubmitTask transaction")), + }; + let task_id = body.task_id(); + let escrowed = body.limits.max_ucu + self.handle.params.fee(body.limits.max_ucu); + self.handle.submit(tx).map_err(reject_err)?; + Ok(SubmitTaskResp { + task_id: hex::encode(task_id), + escrowed: escrowed.to_string(), + }) + } + + fn claim_task(&self, tx: SignedTx) -> RpcResult { + let task = match &tx.tx { + Tx::ClaimTask { task } => *task, + _ => return Err(invalid("expected a ClaimTask transaction")), + }; + self.handle.submit(tx).map_err(reject_err)?; + let claim_stake = self.handle.task_claim_stake(&task).unwrap_or(0); + Ok(ClaimResp { + ok: true, + claim_stake: claim_stake.to_string(), + }) + } + + fn submit_proof(&self, tx: SignedTx) -> RpcResult { + if !matches!(tx.tx, Tx::SubmitProof(_)) { + return Err(invalid("expected a SubmitProof transaction")); + } + self.handle.submit(tx).map_err(reject_err)?; + Ok(OkResp { ok: true }) + } + + fn transfer(&self, tx: SignedTx) -> RpcResult { + if !matches!(tx.tx, Tx::Transfer { .. }) { + return Err(invalid("expected a Transfer transaction")); + } + self.handle.submit(tx).map_err(reject_err)?; + Ok(OkResp { ok: true }) + } + + fn challenge(&self, tx: SignedTx) -> RpcResult { + let task = match &tx.tx { + Tx::Challenge { task, .. } => *task, + _ => return Err(invalid("expected a Challenge transaction")), + }; + // Lock the bond + record the challenge, then re-execute and resolve. + self.handle.submit(tx).map_err(reject_err)?; + self.handle + .resolve_pending_challenge(task) + .map_err(reject_err)?; + Ok(OkResp { ok: true }) + } + + fn get_task(&self, task_id: String) -> RpcResult { + let task = parse_hash(&task_id)?; + self.handle + .task_view(&task) + .ok_or_else(|| not_found("task")) + } + + fn get_account(&self, id: String) -> RpcResult { + let id = parse_hash(&id)?; + Ok(self.handle.account_view(&id)) + } + + fn get_standing(&self, id: String) -> RpcResult { + let id = parse_hash(&id)?; + Ok(self.handle.standing_view(&id)) + } + + fn get_head(&self) -> RpcResult { + Ok(self.handle.head()) + } + + fn get_block(&self, height: u64) -> RpcResult { + self.handle + .block_at(height) + .ok_or_else(|| not_found("block")) + } + + fn get_block_txs(&self, height: u64) -> RpcResult> { + self.handle + .block_txs_at(height) + .ok_or_else(|| not_found("block")) + } + + fn get_q_entry(&self, task_id: String) -> RpcResult { + let task = parse_hash(&task_id)?; + self.handle + .q_entry(&task) + .ok_or_else(|| not_found("q-entry")) + } + + fn estimate_ucu( + &self, + program: String, + input: String, + _benchmark: u32, + ) -> RpcResult { + let program = parse_hash(&program)?; + let input = parse_hash(&input)?; + match self.handle.estimate(&program, &input) { + Ok(ucu) => Ok(EstimateResp { + ucu: ucu.to_string(), + }), + Err(EstimateError::MissingProgram) => Err(not_found("program blob")), + Err(EstimateError::MissingInput) => Err(not_found("input blob")), + } + } + + fn put_blob(&self, data: String) -> RpcResult { + let bytes = parse_hex(&data)?; + let id = self.handle.put_blob(bytes); + Ok(PutBlobResp { + content_id: hex::encode(id), + }) + } + + fn get_blob(&self, content_id: String) -> RpcResult { + let id = parse_hash(&content_id)?; + let bytes = self.handle.get_blob(&id).ok_or_else(|| not_found("blob"))?; + Ok(GetBlobResp { + data: hex::encode(bytes), + }) + } +} + +/// Start the JSON-RPC server, returning the bound address and a handle that keeps +/// the server alive while held. +pub async fn start_server( + handle: Arc, + addr: SocketAddr, +) -> anyhow::Result<(SocketAddr, ServerHandle)> { + let server = Server::builder().build(addr).await?; + let bound = server.local_addr()?; + let module = RpcServerImpl::new(handle).into_rpc(); + let server_handle = server.start(module); + Ok((bound, server_handle)) +} + +// ============================ Error mapping ================================ + +const ERR_BASE: i32 = -32000; + +fn reject_code(r: &Reject) -> i32 { + // Stable, distinct codes per Reject variant (05 ยง3). + let offset = match r { + Reject::BadSignature => 1, + Reject::BadNonce => 2, + Reject::UnknownAccount => 3, + Reject::UnknownTask => 4, + Reject::InsufficientBalance => 5, + Reject::BadStatus => 6, + Reject::DeadlinePassed => 7, + Reject::AlreadyClaimed => 8, + Reject::WrongProvider => 9, + Reject::UcuExceedsLimit => 10, + Reject::BenchmarkMismatch => 11, + Reject::NotInClawbackWindow => 12, + Reject::BondTooSmall => 13, + Reject::UnsupportedFeature => 14, + Reject::ConservationViolated => 15, + Reject::Invalid => 16, + }; + ERR_BASE - offset +} + +fn reject_err(r: Reject) -> ErrorObjectOwned { + ErrorObjectOwned::owned(reject_code(&r), r.to_string(), None::<()>) +} + +fn invalid(msg: &str) -> ErrorObjectOwned { + ErrorObjectOwned::owned(-32602, msg.to_string(), None::<()>) +} + +fn not_found(what: &str) -> ErrorObjectOwned { + ErrorObjectOwned::owned(-32004, format!("{what} not found"), None::<()>) +} + +fn parse_hex(s: &str) -> RpcResult> { + let s = s.strip_prefix("0x").unwrap_or(s); + hex::decode(s).map_err(|e| invalid(&format!("bad hex: {e}"))) +} + +fn parse_hash(s: &str) -> RpcResult { + let v = parse_hex(s)?; + v.try_into().map_err(|_| invalid("expected 32-byte hex")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn version_is_set() { + assert!(!version().is_empty()); + } + + #[test] + fn parse_hash_roundtrip() { + let h = [7u8; 32]; + assert_eq!(parse_hash(&hex::encode(h)).unwrap(), h); + assert!(parse_hash("zz").is_err()); + } + + #[test] + fn reject_codes_are_distinct() { + let variants = [ + Reject::BadSignature, + Reject::BadNonce, + Reject::InsufficientBalance, + Reject::BadStatus, + Reject::Invalid, + ]; + let codes: Vec = variants.iter().map(reject_code).collect(); + let mut sorted = codes.clone(); + sorted.sort_unstable(); + sorted.dedup(); + assert_eq!(codes.len(), sorted.len()); + } +} diff --git a/node/src/main.rs b/node/src/main.rs index 6bb176b..d2c725d 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -1,24 +1,106 @@ -//! DUCP reference node (Rust). +//! DUCP reference node (Rust) โ€” CLI entry point. //! //! Runs a node on the DUCP network. Here, "node" means a *network participant* โ€” -//! the software you run to take part in DUCP (as a Provider, Validator, and so -//! on). It is unrelated to Node.js and contains no JavaScript. +//! the software you run to take part in DUCP (as a Provider, Validator, and so on). +//! It is unrelated to Node.js and contains no JavaScript. //! //! Specification: -//! Status: scaffold for spec v0.1.0 โ€” not yet operational. +//! Status: Profile 0 reference node for spec v0.2.0. + +use std::net::SocketAddr; + +use clap::Parser; +use ducp_node::{start_server, NodeHandle}; +use ducp_types::{keys, Identity, Ucu, UCU_SCALE}; + +/// Run a DUCP Profile 0 node (single-sequencer devnet). +#[derive(Parser, Debug)] +#[command(name = "ducp-node", version, about)] +struct Cli { + /// Address to bind the JSON-RPC server to. + #[arg(long, default_value = "127.0.0.1:8645")] + listen: SocketAddr, + + /// 32-byte hex seed for the sequencer's Ed25519 key. + #[arg( + long, + default_value = "0000000000000000000000000000000000000000000000000000000000000000" + )] + seed: String, + + /// Genesis allocation, repeatable: `--alloc :`. + /// If none are given, three dev keys (seeds 01.., 02.., 03..) are funded. + #[arg(long = "alloc", value_name = "IDENTITY_HEX:AMOUNT")] + alloc: Vec, +} + +fn parse_seed(hex_str: &str) -> anyhow::Result<[u8; 32]> { + let v = hex::decode(hex_str.strip_prefix("0x").unwrap_or(hex_str))?; + let arr: [u8; 32] = v + .try_into() + .map_err(|_| anyhow::anyhow!("seed must be 32 bytes"))?; + Ok(arr) +} + +fn parse_alloc(s: &str) -> anyhow::Result<(Identity, Ucu)> { + let (id_hex, amount) = s + .split_once(':') + .ok_or_else(|| anyhow::anyhow!("alloc must be :"))?; + let id_bytes = hex::decode(id_hex.strip_prefix("0x").unwrap_or(id_hex))?; + let id: Identity = id_bytes + .try_into() + .map_err(|_| anyhow::anyhow!("identity must be 32 bytes"))?; + let amount: Ucu = amount.parse()?; + Ok((id, amount)) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), + ) + .init(); + + let cli = Cli::parse(); + let seed = parse_seed(&cli.seed)?; + let proposer = keys::identity(&seed); + + let allocations: Vec<(Identity, Ucu)> = if cli.alloc.is_empty() { + // Default dev allocations: fund three well-known dev keys with 1,000,000 ๐•Œ. + let fund = 1_000_000 * UCU_SCALE; + vec![ + (keys::identity(&[1u8; 32]), fund), + (keys::identity(&[2u8; 32]), fund), + (keys::identity(&[3u8; 32]), fund), + ] + } else { + cli.alloc + .iter() + .map(|s| parse_alloc(s)) + .collect::>()? + }; + + let handle = NodeHandle::new(proposer, &allocations); -fn main() { println!( "DUCP node (Rust reference implementation) v{}", env!("CARGO_PKG_VERSION") ); - println!(" implements DUCP specification v0.1.0"); - println!(" https://github.com/ducp-protocol/spec"); - println!(); - println!("subsystems (scaffold โ€” not yet implemented):"); - println!(" dvm v{}", ducp_dvm::version()); - println!(" verification v{}", ducp_verification::version()); - println!(" ledger v{}", ducp_ledger::version()); - println!(" consensus v{}", ducp_consensus::version()); - println!(" governance v{}", ducp_governance::version()); + println!(" implements DUCP specification v0.2.0 (Profile 0)"); + println!(" sequencer {}", hex::encode(proposer)); + println!( + " benchmark v{} (fuel_per_ucu={})", + handle.benchmark.version, handle.benchmark.fuel_per_ucu + ); + println!(" genesis {} account(s)", allocations.len()); + + let (bound, server_handle) = start_server(handle, cli.listen).await?; + println!(" JSON-RPC http://{bound}"); + tracing::info!(%bound, "DUCP node listening"); + + tokio::signal::ctrl_c().await?; + tracing::info!("shutting down"); + server_handle.stop()?; + Ok(()) } diff --git a/node/tests/e2e.rs b/node/tests/e2e.rs new file mode 100644 index 0000000..4a6de3d --- /dev/null +++ b/node/tests/e2e.rs @@ -0,0 +1,334 @@ +//! M3 end-to-end: submit โ†’ match โ†’ execute โ†’ verify(sampling off) โ†’ settle, driven +//! over JSON-RPC against a live single-sequencer node. Asserts payment transfer, +//! work-issuance mint, Standing accrual, and the reward-neutral (๐•Œ, โ„š) entry. + +use ducp_consensus::{ConsensusEngine, SingleSequencer}; +use ducp_dvm::{echo_module, Benchmark, Dvm, WasmtimeDvm}; +use ducp_ledger::State; +use ducp_node::{start_server, DucpApiClient, NodeHandle}; +use ducp_types::{ + content_id, keys, ComputeProof, FailurePolicy, IrId, Limits, SignedTx, TaskBody, TierData, Tx, + VerificationTier, UCU_SCALE, +}; +use jsonrpsee::http_client::HttpClientBuilder; + +fn h32(s: &str) -> [u8; 32] { + hex::decode(s).unwrap().try_into().unwrap() +} + +#[tokio::test] +async fn happy_path_submit_to_settle_over_rpc() { + let req_seed = [1u8; 32]; + let prov_seed = [2u8; 32]; + let req = keys::identity(&req_seed); + let prov = keys::identity(&prov_seed); + + let handle = NodeHandle::new( + keys::identity(&[0u8; 32]), + &[(req, 1_000 * UCU_SCALE), (prov, 1_000 * UCU_SCALE)], + ); + let (addr, _server) = start_server(handle, "127.0.0.1:0".parse().unwrap()) + .await + .unwrap(); + let client = HttpClientBuilder::default() + .build(format!("http://{addr}")) + .unwrap(); + + // Requester uploads program + input as content-addressed blobs. + let program = echo_module(); + let input = b"hello ducp".to_vec(); + let prog_id = client + .put_blob(hex::encode(&program)) + .await + .unwrap() + .content_id; + let in_id = client + .put_blob(hex::encode(&input)) + .await + .unwrap() + .content_id; + + // Advisory metering before committing. + let est = client + .estimate_ucu(prog_id.clone(), in_id.clone(), 0) + .await + .unwrap(); + assert_ne!(est.ucu, "0"); + + // Submit the task. + let body = TaskBody { + ir: IrId::Wasm, + program: h32(&prog_id), + input: h32(&in_id), + limits: Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 16 * 1024 * 1024, + }, + tier: VerificationTier::SampledReexec, + benchmark: 0, + deadline: 100, + failure_policy: FailurePolicy::ReturnOnFailure, + nonce: 1, + }; + let task = body.task_id(); + let submit = client + .submit_task(SignedTx::sign(&req_seed, Tx::SubmitTask(body.clone()), 0)) + .await + .unwrap(); + assert_eq!(submit.task_id, hex::encode(task)); + + // Provider claims (posts Standing-discounted stake). + let claim = client + .claim_task(SignedTx::sign(&prov_seed, Tx::ClaimTask { task }, 0)) + .await + .unwrap(); + assert!(claim.ok); + assert_eq!(claim.claim_stake, (5 * UCU_SCALE).to_string()); // 0.5 ยท max_ucu + + // Provider executes deterministically and submits the proof. + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let outcome = dvm.execute(&program, &input, &body.limits, &bench); + assert_eq!(outcome.output, input); // echo + let proof = ComputeProof { + task, + provider: prov, + output: content_id(&outcome.output), + result_hash: outcome.result_hash, + ucu_count: outcome.ucu_count, + benchmark: 0, + tier_data: TierData::SampledReexec, + power_seal: None, + }; + let ok = client + .submit_proof(SignedTx::sign(&prov_seed, Tx::SubmitProof(proof), 1)) + .await + .unwrap(); + assert!(ok.ok); + + // Task settled. + let tv = client.get_task(hex::encode(task)).await.unwrap(); + assert_eq!(tv.status, "Settled"); + let receipt = tv.receipt.expect("receipt recorded"); + assert_eq!(receipt.paid_to_provider, outcome.ucu_count); + + // (๐•Œ, โ„š) pair recorded, โ„š null (I-Q-NULL / reward-neutral). + let q = client.get_q_entry(hex::encode(task)).await.unwrap(); + assert!(q.q.is_none()); + assert_eq!(q.ucu, outcome.ucu_count); + + // Provider: stake bonded; Standing accrued 1:1 with ๐•Œ. + let acct = client.get_account(hex::encode(prov)).await.unwrap(); + assert_eq!(acct.bonded, (5 * UCU_SCALE).to_string()); + let st = client.get_standing(hex::encode(prov)).await.unwrap(); + assert_eq!(st.sp, outcome.ucu_count.to_string()); + + // Head advanced past the three blocks (submit, claim, proof+settle). + let head = client.get_head().await.unwrap(); + assert!(head.height >= 3, "head height {}", head.height); +} + +#[tokio::test] +async fn fraudulent_proof_is_challenged_and_slashed() { + let req_seed = [1u8; 32]; + let prov_seed = [2u8; 32]; + let chal_seed = [3u8; 32]; + let req = keys::identity(&req_seed); + let prov = keys::identity(&prov_seed); + let chal = keys::identity(&chal_seed); + + let handle = NodeHandle::new( + keys::identity(&[0u8; 32]), + &[ + (req, 1_000 * UCU_SCALE), + (prov, 1_000 * UCU_SCALE), + (chal, 1_000 * UCU_SCALE), + ], + ); + let (addr, _server) = start_server(handle, "127.0.0.1:0".parse().unwrap()) + .await + .unwrap(); + let client = HttpClientBuilder::default() + .build(format!("http://{addr}")) + .unwrap(); + + let program = echo_module(); + let input = b"verify me".to_vec(); + let prog_id = client + .put_blob(hex::encode(&program)) + .await + .unwrap() + .content_id; + let in_id = client + .put_blob(hex::encode(&input)) + .await + .unwrap() + .content_id; + + let body = TaskBody { + ir: IrId::Wasm, + program: h32(&prog_id), + input: h32(&in_id), + limits: Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 16 * 1024 * 1024, + }, + tier: VerificationTier::SampledReexec, + benchmark: 0, + deadline: 100, + failure_policy: FailurePolicy::ReturnOnFailure, + nonce: 1, + }; + let task = body.task_id(); + client + .submit_task(SignedTx::sign(&req_seed, Tx::SubmitTask(body.clone()), 0)) + .await + .unwrap(); + client + .claim_task(SignedTx::sign(&prov_seed, Tx::ClaimTask { task }, 0)) + .await + .unwrap(); + + // Provider submits a FRAUDULENT proof: correct ucu_count, forged result_hash. + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let honest = dvm.execute(&program, &input, &body.limits, &bench); + let fraud_proof = ComputeProof { + task, + provider: prov, + output: content_id(b"forged"), + result_hash: [0xBA; 32], // forged โ€” not the real echo hash + ucu_count: honest.ucu_count, + benchmark: 0, + tier_data: TierData::SampledReexec, + power_seal: None, + }; + client + .submit_proof(SignedTx::sign(&prov_seed, Tx::SubmitProof(fraud_proof), 1)) + .await + .unwrap(); + + // It settled optimistically. + let tv = client.get_task(hex::encode(task)).await.unwrap(); + assert_eq!(tv.status, "Settled"); + + // A challenger contests within the clawback window. + let p = honest.ucu_count; + let bond = p / 4 + 1; // โ‰ฅ bond_min = 0.25ยทP + client + .challenge(SignedTx::sign(&chal_seed, Tx::Challenge { task, bond }, 0)) + .await + .unwrap(); + + // Fraud proven: provider Standing floored, task failed, challenger rewarded. + let prov_standing = client.get_standing(hex::encode(prov)).await.unwrap(); + assert_eq!(prov_standing.sp, "0"); + assert_eq!(prov_standing.strikes, 1); + let tv = client.get_task(hex::encode(task)).await.unwrap(); + assert_eq!(tv.status, "Failed"); + let chal_acct = client.get_account(hex::encode(chal)).await.unwrap(); + // Challenger's bonded is released; balance is back near the start (reward โ‰ฅ 0). + assert_eq!(chal_acct.bonded, "0"); +} + +#[tokio::test] +async fn replica_replays_blocks_to_identical_state_root() { + // 1 sequencer + a replica that fetches blocks over RPC and replays them. + let req_seed = [1u8; 32]; + let prov_seed = [2u8; 32]; + let req = keys::identity(&req_seed); + let prov = keys::identity(&prov_seed); + let proposer = keys::identity(&[0u8; 32]); + let allocations = [(req, 1_000 * UCU_SCALE), (prov, 1_000 * UCU_SCALE)]; + + let handle = NodeHandle::new(proposer, &allocations); + let (addr, _server) = start_server(handle, "127.0.0.1:0".parse().unwrap()) + .await + .unwrap(); + let client = HttpClientBuilder::default() + .build(format!("http://{addr}")) + .unwrap(); + + // Drive a happy path so the chain has several blocks. + let program = echo_module(); + let input = b"replicate me".to_vec(); + let prog_id = client + .put_blob(hex::encode(&program)) + .await + .unwrap() + .content_id; + let in_id = client + .put_blob(hex::encode(&input)) + .await + .unwrap() + .content_id; + let body = TaskBody { + ir: IrId::Wasm, + program: h32(&prog_id), + input: h32(&in_id), + limits: Limits { + max_ucu: 10 * UCU_SCALE, + max_memory_bytes: 16 * 1024 * 1024, + }, + tier: VerificationTier::SampledReexec, + benchmark: 0, + deadline: 100, + failure_policy: FailurePolicy::ReturnOnFailure, + nonce: 1, + }; + let task = body.task_id(); + client + .submit_task(SignedTx::sign(&req_seed, Tx::SubmitTask(body.clone()), 0)) + .await + .unwrap(); + client + .claim_task(SignedTx::sign(&prov_seed, Tx::ClaimTask { task }, 0)) + .await + .unwrap(); + let dvm = WasmtimeDvm::new(); + let bench = Benchmark::devnet(&dvm); + let outcome = dvm.execute(&program, &input, &body.limits, &bench); + let proof = ComputeProof { + task, + provider: prov, + output: content_id(&outcome.output), + result_hash: outcome.result_hash, + ucu_count: outcome.ucu_count, + benchmark: 0, + tier_data: TierData::SampledReexec, + power_seal: None, + }; + client + .submit_proof(SignedTx::sign(&prov_seed, Tx::SubmitProof(proof), 1)) + .await + .unwrap(); + + // Replica: fresh genesis + sequencer, replay every block fetched over RPC. + let head = client.get_head().await.unwrap(); + let mut replica_seq = SingleSequencer::new(proposer); + let mut replica_state = State::genesis(&allocations, 0); + for height in 1..=head.height { + let block = client.get_block(height).await.unwrap(); + let txs = client.get_block_txs(height).await.unwrap(); + replica_state = replica_seq + .commit(&block, &txs, &replica_state, &Default::default()) + .expect("replica replay"); + replica_seq.adopt(&block); + } + + // The replica converges on the sequencer's committed state_root. + assert_eq!(hex::encode(replica_state.state_root()), head.state_root); +} + +#[tokio::test] +async fn unknown_task_query_errors() { + let handle = NodeHandle::new(keys::identity(&[0u8; 32]), &[]); + let (addr, _server) = start_server(handle, "127.0.0.1:0".parse().unwrap()) + .await + .unwrap(); + let client = HttpClientBuilder::default() + .build(format!("http://{addr}")) + .unwrap(); + let res = client.get_task(hex::encode([9u8; 32])).await; + assert!(res.is_err()); +} diff --git a/scripts/devnet.sh b/scripts/devnet.sh new file mode 100755 index 0000000..ba18cfe --- /dev/null +++ b/scripts/devnet.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# +# Profile 0 devnet demo: 1 single-sequencer node + 1 worker. +# +# Starts a ducp-node sequencer, then runs the beachhead workload against it via +# JSON-RPC (submit -> claim -> execute -> proof -> settle), repeatedly. +# +# PORT=8650 TASKS=5 scripts/devnet.sh +# +set -euo pipefail +cd "$(dirname "$0")/.." + +PORT="${PORT:-8650}" +TASKS="${TASKS:-5}" + +echo "==> building node + worker" +cargo build -p ducp-node --bins + +echo "==> starting sequencer on 127.0.0.1:${PORT}" +./target/debug/ducp-node --listen "127.0.0.1:${PORT}" & +SEQ_PID=$! +trap 'kill "${SEQ_PID}" 2>/dev/null || true' EXIT + +# Wait for the RPC port to accept connections. +for _ in $(seq 1 30); do + if nc -z 127.0.0.1 "${PORT}" 2>/dev/null; then break; fi + sleep 0.2 +done + +echo "==> running worker (${TASKS} tasks)" +./target/debug/ducp-worker --sequencer "http://127.0.0.1:${PORT}" --tasks "${TASKS}" + +echo "==> devnet demo complete" diff --git a/test-vectors/README.md b/test-vectors/README.md new file mode 100644 index 0000000..1137987 --- /dev/null +++ b/test-vectors/README.md @@ -0,0 +1,19 @@ +# DUCP Profile 0 โ€” Conformance Test Vectors + +Golden vectors published with the reference implementation. A conforming node MUST +reproduce all of them (spec/implementation/05 ยง5, spec/09 ยง10). They are the +per-milestone acceptance gates and the cross-implementation interop contract. + +Loaded by the [`ducp-conformance`](../crates/conformance) harness. + +| Family | Dir | Source | Milestone | +|---|---|---|---| +| Codec / hash | `codec/` | spec/implementation/01 ยง7 | M0 | +| Metering | `metering/` | spec/implementation/02 ยง5 | M1 | +| Settlement | `settlement/` | spec/implementation/04 ยง3 | M2/M3 | +| Fraud | `fraud/` | spec/implementation/03 ยง4 | M4/M5 | +| Replication | `replication/` | spec/implementation/04 ยง6 | M6 | +| โ„š observable | `q-observable/` | spec/09 ยง10, DP-0001 ยง9 | cross-cutting | + +Binary values are hex-encoded strings. Amounts are decimal strings (๐•Œ base units, +1 ๐•Œ = 10โน). All hashes are BLAKE3-256 over canonical (borsh) bytes. diff --git a/test-vectors/codec/types.json b/test-vectors/codec/types.json new file mode 100644 index 0000000..dcb6d4d --- /dev/null +++ b/test-vectors/codec/types.json @@ -0,0 +1,191 @@ +[ + { + "name": "task_body", + "value": { + "benchmark": 0, + "deadline": 100, + "failure_policy": "return_on_failure", + "input": "2222222222222222222222222222222222222222222222222222222222222222", + "ir": "wasm", + "limits": { + "max_memory_bytes": 1048576, + "max_ucu": "10000000000" + }, + "nonce": 7, + "program": "1111111111111111111111111111111111111111111111111111111111111111", + "tier": "sampled_reexec" + }, + "canonical_hex": "001111111111111111111111111111111111111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222200e40b54020000000000000000000000000010000000000000000000006400000000000000010700000000000000", + "hash": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe" + }, + { + "name": "limits", + "value": { + "max_memory_bytes": 1048576, + "max_ucu": "10000000000" + }, + "canonical_hex": "00e40b540200000000000000000000000000100000000000", + "hash": "9f36d4c7f459e1ce735ba18b79501cd3d752e395aa66463743e2de0c35a5bf02" + }, + { + "name": "submission", + "value": { + "claim_stake": "0", + "fee": "10000000", + "provider": null, + "requester": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c", + "status": "submitted", + "task": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe", + "ucu_count": "0" + }, + "canonical_hex": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c0000000000000000000000000000000080969800000000000000000000000000000000000000000000000000000000000000", + "hash": "4f9cb481eab3c09ee3444e20f0feb900f85e3d82fa1d2347d98dbaa24e72f857" + }, + { + "name": "compute_proof_no_seal", + "value": { + "benchmark": 0, + "output": "6c7f030d06ffea2138789dc7c7c97d18f959f17b9960d3ed9a0f16b9e1984fea", + "power_seal": null, + "provider": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394", + "result_hash": "6c7f030d06ffea2138789dc7c7c97d18f959f17b9960d3ed9a0f16b9e1984fea", + "task": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe", + "tier_data": "sampled_reexec", + "ucu_count": "4000000000" + }, + "canonical_hex": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b3946c7f030d06ffea2138789dc7c7c97d18f959f17b9960d3ed9a0f16b9e1984fea6c7f030d06ffea2138789dc7c7c97d18f959f17b9960d3ed9a0f16b9e1984fea00286bee000000000000000000000000000000000000", + "hash": "18710787ec665a0c5d6e6cf9a0a7bfa5ff8277ea0644d44dce75420a4f7e470b" + }, + { + "name": "power_seal", + "value": { + "attestation_evidence": "0069321a584d777ae8b5c4957cf4bf6bc24155531d8b73e409589ad23c1796dc", + "benchmark": 0, + "boundary": "node", + "power_cap_milliwatts": 300000, + "seal_grade": "S1", + "t_max_millikelvin": 350000, + "window_millis": 1000 + }, + "canonical_hex": "0101e093040000000000e80300000000000030570500000000000069321a584d777ae8b5c4957cf4bf6bc24155531d8b73e409589ad23c1796dc00000000", + "hash": "fa51c91277ecf77e6b5e5a2f67919c364ffe1821e2f208fe8a348bf7a1babbd5" + }, + { + "name": "compute_proof_with_seal", + "value": { + "benchmark": 0, + "output": "6c7f030d06ffea2138789dc7c7c97d18f959f17b9960d3ed9a0f16b9e1984fea", + "power_seal": { + "attestation_evidence": "0069321a584d777ae8b5c4957cf4bf6bc24155531d8b73e409589ad23c1796dc", + "benchmark": 0, + "boundary": "node", + "power_cap_milliwatts": 300000, + "seal_grade": "S1", + "t_max_millikelvin": 350000, + "window_millis": 1000 + }, + "provider": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394", + "result_hash": "6c7f030d06ffea2138789dc7c7c97d18f959f17b9960d3ed9a0f16b9e1984fea", + "task": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe", + "tier_data": "sampled_reexec", + "ucu_count": "4000000000" + }, + "canonical_hex": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b3946c7f030d06ffea2138789dc7c7c97d18f959f17b9960d3ed9a0f16b9e1984fea6c7f030d06ffea2138789dc7c7c97d18f959f17b9960d3ed9a0f16b9e1984fea00286bee0000000000000000000000000000000000010101e093040000000000e80300000000000030570500000000000069321a584d777ae8b5c4957cf4bf6bc24155531d8b73e409589ad23c1796dc00000000", + "hash": "5be4a8ab1ddab7b7e96dc16d76a6be98d1e4af20721f96601be260c305fcb4d7" + }, + { + "name": "receipt", + "value": { + "clawback_until": 44, + "paid_to_provider": "4000000000", + "settled_epoch": 12, + "standing_delta": "4000000000", + "task": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe", + "validator_fee": "10000000", + "work_issuance": "40000000" + }, + "canonical_hex": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe00286bee000000000000000000000000005a62020000000000000000000000008096980000000000000000000000000000286bee0000000000000000000000000c000000000000002c00000000000000", + "hash": "a61c9aaef3dec90e36c6a226a0b3d300c85f5194bd4c49819def51f6cf6da1d6" + }, + { + "name": "account", + "value": { + "balance": "1000000000000", + "bonded": "2000000000", + "escrowed": "0", + "id": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394" + }, + "canonical_hex": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b3940010a5d4e800000000000000000000000000000000000000000000000000000000943577000000000000000000000000", + "hash": "99d42598ef3646e150421e2167ca728b32c78cfa96eff2b4d2972ec6d73cedef" + }, + { + "name": "standing_record", + "value": { + "id": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394", + "last_decay_epoch": 12, + "sp": "123000000000", + "strikes": 0 + }, + "canonical_hex": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394000e5fa31c00000000000000000000000c0000000000000000000000", + "hash": "bca35fcbee69c65a965fd7cac3d0b9bbd5c9853c21518a33d22f87d011a3fd12" + }, + { + "name": "signed_transfer", + "value": { + "author": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", + "nonce": 1, + "sig": "8bbfd7a3aac1a1e4f544459b539391445d7e64df0aba40bde0d5212e85e5da4473316a501f04547fae39d1005938590c6eb42c3c5ecd5b3f3694e76dbbbf5f0f", + "tx": { + "transfer": { + "amount": "5000000000", + "to": "fd1724385aa0c75b64fb78cd602fa1d991fdebf76b13c58ed702eac835e9f618" + } + } + }, + "canonical_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c04fd1724385aa0c75b64fb78cd602fa1d991fdebf76b13c58ed702eac835e9f61800f2052a01000000000000000000000001000000000000008bbfd7a3aac1a1e4f544459b539391445d7e64df0aba40bde0d5212e85e5da4473316a501f04547fae39d1005938590c6eb42c3c5ecd5b3f3694e76dbbbf5f0f", + "hash": "507e7b02bb6d121ae997bebf65b7db36d2a4e89edeacfd8bc9279ad6d515d571" + }, + { + "name": "q_entry_null", + "value": { + "benchmark": 0, + "boundary": null, + "q": null, + "seal_grade": null, + "task": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe", + "ucu": "4000000000" + }, + "canonical_hex": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe00286bee00000000000000000000000000000000000000", + "hash": "d80dd77a05aaf4a933bf640130a49bce162b27f014d4f330bb4e79ad31e14f59" + }, + { + "name": "q_entry_valued", + "value": { + "benchmark": 0, + "boundary": "chip", + "q": { + "micro_q": 1640000 + }, + "seal_grade": "S2", + "task": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe", + "ucu": "4000000000" + }, + "canonical_hex": "c6c23f0a280dfa69526ff3a593d58b8b45fa417e715dc43444b34c2aacbde3fe00286bee0000000000000000000000000140061900000000000102010000000000", + "hash": "ad1044ad1465631532bf2d81307990da6067b63c4113a07a9ace237ce970d39b" + }, + { + "name": "block", + "value": { + "epoch": 0, + "height": 1, + "parent": "0000000000000000000000000000000000000000000000000000000000000000", + "proposer": "e734ea6c2b6257de72355e472aa05a4c487e6b463c029ed306df2f01b5636b58", + "state_root": "0534aa23c5a55c4d046b124806c16822362b40e0d1803b6fbee0cf40fc6c3435", + "txs": [ + "507e7b02bb6d121ae997bebf65b7db36d2a4e89edeacfd8bc9279ad6d515d571" + ] + }, + "canonical_hex": "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000507e7b02bb6d121ae997bebf65b7db36d2a4e89edeacfd8bc9279ad6d515d5710534aa23c5a55c4d046b124806c16822362b40e0d1803b6fbee0cf40fc6c3435e734ea6c2b6257de72355e472aa05a4c487e6b463c029ed306df2f01b5636b58", + "hash": "6be5e00d4fff97f78162a6911b3688562994cb00de984b01023d931660794caf" + } +] diff --git a/test-vectors/fraud/challenge.json b/test-vectors/fraud/challenge.json new file mode 100644 index 0000000..f5a4c17 --- /dev/null +++ b/test-vectors/fraud/challenge.json @@ -0,0 +1,34 @@ +{ + "forged_result_hash": "babababababababababababababababababababababababababababababababa", + "true_result_hash": "ca5f7a1f407a1a35a21511f3341a86f34aada14cb485a1ad96286228a9a946e6", + "verdict_is_fraud": true, + "requester": { + "id": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c", + "balance": "99990000000", + "escrowed": "0", + "bonded": "0" + }, + "provider": { + "id": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394", + "balance": "95000007142", + "escrowed": "0", + "bonded": "4999978574" + }, + "challenger": { + "id": "ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1", + "balance": "100000007142", + "escrowed": "0", + "bonded": "0" + }, + "provider_standing": { + "id": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394", + "sp": "0", + "last_decay_epoch": 0, + "strikes": 1 + }, + "minted": "300000000071", + "burned": "7213", + "fee_pool": "10000000", + "conserved": true, + "state_root": "bc634ab64be17981157015001d73a5db7bfff7b742649a6e1bd4e9d30b6559ef" +} diff --git a/test-vectors/metering/cases.json b/test-vectors/metering/cases.json new file mode 100644 index 0000000..aa8dbfd --- /dev/null +++ b/test-vectors/metering/cases.json @@ -0,0 +1,23 @@ +[ + { + "name": "reference", + "input_hex": "", + "total_fuel": 1400014, + "ucu_count": "1000000000", + "result_hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262" + }, + { + "name": "echo_empty", + "input_hex": "", + "total_fuel": 10, + "ucu_count": "7142", + "result_hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262" + }, + { + "name": "echo_hello_world", + "input_hex": "68656c6c6f20776f726c64", + "total_fuel": 10, + "ucu_count": "7142", + "result_hash": "d74981efa70a0c880b8d8c1985d075dbcbf679b99a5f9914e5aaf96b831a9e24" + } +] diff --git a/test-vectors/q-observable/dp0001.json b/test-vectors/q-observable/dp0001.json new file mode 100644 index 0000000..11fc104 --- /dev/null +++ b/test-vectors/q-observable/dp0001.json @@ -0,0 +1,34 @@ +{ + "ucu": "50000000000000", + "providers": [ + { + "label": "A", + "micro_q": 428571, + "seal_grade": "S1", + "boundary": "node", + "paid_ucu": "50000000000000" + }, + { + "label": "B", + "micro_q": 1000000, + "seal_grade": "S2", + "boundary": "chip", + "paid_ucu": "50000000000000" + }, + { + "label": "C", + "micro_q": 1644000, + "seal_grade": "S2", + "boundary": "chip", + "paid_ucu": "50000000000000" + }, + { + "label": "D", + "micro_q": null, + "seal_grade": null, + "boundary": null, + "paid_ucu": "50000000000000" + } + ], + "all_payments_equal": true +} diff --git a/test-vectors/replication/blocks.json b/test-vectors/replication/blocks.json new file mode 100644 index 0000000..b676edc --- /dev/null +++ b/test-vectors/replication/blocks.json @@ -0,0 +1,11 @@ +{ + "blocks": 4, + "block_state_roots": [ + "d46017e52596a0fbde92926e090dbbbd22488509f49adf9538c71bc662378173", + "b32522dfe47316bab659c34b15fb72b59878472eeb3085186036e0164a0d1425", + "f8788e6922f7d2d080cdfac7f59cb08d066e2fce6ae29f7de73b9002f88f7a0c", + "a00e46a403b63685a1a0aca121a178e3844cc6a0b6b329794607edeebc178f52" + ], + "final_state_root": "a00e46a403b63685a1a0aca121a178e3844cc6a0b6b329794607edeebc178f52", + "replica_matches": true +} diff --git a/test-vectors/settlement/finality.json b/test-vectors/settlement/finality.json new file mode 100644 index 0000000..d7cfe09 --- /dev/null +++ b/test-vectors/settlement/finality.json @@ -0,0 +1,22 @@ +{ + "epoch": 32, + "provider": { + "id": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394", + "balance": "104040000000", + "escrowed": "0", + "bonded": "0" + }, + "receipt": { + "task": "3aa7e2573318aa44cefeb9ad80d0e3feff046678d4c12555adf38d7bcbca85d5", + "paid_to_provider": "4000000000", + "work_issuance": "40000000", + "validator_fee": "10000000", + "standing_delta": "4000000000", + "settled_epoch": 0, + "clawback_until": 32 + }, + "released": true, + "receipt_unchanged": true, + "conserved": true, + "state_root": "0dabb4ddc416dfd14b1ba29ba7d9be2ede4311494e01f680c529288ef3e9669b" +} diff --git a/test-vectors/settlement/happy_path.json b/test-vectors/settlement/happy_path.json new file mode 100644 index 0000000..6f2bf00 --- /dev/null +++ b/test-vectors/settlement/happy_path.json @@ -0,0 +1,40 @@ +{ + "state_root": "185a4051471ed15becb9cfb40b7dcb679d457f5a3ec9639b983beecf61f7f0b7", + "requester": { + "id": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c", + "balance": "95990000000", + "escrowed": "0", + "bonded": "0" + }, + "provider": { + "id": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394", + "balance": "99040000000", + "escrowed": "0", + "bonded": "5000000000" + }, + "provider_standing": { + "id": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394", + "sp": "4000000000", + "last_decay_epoch": 0, + "strikes": 0 + }, + "receipt": { + "task": "3aa7e2573318aa44cefeb9ad80d0e3feff046678d4c12555adf38d7bcbca85d5", + "paid_to_provider": "4000000000", + "work_issuance": "40000000", + "validator_fee": "10000000", + "standing_delta": "4000000000", + "settled_epoch": 0, + "clawback_until": 32 + }, + "q_entry": { + "task": "3aa7e2573318aa44cefeb9ad80d0e3feff046678d4c12555adf38d7bcbca85d5", + "ucu": "4000000000", + "q": null, + "seal_grade": null, + "boundary": null, + "benchmark": 0 + }, + "minted": "200040000000", + "fee_pool": "10000000" +}