diff --git a/.github/actions/build-dependencies/action.yml b/.github/actions/build-dependencies/action.yml index 18cb5b6ac..ec8e80a08 100644 --- a/.github/actions/build-dependencies/action.yml +++ b/.github/actions/build-dependencies/action.yml @@ -53,7 +53,7 @@ runs: - name: Install solc shell: bash run: | - cargo install svm-rs --version =0.5.23 + cargo +1.94.1 install --locked svm-rs --version =0.5.23 solc_version() { # Install the version of `solc` specified in the contracts diff --git a/.github/workflows/daily-deny.yml b/.github/workflows/daily-deny.yml index 6d718cd01..5bbd2b29c 100644 --- a/.github/workflows/daily-deny.yml +++ b/.github/workflows/daily-deny.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 - name: Install `cargo deny` - run: cargo +1.94.0 install cargo-deny --version =0.19.0 + run: cargo +1.94.1 install --locked cargo-deny --version =0.19.0 - name: Run `cargo deny` run: cargo deny -L error --all-features check --hide-inclusion-graph diff --git a/.github/workflows/libraries-on-redox.yml b/.github/workflows/libraries-on-redox.yml index 062578e7f..31d43193b 100644 --- a/.github/workflows/libraries-on-redox.yml +++ b/.github/workflows/libraries-on-redox.yml @@ -28,7 +28,7 @@ jobs: - name: Run Tests for Libraries on Redoxer shell: bash run: | - cargo +1.94.0 install redoxer --version =0.2.62 + cargo +1.94.1 install --locked redoxer --version =0.2.62 TARGET=x86_64-unknown-redox redoxer toolchain find ./common ./crypto -name "Cargo.toml" | while IFS="\n" read -r manifest; do diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5bea97b3d..7b7fc0ab9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -106,7 +106,7 @@ jobs: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 - name: Install cargo deny - run: cargo +1.94.0 install cargo-deny --version =0.19.0 + run: cargo +1.94.1 install --locked cargo-deny --version =0.19.0 - name: Run cargo deny run: cargo deny -L error --all-features check --hide-inclusion-graph @@ -152,8 +152,8 @@ jobs: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 - name: Verify all dependencies are in use run: | - cargo +1.94.0 install cargo-machete --version =0.9.1 - cargo +1.94.0 machete + cargo +1.94.1 install --locked cargo-machete --version =0.9.1 + cargo +1.94.1 machete msrv: runs-on: ubuntu-latest @@ -164,7 +164,7 @@ jobs: - name: Verify claimed `rust-version` shell: bash run: | - cargo +1.94.0 install cargo-msrv --version =0.19.3 + cargo +1.94.1 install --locked cargo-msrv --version =0.19.3 # Remove `patches/{home, getrandom}` as they force the entire workspace to Rust 1.85(+) echo "$(cat ./Cargo.toml | grep -v 'patches/home' | grep -v 'patches/getrandom')" > ./Cargo.toml @@ -244,6 +244,19 @@ jobs: } check_workspace + vet: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 + + - name: Install cargo vet + run: cargo +1.94.1 install --locked cargo-vet --git https://github.com/kayabaNerve/cargo-vet --rev d2fb27daaeb839e5fa4f6b28c5cdd4a9185542b5 + + - name: Run cargo vet + run: | + cargo vet --locked + ./supply-chain/no-first-party.sh + slither: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/reproducible-runtime.yml b/.github/workflows/reproducible-runtime.yml index ee991a55e..9fd33b0f6 100644 --- a/.github/workflows/reproducible-runtime.yml +++ b/.github/workflows/reproducible-runtime.yml @@ -132,8 +132,8 @@ jobs: source_of_truth: ${{ needs.source_of_truth.outputs.hash }} shell: bash run: | - rustup toolchain install 1.94.0 --profile minimal --component rust-src - cargo +1.94.0 build -p serai-runtime --release --no-default-features + rustup toolchain install 1.94.1 --profile minimal --component rust-src + cargo +1.94.1 build -p serai-runtime --release --no-default-features HASH=$(sha256sum $(find ./target -name serai_runtime.wasm | head -n1) | cut -d' ' -f1) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d4ff84819..d0fc42b83 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,40 +37,16 @@ jobs: - name: Run Tests run: | - cargo test --all-features \ - -p serai-message-queue \ - -p serai-processor-messages \ - -p serai-processor-key-gen \ - -p serai-processor-view-keys \ - -p serai-processor-frost-attempt-manager \ - -p serai-processor-primitives \ - -p serai-processor-scanner \ - -p serai-processor-scheduler-primitives \ - -p serai-processor-utxo-scheduler-primitives \ - -p serai-processor-utxo-scheduler \ - -p serai-processor-transaction-chaining-scheduler \ - -p serai-processor-smart-contract-scheduler \ - -p serai-processor-signers \ - -p serai-processor-bin \ - -p serai-bitcoin-processor \ - -p serai-processor-ethereum-primitives \ - -p serai-ethereum-test-primitives \ - -p serai-processor-ethereum-deployer \ - -p serai-processor-ethereum-router \ - -p serai-processor-ethereum-erc20 \ - -p serai-ethereum-processor \ - -p serai-monero-processor \ - -p tendermint-machine \ - -p tributary-sdk \ - -p serai-cosign-types \ - -p serai-cosign \ - -p serai-coordinator-substrate \ - -p serai-coordinator-tributary \ - -p serai-coordinator-p2p \ - -p serai-coordinator-libp2p-p2p \ - -p serai-coordinator \ - -p serai-orchestrator \ - -p serai-docker-tests + cargo test --all-features -p serai-message-queue + find ./processor -name "Cargo.toml" | while IFS="\n" read -r manifest; do + cargo test --all-features --manifest-path $manifest + done + find ./coordinator -name "Cargo.toml" | while IFS="\n" read -r manifest; do + cargo test --all-features --manifest-path $manifest + done + cargo test --all-features -p serai-orchestrator + cargo test --all-features -p serai-shim-rpc + cargo test --all-features -p serai-docker-tests test-substrate: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index b696325d9..63569f05d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,7 +39,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.17", + "cpufeatures 0.2.99", ] [[package]] @@ -1427,7 +1427,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.17", + "cpufeatures 0.2.99", ] [[package]] @@ -1485,7 +1485,7 @@ name = "ciphersuite" version = "0.4.99" dependencies = [ "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "std-shims 0.1.5", "zeroize", ] @@ -1595,7 +1595,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures 0.2.99", "proptest", "serde_core", ] @@ -1701,11 +1701,9 @@ dependencies = [ [[package]] name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +version = "0.4.99" dependencies = [ - "memchr", + "std-shims 0.1.5", ] [[package]] @@ -1716,11 +1714,9 @@ checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" [[package]] name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +version = "0.2.99" dependencies = [ - "libc", + "cpufeatures 0.3.0", ] [[package]] @@ -1990,7 +1986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures 0.2.99", "curve25519-dalek-derive", "digest 0.10.7", "ff", @@ -2030,30 +2026,37 @@ dependencies = [ ] [[package]] -name = "dalek-ff-group" -version = "0.5.99" +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "crypto-bigint 0.5.5", - "crypto-bigint 0.7.3", - "dalek-ff-group 0.5.0", - "prime-field", + "darling_core 0.20.11", + "darling_macro 0.20.11", ] [[package]] name = "darling" -version = "0.20.99" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling 0.21.3", + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] -name = "darling" -version = "0.21.3" +name = "darling_core" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ - "darling_core", - "darling_macro", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", ] [[package]] @@ -2071,13 +2074,24 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", "quote", "syn 2.0.117", ] @@ -2325,7 +2339,7 @@ dependencies = [ "blake2 0.11.0-rc.5", "ciphersuite 0.4.2", "ciphersuite-kp256", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "dkg", "dkg-recovery", "ec-divisors", @@ -2335,7 +2349,7 @@ dependencies = [ "generalized-bulletproofs-circuit-abstraction", "generalized-bulletproofs-ec-gadgets", "generic-array 1.3.5", - "multiexp 0.5.0", + "multiexp", "rand 0.8.5", "rand_chacha 0.3.1", "rand_core 0.6.4", @@ -2350,10 +2364,10 @@ name = "dkg-musig" version = "0.6.0" dependencies = [ "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "dkg", "dkg-recovery", - "multiexp 0.5.0", + "multiexp", "rand_core 0.6.4", "std-shims 0.1.5", "thiserror 2.0.18", @@ -2415,9 +2429,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ec-divisors" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide?rev=dc1b3dbe436aae61ec363505052d4715d38ce1df#dc1b3dbe436aae61ec363505052d4715d38ce1df" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" dependencies = [ - "dalek-ff-group 0.5.99", + "dalek-ff-group", "ff", "group", "rand_core 0.6.4", @@ -2623,7 +2637,7 @@ dependencies = [ "group", "k256", "rand_core 0.6.4", - "sha3 0.10.8", + "sha3 0.11.0", "std-shims 0.1.5", "subtle", "tokio", @@ -3122,22 +3136,23 @@ checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" [[package]] name = "generalized-bulletproofs" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide?rev=dc1b3dbe436aae61ec363505052d4715d38ce1df#dc1b3dbe436aae61ec363505052d4715d38ce1df" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" dependencies = [ "blake2 0.10.6", "ciphersuite 0.4.99", "ff", "flexible-transcript", - "multiexp 0.4.2", + "multiexp", "rand_core 0.6.4", "std-shims 0.1.99", + "subtle", "zeroize", ] [[package]] name = "generalized-bulletproofs-circuit-abstraction" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide?rev=dc1b3dbe436aae61ec363505052d4715d38ce1df#dc1b3dbe436aae61ec363505052d4715d38ce1df" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" dependencies = [ "ciphersuite 0.4.99", "generalized-bulletproofs", @@ -3148,7 +3163,7 @@ dependencies = [ [[package]] name = "generalized-bulletproofs-ec-gadgets" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide?rev=dc1b3dbe436aae61ec363505052d4715d38ce1df#dc1b3dbe436aae61ec363505052d4715d38ce1df" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=0d6f5e840ad1f955e4e4dec00c5165f134815b15#0d6f5e840ad1f955e4e4dec00c5165f134815b15" dependencies = [ "ciphersuite 0.4.99", "generalized-bulletproofs-circuit-abstraction", @@ -3312,12 +3327,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.13.2" @@ -3416,9 +3425,10 @@ checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" [[package]] name = "hex_lit" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +version = "0.1.99" +dependencies = [ + "hex-literal", +] [[package]] name = "hickory-proto" @@ -3794,14 +3804,7 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] +version = "1.99.99" [[package]] name = "indexmap" @@ -4010,7 +4013,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures 0.2.17", + "cpufeatures 0.2.99", ] [[package]] @@ -4027,7 +4030,7 @@ dependencies = [ name = "keccak-asm" version = "0.1.99" dependencies = [ - "sha3 0.10.8", + "sha3 0.11.0", ] [[package]] @@ -4064,7 +4067,7 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.99.0" +version = "1.99.99" [[package]] name = "leb128fmt" @@ -4762,7 +4765,7 @@ dependencies = [ "hex", "prime-field", "rand_core 0.6.4", - "sha3 0.11.0-rc.9", + "sha3 0.11.0", "zeroize", ] @@ -4789,7 +4792,7 @@ version = "0.11.0" dependencies = [ "ciphersuite 0.4.2", "ciphersuite-kp256", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "dkg", "dkg-dealer", "dkg-recovery", @@ -4797,7 +4800,7 @@ dependencies = [ "flexible-transcript", "hex", "minimal-ed448", - "multiexp 0.5.0", + "multiexp", "rand_chacha 0.3.1", "rand_core 0.6.4", "schnorr-signatures", @@ -4877,7 +4880,7 @@ version = "0.1.0" source = "git+https://github.com/monero-oxide/monero-oxide?rev=c8be5d3d1287669946a83fbfcb296ce2a8852e47#c8be5d3d1287669946a83fbfcb296ce2a8852e47" dependencies = [ "curve25519-dalek", - "dalek-ff-group 0.5.99", + "dalek-ff-group", "flexible-transcript", "group", "modular-frost", @@ -5072,22 +5075,8 @@ dependencies = [ [[package]] name = "multiexp" version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec2ce93a6f06ac6cae04c1da3f2a6a24fcfc1f0eb0b4e0f3d302f0df45326cb" -dependencies = [ - "ff", - "group", - "rand_core 0.6.4", - "rustversion", - "std-shims 0.1.99", - "zeroize", -] - -[[package]] -name = "multiexp" -version = "0.5.0" dependencies = [ - "dalek-ff-group 0.5.0", + "dalek-ff-group", "ff", "group", "k256", @@ -5237,9 +5226,7 @@ checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" [[package]] name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +version = "0.3.99" [[package]] name = "nu-ansi-term" @@ -5396,7 +5383,7 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "option-ext" -version = "0.2.0" +version = "0.2.99" [[package]] name = "p256" @@ -5680,7 +5667,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures 0.2.17", + "cpufeatures 0.2.99", "opaque-debug", "universal-hash", ] @@ -5692,7 +5679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures 0.2.99", "opaque-debug", "universal-hash", ] @@ -7579,11 +7566,10 @@ name = "schnorr-signatures" version = "0.5.2" dependencies = [ "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", - "digest 0.11.2", + "dalek-ff-group", "flexible-transcript", "hex", - "multiexp 0.5.0", + "multiexp", "rand_core 0.6.4", "sha2 0.10.9", "std-shims 0.1.5", @@ -7789,7 +7775,7 @@ version = "0.1.0" dependencies = [ "borsh", "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "monero-address", "monero-ed25519", "serai-primitives", @@ -7839,12 +7825,10 @@ dependencies = [ "blake2 0.11.0-rc.5", "borsh", "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "dkg-musig", - "env_logger", "frost-schnorrkel", "hex", - "log", "modular-frost", "rand_core 0.6.4", "schnorrkel", @@ -7923,7 +7907,7 @@ dependencies = [ "blake2 0.11.0-rc.5", "borsh", "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "dkg", "log", "rand_core 0.6.4", @@ -7959,10 +7943,17 @@ version = "0.1.0" dependencies = [ "blake2 0.11.0-rc.5", "borsh", - "log", + "hex", + "rand 0.8.5", + "rand_core 0.6.4", + "schnorrkel", + "serai-abi", "serai-client-serai", "serai-cosign-types", "serai-db", + "serai-env", + "serai-primitives", + "serai-shim-rpc", "serai-task", "tokio", ] @@ -7972,6 +7963,7 @@ name = "serai-cosign-types" version = "0.1.0" dependencies = [ "borsh", + "rand_core 0.6.4", "schnorrkel", "serai-primitives", ] @@ -8049,6 +8041,10 @@ dependencies = [ [[package]] name = "serai-env" version = "0.1.0" +dependencies = [ + "env_logger", + "log", +] [[package]] name = "serai-ethereum-processor" @@ -8069,7 +8065,6 @@ dependencies = [ "ethereum-schnorr-contract", "hex", "k256", - "log", "modular-frost", "rand_core 0.6.4", "serai-client-ethereum", @@ -8094,8 +8089,6 @@ dependencies = [ name = "serai-ethereum-relayer" version = "0.1.0" dependencies = [ - "env_logger", - "log", "serai-db", "serai-env", "tokio", @@ -8120,7 +8113,7 @@ dependencies = [ "bitvec", "borsh", "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "dkg-musig", "frame-benchmarking", "frame-support", @@ -8147,7 +8140,7 @@ dependencies = [ "bitvec", "borsh", "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "frame-benchmarking", "frame-support", "frame-system", @@ -8177,11 +8170,9 @@ version = "0.1.0" dependencies = [ "borsh", "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", - "env_logger", + "dalek-ff-group", "flexible-transcript", "hex", - "log", "rand_core 0.6.4", "schnorr-signatures", "serai-db", @@ -8197,7 +8188,7 @@ name = "serai-message-queue-tests" version = "0.1.0" dependencies = [ "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "dockertest", "hex", "rand_core 0.6.4", @@ -8214,7 +8205,7 @@ version = "0.1.0" dependencies = [ "borsh", "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "dkg-evrf", "log", "modular-frost", @@ -8244,7 +8235,7 @@ version = "0.1.0" dependencies = [ "bitcoin-serai", "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "dkg", "dkg-dealer", "dkg-evrf", @@ -8256,7 +8247,7 @@ dependencies = [ "frost-schnorrkel", "minimal-ed448", "modular-frost", - "multiexp 0.5.0", + "multiexp", "prime-field", "schnorr-signatures", "secq256k1", @@ -8274,14 +8265,13 @@ dependencies = [ "borsh", "ciphersuite 0.4.2", "clap", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "dirs", "embedwards25519", "frame-benchmarking", "futures-util", "hex", "jsonrpsee", - "log", "rand_core 0.6.4", "sc-authority-discovery", "sc-basic-authorship", @@ -8317,6 +8307,7 @@ dependencies = [ "sp-runtime", "sp-state-machine", "sp-timestamp", + "sp-tracing", "tokio", "zeroize", ] @@ -8326,7 +8317,7 @@ name = "serai-orchestrator" version = "0.0.1" dependencies = [ "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "embedwards25519", "flexible-transcript", "hex", @@ -8346,7 +8337,7 @@ dependencies = [ "bitvec", "borsh", "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "dkg-musig", "embedwards25519", "parity-scale-codec", @@ -8365,9 +8356,7 @@ dependencies = [ "borsh", "ciphersuite 0.4.2", "dkg-evrf", - "env_logger", "hex", - "log", "serai-cosign-types", "serai-db", "serai-env", @@ -8675,6 +8664,23 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", ] +[[package]] +name = "serai-shim-rpc" +version = "0.1.0" +dependencies = [ + "blake2 0.11.0-rc.5", + "borsh", + "hex", + "jsonrpsee", + "rand_core 0.6.4", + "schnorrkel", + "serai-abi", + "serai-client-serai", + "serai-primitives", + "serde", + "tokio", +] + [[package]] name = "serai-signals-pallet" version = "0.1.0" @@ -8720,7 +8726,7 @@ dependencies = [ "bitvec", "borsh", "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "embedwards25519", "frame-benchmarking", "frame-support", @@ -8839,7 +8845,7 @@ dependencies = [ "base64", "chrono", "hex", - "indexmap 1.9.3", + "indexmap 1.99.99", "indexmap 2.13.0", "serde", "serde_derive", @@ -8854,7 +8860,7 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ - "darling 0.20.99", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.117", @@ -8877,7 +8883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures 0.2.99", "digest 0.10.7", ] @@ -8888,7 +8894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures 0.2.99", "digest 0.10.7", ] @@ -8915,9 +8921,9 @@ dependencies = [ [[package]] name = "sha3" -version = "0.11.0-rc.9" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b233a7d59d7bfc027208506a33ffc9532b2acb24ddc61fe7e758dc2250db431" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" dependencies = [ "digest 0.11.2", "keccak 0.2.0", @@ -10218,7 +10224,7 @@ dependencies = [ "blake2 0.11.0-rc.5", "borsh", "ciphersuite 0.4.2", - "dalek-ff-group 0.5.0", + "dalek-ff-group", "flexible-transcript", "futures-channel", "futures-util", @@ -11178,3 +11184,11 @@ dependencies = [ "cc", "pkg-config", ] + +[[patch.unused]] +name = "schemars" +version = "0.9.99" + +[[patch.unused]] +name = "schemars" +version = "1.99.99" diff --git a/Cargo.toml b/Cargo.toml index 37315e1ec..4790b2b6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ members = [ "tests/no-std", + "tests/shim-rpc", "tests/docker", "tests/message-queue", # TODO "tests/processor", @@ -188,14 +189,18 @@ primitive-types-12 = { package = "primitive-types", path = "patches/ethereum/pri rapidhash = { path = "patches/ethereum/rapidhash" } rlp = { path = "patches/ethereum/rlp" } secp256k1-30 = { package = "secp256k1", path = "patches/ethereum/secp256k1-0.30" } +indexmap = { path = "patches/indexmap" } +schemars-09 = { package = "schemars", path = "patches/schemars-09" } +schemars-10 = { package = "schemars", path = "patches/schemars-10" } wit-bindgen-rust-macro = { path = "patches/wit-bindgen-rust-macro" } # Dependencies from monero-oxide which originate from within our own tree, potentially shimmed to account for deviations since publishing std-shims = { path = "patches/std-shims" } simple-request = { path = "patches/simple-request" } flexible-transcript = { path = "crypto/transcript" } +multiexp = { path = "crypto/multiexp" } ciphersuite = { path = "patches/ciphersuite" } -dalek-ff-group = { path = "patches/dalek-ff-group" } +dalek-ff-group = { path = "crypto/dalek-ff-group" } minimal-ed448 = { path = "crypto/ed448" } modular-frost = { path = "crypto/frost" } @@ -205,21 +210,26 @@ lazy_static = { path = "patches/lazy_static" } home = { path = "patches/home" } # Updates to the latest version -darling = { path = "patches/darling" } +cpufeatures = { path = "patches/cpufeatures" } dtoa = { path = "patches/dtoa" } getrandom-02 = { package = "getrandom", path = "patches/getrandom-0.2" } getrandom-03 = { package = "getrandom", path = "patches/getrandom-0.3" } ryu = { path = "patches/ryu" } thiserror = { path = "patches/thiserror" } +# `core2` can be replaced with our in-tree `std-shims` +core2 = { path = "patches/core2" } +# `hex_lit` can be replaced with `hex-literal` (from RustCrypto) +hex_lit = { path = "patches/hex_lit" } +# `keccak-asm` is liberal with 'use at your risk', yet API-compatible with `sha3`, so we prefer the latter +keccak-asm = { path = "patches/keccak-asm" } + # `option-ext` is a copyleft library for a single one liner, which we provide ourselves option-ext = { path = "patches/option-ext" } - # This is a trivial third-party crate which over-scopes its dependencies aurora-engine-modexp = { path = "patches/ethereum/aurora-engine-modexp" } - -# `keccak-asm` is liberal with 'use at your risk', yet API-compatible with `sha3`, so we prefer the latter -keccak-asm = { path = "patches/keccak-asm" } +# `nonzero_ext` is more than 300 lines when we only need 10 +nonzero_ext = { path = "patches/nonzero_ext" } # `hickory-resolver` uses `moka`, which is obscene with LLM use and frequently has critical bugs # This is a version patched to use `lru-slab` for its LRU cache instead @@ -231,6 +241,7 @@ k256 = { git = "https://github.com/kayabaNerve/elliptic-curves", rev = "8293be3f p256 = { git = "https://github.com/kayabaNerve/elliptic-curves", rev = "8293be3f9855eea0936523a9b100f1f69261ea4b" } [workspace.lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)'] } # https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html ambiguous_negative_literals = "warn" closure_returning_async_block = "warn" diff --git a/README.md b/README.md index 7ab1eca85..09dbcef8d 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ wallet. - `patches`: Patches for our supply-chain to minimize dependencies and ensure a tighter surface. +- `supply-chain`: `cargo vet` attestations for our supply chain. + ### Links - [Website](https://serai.exchange/): https://serai.exchange/ diff --git a/audits/crypto/dkg/evrf/README.md b/audits/crypto/dkg/evrf/README.md index 601be6b42..acbf434f6 100644 --- a/audits/crypto/dkg/evrf/README.md +++ b/audits/crypto/dkg/evrf/README.md @@ -16,7 +16,7 @@ confirm: - The secret shares sent can be received by the intended recipient so long as they can access the bulletin board -Additionally, Serai desired a robust scheme (albeit with an biased key as the +Additionally, Serai desired a robust scheme (albeit with a biased key as the output, which is fine for our purposes). Accordingly, our implementation instantiates the threshold eVRF DKG from the eVRF paper, with our own proposal for verifiable encryption, with the caller allowed to decide the set of diff --git a/common/env/Cargo.toml b/common/env/Cargo.toml index be34cbac4..d3f1c4073 100644 --- a/common/env/Cargo.toml +++ b/common/env/Cargo.toml @@ -15,3 +15,7 @@ rustdoc-args = ["--cfg", "docsrs"] [lints] workspace = true + +[dependencies] +log = { version = "0.4", default-features = false, features = ["std"] } +env_logger = { version = "0.10", default-features = false, features = ["humantime"] } diff --git a/common/env/src/lib.rs b/common/env/src/lib.rs index d7b2bb333..efb960ad4 100644 --- a/common/env/src/lib.rs +++ b/common/env/src/lib.rs @@ -1,8 +1,78 @@ #![cfg_attr(docsrs, feature(doc_cfg))] +/// Re-export of `log` for direct access (e.g. `serai_env::log::Level`). +pub use log; + // Obtain a variable from the Serai environment/secret store. pub fn var(variable: &str) -> Option { // TODO: Move this to a proper secret store // TODO: Unset this variable std::env::var(variable).ok() } + +pub fn init_logger() { + // TODO: Implement `env_logger::Env` for this library instead of using `env_logger::Env`? + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .try_init() + .unwrap(); +} + +/// Coverage-gated `trace!`. Compiles to nothing under `cfg(coverage)`. +#[cfg(not(coverage))] +#[macro_export] +macro_rules! trace { + ($($arg:tt)+) => { $crate::log::trace!($($arg)+) }; +} +#[cfg(coverage)] +#[macro_export] +macro_rules! trace { + ($($arg:tt)+) => {}; +} + +/// Coverage-gated `debug!`. Compiles to nothing under `cfg(coverage)`. +#[cfg(not(coverage))] +#[macro_export] +macro_rules! debug { + ($($arg:tt)+) => { $crate::log::debug!($($arg)+) }; +} +#[cfg(coverage)] +#[macro_export] +macro_rules! debug { + ($($arg:tt)+) => {}; +} + +/// Coverage-gated `info!`. Compiles to nothing under `cfg(coverage)`. +#[cfg(not(coverage))] +#[macro_export] +macro_rules! info { + ($($arg:tt)+) => { $crate::log::info!($($arg)+) }; +} +#[cfg(coverage)] +#[macro_export] +macro_rules! info { + ($($arg:tt)+) => {}; +} + +/// Coverage-gated `warn!`. Compiles to nothing under `cfg(coverage)`. +#[cfg(not(coverage))] +#[macro_export] +macro_rules! warn { + ($($arg:tt)+) => { $crate::log::warn!($($arg)+) }; +} +#[cfg(coverage)] +#[macro_export] +macro_rules! warn { + ($($arg:tt)+) => {}; +} + +/// Coverage-gated `error!`. Compiles to nothing under `cfg(coverage)`. +#[cfg(not(coverage))] +#[macro_export] +macro_rules! error { + ($($arg:tt)+) => { $crate::log::error!($($arg)+) }; +} +#[cfg(coverage)] +#[macro_export] +macro_rules! error { + ($($arg:tt)+) => {}; +} diff --git a/common/task/Cargo.toml b/common/task/Cargo.toml index fb458761c..863b44582 100644 --- a/common/task/Cargo.toml +++ b/common/task/Cargo.toml @@ -19,3 +19,6 @@ workspace = true [dependencies] log = { version = "0.4", default-features = false, features = ["std"] } tokio = { version = "1", default-features = false, features = ["macros", "sync", "time"] } + +[features] +test-helpers = [] diff --git a/common/task/src/lib.rs b/common/task/src/lib.rs index ca002a8a4..be6022114 100644 --- a/common/task/src/lib.rs +++ b/common/task/src/lib.rs @@ -12,6 +12,10 @@ use tokio::sync::mpsc; mod type_name; +/// Test helpers for asserting [`ContinuallyRan`] task iteration behavior. +#[cfg(any(test, feature = "test-helpers"))] +pub mod test_helpers; + /// A handle for a task. /// /// The task will only stop running once all handles for it are dropped. @@ -106,8 +110,9 @@ pub trait ContinuallyRan: Sized + Send { let mut current_sleep_before_next_task = default_sleep_before_next_task; let increase_sleep_before_next_task = |current_sleep_before_next_task: &mut u64| { let new_sleep = *current_sleep_before_next_task + default_sleep_before_next_task; - // Set a limit of sleeping for two minutes - *current_sleep_before_next_task = new_sleep.max(Self::MAX_DELAY_BETWEEN_ITERATIONS); + // Set a limit of sleeping **at most** two minutes + // use min to get the smallest value: either new_sleep, or 2 minutes. Never greater + *current_sleep_before_next_task = new_sleep.min(Self::MAX_DELAY_BETWEEN_ITERATIONS); }; loop { diff --git a/common/task/src/test_helpers.rs b/common/task/src/test_helpers.rs new file mode 100644 index 000000000..02de57aba --- /dev/null +++ b/common/task/src/test_helpers.rs @@ -0,0 +1,34 @@ +//! Common test utilities for [`ContinuallyRan`] tasks. + +use crate::ContinuallyRan; + +/// Test helpers for asserting task iteration behavior. +pub struct TaskTest; + +impl TaskTest { + /// Assert that a task iteration succeeds and returns the expected progress value. + pub async fn task_runs_once_and_matches_progress( + task: &mut T, + made_progress: bool, + ) { + log::debug!("running task once: {}", core::any::type_name::()); + assert_eq!(task.run_iteration().await.unwrap(), made_progress); + } + + /// Assert that a task iteration fails with an error containing the given string. + // TODO: Replace this with typed errors. + pub async fn task_runs_and_fails_with(task: &mut T, error: &str) { + log::debug!("running task (expecting failure): {}", core::any::type_name::()); + let err = task.run_iteration().await.unwrap_err(); + let err_str = format!("{err:?}"); + assert!(err_str.contains(error), "{err_str}"); + } +} + +/// Trait for test structs that can produce a [`ContinuallyRan`] task. +pub trait IntoTask { + /// The task type produced by this test struct. + type Task: 'static + ContinuallyRan; + /// Create the task from the current test state. + fn task(&self) -> Self::Task; +} diff --git a/coordinator/Cargo.toml b/coordinator/Cargo.toml index 925257860..a495df1fe 100644 --- a/coordinator/Cargo.toml +++ b/coordinator/Cargo.toml @@ -44,9 +44,6 @@ tributary-sdk = { path = "./tributary-sdk" } serai-client-serai = { path = "../substrate/client/serai", default-features = false } -log = { version = "0.4", default-features = false, features = ["std"] } -env_logger = { version = "0.10", default-features = false, features = ["humantime"] } - tokio = { version = "1", default-features = false, features = ["time", "sync", "macros", "rt-multi-thread"] } serai-cosign = { path = "./cosign" } diff --git a/coordinator/cosign/Cargo.toml b/coordinator/cosign/Cargo.toml index 478e81c8f..5e268dadb 100644 --- a/coordinator/cosign/Cargo.toml +++ b/coordinator/cosign/Cargo.toml @@ -22,7 +22,7 @@ blake2 = { version = "0.11.0-rc.5", default-features = false, features = ["alloc borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } serai-client-serai = { path = "../../substrate/client/serai", default-features = false } -log = { version = "0.4", default-features = false, features = ["std"] } +serai-env = { path = "../../common/env", version = "0.1.0" } tokio = { version = "1", default-features = false } @@ -30,3 +30,19 @@ serai-db = { path = "../../common/db", version = "0.1.1" } serai-task = { path = "../../common/task", version = "0.1" } serai-cosign-types = { path = "./types" } + +[dev-dependencies] +hex = { version = "0.4", default-features = false } + +rand_core = { version = "0.6", default-features = false } +rand = { version = "0.8", default-features = false } + +schnorrkel = { version = "0.11", default-features = false, features = ["std"] } + +serai-primitives = { path = "../../substrate/primitives", features = ["test-helpers"] } +serai-abi = { path = "../../substrate/abi", default-features = false, features = ["std"] } + +serai-cosign-types = { path = "./types", features = ["test-helpers"] } +serai-task = { path = "../../common/task", features = ["test-helpers"] } + +serai-shim-rpc = { path = "../../tests/shim-rpc" } diff --git a/coordinator/cosign/src/delay.rs b/coordinator/cosign/src/delay.rs index 1d57d7a39..fdac819ba 100644 --- a/coordinator/cosign/src/delay.rs +++ b/coordinator/cosign/src/delay.rs @@ -6,15 +6,34 @@ use serai_task::{DoesNotError, ContinuallyRan}; use crate::evaluator::CosignedBlocks; +#[expect(clippy::cfg_not_test)] +#[cfg(not(test))] /// How often callers should broadcast the cosigns flagged for rebroadcasting. pub const BROADCAST_FREQUENCY: Duration = Duration::from_mins(1); +#[cfg(test)] +/// How often callers should broadcast the cosigns flagged for rebroadcasting. +pub const BROADCAST_FREQUENCY: Duration = Duration::from_secs(6); + +#[expect(clippy::cfg_not_test)] +#[cfg(not(test))] const SYNCHRONY_EXPECTATION: Duration = Duration::from_secs(10); -const ACKNOWLEDGEMENT_DELAY: Duration = +#[cfg(test)] +const SYNCHRONY_EXPECTATION: Duration = Duration::from_secs(1); + +pub(crate) const ACKNOWLEDGEMENT_DELAY: Duration = Duration::from_secs(BROADCAST_FREQUENCY.as_secs() + SYNCHRONY_EXPECTATION.as_secs()); +pub(crate) fn now_timestamp() -> Duration { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("current time was less than the epoch") +} + create_db!( SubstrateCosignDelay { - // The latest cosigned block number. + // The latest block number marked as cosigned by the delay task. + // Cosigned after a delay if it had events and cosigns, + // simply marked as cosigned if the block had no events and no cosigns. LatestCosignedBlockNumber: () -> u64, } ); @@ -31,20 +50,54 @@ impl ContinuallyRan for CosignDelayTask { async move { let mut made_progress = false; loop { - let mut txn = self.db.txn(); + let latest_cosigned_block_number = LatestCosignedBlockNumber::get(&self.db).unwrap_or(0); - // Receive the next block to mark as cosigned - let Some((block_number, time_evaluated)) = CosignedBlocks::try_recv(&mut txn) else { + let mut txn = self.db.txn(); + let Some((block_number, time_evaluated, has_events)) = CosignedBlocks::try_recv(&mut txn) + else { break; }; + + // Defensive check, not likely to happen but does not allow regressing + if block_number <= latest_cosigned_block_number { + serai_env::warn!( + "attempting to delay #{block_number} when #{} was already cosigned", + latest_cosigned_block_number, + ); + // consume and skip without sleeping. + txn.commit(); + continue; + } + + // No events means no cosigns to wait for, mark as cosigned immediately + if !has_events { + LatestCosignedBlockNumber::set(&mut txn, &block_number); + txn.commit(); + made_progress = true; + continue; + } + // Calculate when we should mark it as valid - let time_valid = - SystemTime::UNIX_EPOCH + Duration::from_secs(time_evaluated) + ACKNOWLEDGEMENT_DELAY; - // Sleep until then - tokio::time::sleep(SystemTime::now().duration_since(time_valid).unwrap_or(Duration::ZERO)) - .await; + let now_timestamp = now_timestamp(); + let time_valid_timestamp = Duration::from_secs(time_evaluated) + ACKNOWLEDGEMENT_DELAY; - // Set the cosigned block + // Drop txn during sleep + drop(txn); + + if let Some(time_left) = time_valid_timestamp.checked_sub(now_timestamp) { + serai_env::debug!( + "delaying consideration of #{block_number} as cosigned for {} seconds", + time_left.as_secs() + ); + tokio::time::sleep(time_left).await; + } + + let mut txn = self.db.txn(); + // Consume block to continue + assert_eq!( + Some((block_number, time_evaluated, has_events)), + CosignedBlocks::try_recv(&mut txn) + ); LatestCosignedBlockNumber::set(&mut txn, &block_number); txn.commit(); diff --git a/coordinator/cosign/src/evaluator.rs b/coordinator/cosign/src/evaluator.rs index 5ea025f20..54cda860d 100644 --- a/coordinator/cosign/src/evaluator.rs +++ b/coordinator/cosign/src/evaluator.rs @@ -1,14 +1,24 @@ use core::future::Future; -use std::time::{Duration, Instant, SystemTime}; +use std::time::{Duration, Instant}; use serai_db::*; use serai_task::ContinuallyRan; use crate::{ - HasEvents, GlobalSession, NetworksLatestCosignedBlock, RequestNotableCosigns, - intend::{GlobalSessionsChannel, BlockEventData, BlockEvents}, + GlobalSession, HasEvents, NetworksLatestCosignedBlock, RequestNotableCosigns, + delay::now_timestamp, + intend::{BlockEventData, BlockEvents, GlobalSessionsChannel}, }; +#[expect(clippy::cfg_not_test)] +#[cfg(not(test))] +pub(crate) const REQUEST_COSIGNS_SPACING: Duration = Duration::from_mins(1); +#[cfg(test)] +pub(crate) const REQUEST_COSIGNS_SPACING: Duration = Duration::from_secs(6); + +const COSIGN_COMMIT_THRESHOLD_NUMERATOR: u128 = 83; +const COSIGN_COMMIT_THRESHOLD_DENOMINATOR: u128 = 100; + create_db!( SubstrateCosignEvaluator { // The global session currently being evaluated. @@ -18,20 +28,29 @@ create_db!( db_channel!( SubstrateCosignEvaluatorChannels { - // (cosigned block, time cosign was evaluated) - CosignedBlocks: () -> (u64, u64), + // (cosigned block, time cosign was evaluated in seconds since the epoch, has_events) + CosignedBlocks: () -> (u64, u64, bool), } ); -// This is a strict function which won't panic, even with a malicious Serai node, so long as: -// - It's called incrementally (with an increment of 1) -// - It's only called for block numbers we've completed indexing on within the intend task -// - It's only called for block numbers after a global session has started -// - The global sessions channel is populated as the block declaring the session is indexed -// Which all hold true within the context of this task and the intend task. -// -// This function will also ensure the currently evaluated global session is incremented once we -// finish evaluation of the prior session. +/// Commit a block as evaluated. +fn commit_evaluated_block(mut txn: impl DbTxn, block_number: u64, has_events: bool) { + CosignedBlocks::send(&mut txn, &(block_number, now_timestamp().as_secs(), has_events)); + txn.commit(); +} + +/// Fetch the currently being-evaluated global session. +/// +/// This is a strict function which won't panic, even with a malicious Serai node, so long as: +/// - It's called incrementally (with an increment of 1) +/// - It's only called for block numbers we've completed indexing on within the intend task +/// - It's only called for block numbers after a global session has started +/// - The global sessions channel is populated as the block declaring the session is indexed +/// +/// which all hold true within the context of this task and the intend task. +/// +/// This function will also ensure the currently evaluated global session is incremented once we +/// finish evaluation of the prior session. fn currently_evaluated_global_session_strict( txn: &mut impl DbTxn, block_number: u64, @@ -41,6 +60,8 @@ fn currently_evaluated_global_session_strict( Some(existing) => existing, None => { let first = GlobalSessionsChannel::try_recv(txn) + // Panic: invariant, this function should only be called if + // the global sessions channel is populated .expect("fetching latest global session yet none declared"); CurrentlyEvaluatedGlobalSession::set(txn, &first); first @@ -48,7 +69,8 @@ fn currently_evaluated_global_session_strict( }; assert!( existing.1.start_block_number <= block_number, - "candidate's start block number exceeds our block number" + "candidate's start block number ({}) exceeds our block number ({block_number})", + existing.1.start_block_number ); existing }; @@ -73,6 +95,99 @@ pub(crate) fn currently_evaluated_global_session(getter: &impl Get) -> Option<[u CurrentlyEvaluatedGlobalSession::get(getter).map(|(id, _info)| id) } +fn should_request_cosigns(last_request_for_cosigns: &mut Instant) -> bool { + if Instant::now() < (*last_request_for_cosigns + REQUEST_COSIGNS_SPACING) { + return false; + } + + *last_request_for_cosigns = Instant::now(); + + true +} + +/// Calculate the minimum threshold required for cosigning +pub(crate) fn cosign_threshold(total_stake: u64) -> u64 { + const { + assert!(COSIGN_COMMIT_THRESHOLD_NUMERATOR < COSIGN_COMMIT_THRESHOLD_DENOMINATOR); + } + u64::try_from( + (u128::from(total_stake) * COSIGN_COMMIT_THRESHOLD_NUMERATOR) / + COSIGN_COMMIT_THRESHOLD_DENOMINATOR, + ) + .expect("threshold < 1") + + 1 +} + +/// Evaluate non-notable cosigns, returning (weight_cosigned, lowest_common_block). +fn evaluate_non_notable_cosigns( + getter: &impl Get, + block_number: u64, + global_session: [u8; 32], + global_session_info: &GlobalSession, +) -> Result<(u64, Option), String> { + /* + LatestCosign is populated with the latest cosigns for each network which don't + exceed the latest global session we've evaluated the start of. This current block + is during the latest global session we've evaluated the start of. + */ + + let mut weight_cosigned = 0; + let mut lowest_common_block: Option = None; + + for set in &global_session_info.sets { + // Check if this set cosigned this block or not + let Some(signed_cosign) = NetworksLatestCosignedBlock::get(getter, global_session, set.network) + else { + continue; + }; + + if signed_cosign.cosign.block_number >= block_number { + weight_cosigned += global_session_info + .stakes + .get(&set.network) + .ok_or_else(|| "ValidatorSet in global session yet didn't have its stake".to_owned())?; + } + + // Update the lowest block common to all of these cosigns + lowest_common_block = lowest_common_block + .map(|existing| existing.min(signed_cosign.cosign.block_number)) + .or(Some(signed_cosign.cosign.block_number)); + } + + Ok((weight_cosigned, lowest_common_block)) +} + +/// If the cosign threshold isn't met, request cosigns and return an error. +async fn ensure_cosigned( + weight_cosigned: u64, + total_stake: u64, + block_number: u64, + global_session: [u8; 32], + last_request_for_cosigns: &mut Instant, + request: &impl RequestNotableCosigns, + label: &str, +) -> Result<(), String> { + if weight_cosigned >= cosign_threshold(total_stake) { + return Ok(()); + } + + /* + Request the superseding notable cosigns over the network. + + If this session hasn't yet produced notable cosigns, then we presume we'll see the desired + non-notable cosigns as part of normal operations, without needing to explicitly request them. + */ + if should_request_cosigns(last_request_for_cosigns) { + request + .request_notable_cosigns(global_session) + .await + .map_err(|e| format!("Error fetching notable cosigns: {e:?}"))?; + } + + // We return an error so the delay before this task is run again increases + Err(format!("{label} block (#{block_number}) wasn't yet cosigned. this should resolve shortly")) +} + /// A task to determine if a block has been cosigned and we should handle it. pub(crate) struct CosignEvaluatorTask { pub(crate) db: D, @@ -81,19 +196,16 @@ pub(crate) struct CosignEvaluatorTask { } impl ContinuallyRan for CosignEvaluatorTask { + #[cfg(test)] + const DELAY_BETWEEN_ITERATIONS: u64 = 1; + #[cfg(test)] + const MAX_DELAY_BETWEEN_ITERATIONS: u64 = 5; + type Error = String; fn run_iteration(&mut self) -> impl Send + Future> { - let should_request_cosigns = |last_request_for_cosigns: &mut Instant| { - const REQUEST_COSIGNS_SPACING: Duration = Duration::from_mins(1); - if Instant::now() < (*last_request_for_cosigns + REQUEST_COSIGNS_SPACING) { - return false; - } - *last_request_for_cosigns = Instant::now(); - true - }; - async move { + let mut prior_global_session = None; let mut known_cosign = None; let mut made_progress = false; loop { @@ -103,9 +215,35 @@ impl ContinuallyRan for CosignEvaluatorTask { + commit_evaluated_block(txn, block_number, false); + made_progress = true; + continue; + } + // Session queued but starts after this block, skip it + Some(next) if next.1.start_block_number > block_number => { + commit_evaluated_block(txn, block_number, false); + made_progress = true; + continue; + } + // Session covers this block: proceed normally + _ => {} + } + } + // Fetch the global session information let (global_session, global_session_info) = currently_evaluated_global_session_strict(&mut txn, block_number); + // If the global session has changed, clear the cached `known_cosign` + if prior_global_session != Some(global_session) { + prior_global_session = Some(global_session); + known_cosign = None; + } match has_events { // Because this had notable events, we require an explicit cosign for this block by a @@ -125,84 +263,42 @@ impl ContinuallyRan for CosignEvaluatorTask { - // Check if this was satisfied by a cached result which wasn't calculated incrementally - let known_cosigned = if let Some(known_cosign) = known_cosign { - known_cosign >= block_number - } else { - // Clear `known_cosign` which is no longer helpful - known_cosign = None; - false - }; - - // If it isn't already known to be cosigned, evaluate the latest cosigns - if !known_cosigned { - /* - LatestCosign is populated with the latest cosigns for each network which don't - exceed the latest global session we've evaluated the start of. This current block - is during the latest global session we've evaluated the start of. - */ + // If not already known to be cosigned by a cached result, evaluate the latest cosigns + if known_cosign < Some(block_number) { + let (weight_cosigned, lowest_common_block) = evaluate_non_notable_cosigns( + &txn, + block_number, + global_session, + &global_session_info, + )?; - let mut weight_cosigned = 0; - let mut lowest_common_block: Option = None; - for set in global_session_info.sets { - // Check if this set cosigned this block or not - let Some(cosign) = - NetworksLatestCosignedBlock::get(&txn, global_session, set.network) - else { - continue; - }; - if cosign.cosign.block_number >= block_number { - weight_cosigned += - global_session_info.stakes.get(&set.network).ok_or_else(|| { - "ValidatorSet in global session yet didn't have its stake".to_owned() - })?; - } - - // Update the lowest block common to all of these cosigns - lowest_common_block = lowest_common_block - .map(|existing| existing.min(cosign.cosign.block_number)) - .or(Some(cosign.cosign.block_number)); - } - - // Check if the sum weight doesn't cross the required threshold - if weight_cosigned < (((global_session_info.total_stake * 83) / 100) + 1) { - // Request the superseding notable cosigns over the network - // If this session hasn't yet produced notable cosigns, then we presume we'll see - // the desired non-notable cosigns as part of normal operations, without needing to - // explicitly request them - if should_request_cosigns(&mut self.last_request_for_cosigns) { - self - .request - .request_notable_cosigns(global_session) - .await - .map_err(|e| format!("{e:?}"))?; - } - // We return an error so the delay before this task is run again increases - return Err(format!( - "block (#{block_number}) wasn't yet cosigned. this should resolve shortly", - )); - } + ensure_cosigned( + weight_cosigned, + global_session_info.total_stake, + block_number, + global_session, + &mut self.last_request_for_cosigns, + &self.request, + "non-notable", + ) + .await?; // Update the cached result for the block we know is cosigned /* @@ -213,7 +309,7 @@ impl ContinuallyRan for CosignEvaluatorTask ContinuallyRan for CosignEvaluatorTask Result<(Block, Events, HasEvents), String> { - let block = serai - .block_by_number(block_number) - .await - .map_err(|e| format!("{e:?}"))? - .ok_or_else(|| "couldn't get block which should've been finalized".to_owned())?; - let events = serai.events(block.header.hash()).await.map_err(|e| format!("{e:?}"))?; - - if events.validator_sets().set_keys_events().next().is_some() { - return Ok((block, events, HasEvents::Notable)); - } - - if events.coins().burn_with_instruction_events().next().is_some() { - return Ok((block, events, HasEvents::NonNotable)); - } - - Ok((block, events, HasEvents::No)) -} - -// Fetch the `ExternalValidatorSet`s, and their associated keys, used for cosigning as of this -// block. +/// Fetch the `ExternalValidatorSet`s, and their associated keys, used for cosigning as of this +/// block. fn cosigning_sets(getter: &impl Get) -> Vec<(ExternalValidatorSet, Public, Amount)> { - let mut sets = vec![]; - for network in ExternalNetworkId::all() { - let Some(Set { session, key, stake }) = LatestSet::get(getter, network) else { - // If this network doesn't have usable keys, move on - continue; - }; - - sets.push((ExternalValidatorSet { network, session }, key, stake)); - } - sets + ExternalNetworkId::all() + .filter_map(|network| { + let Set { session, key, stake } = LatestSet::get(getter, network)?; + Some((ExternalValidatorSet { network, session }, key, stake)) + }) + .collect() } /// A task to determine which blocks we should intend to cosign. @@ -98,70 +72,107 @@ pub(crate) struct CosignIntendTask { } impl ContinuallyRan for CosignIntendTask { + #[cfg(test)] + const DELAY_BETWEEN_ITERATIONS: u64 = 1; + #[cfg(test)] + const MAX_DELAY_BETWEEN_ITERATIONS: u64 = 5; + type Error = String; fn run_iteration(&mut self) -> impl Send + Future> { async move { - let start_block_number = ScanCosignFrom::get(&self.db).unwrap_or(1); - let latest_block_number = - self.serai.latest_finalized_block_number().await.map_err(|e| format!("{e:?}"))?; + let start_scan_block_number = ScanCosignFrom::get(&self.db).unwrap_or(0); + let latest_serai_block_number = self + .serai + .latest_finalized_block_number() + .await + // Ephemeral RPC Err: task to re-run and continue trying + .map_err(|e| format!("RPC error fetching latest finalized block number: {e}"))?; + + let mut made_progress = false; + + for block_number in start_scan_block_number ..= latest_serai_block_number { + let serai_block = self + .serai + .block_by_number(block_number) + .await + // Ephemeral RPC Err: task to re-run and continue trying + .map_err(|e| format!("RPC error fetching block #{block_number}: {e}"))? + // Block returned `None` even though Serai reported as finalized + .unwrap_or_else(|| { + panic!("couldn't get block #{block_number} which should've been finalized") + }); + + let serai_block_hash = serai_block.header.hash(); + let serai_block_events = self + .serai + .events(serai_block_hash) + .await + // Ephemeral RPC Err: task to re-run and continue trying + .map_err(|e| format!("RPC error fetching events for block #{block_number}: {e}"))?; - for block_number in start_block_number ..= latest_block_number { let mut txn = self.db.txn(); - - let (block, events, mut has_events) = - block_has_events_justifying_a_cosign(&self.serai, block_number) - .await - .map_err(|e| format!("{e:?}"))?; - let mut builds_upon = BuildsUpon::get(&txn).unwrap_or(IncrementalUnbalancedMerkleTree::new()); // Check we are indexing a linear chain - if block.header.builds_upon() != - builds_upon.clone().calculate(serai_client_serai::abi::BLOCK_BRANCH_TAG) - { - Err(format!( - "node's block #{block_number} doesn't build upon the block #{} prior indexed", - block_number - 1 - ))?; - } - let block_hash = block.header.hash(); - SubstrateBlockHash::set(&mut txn, block_number, &block_hash); + assert_eq!( + serai_block.header.builds_upon(), + builds_upon.clone().calculate(serai_client_serai::abi::BLOCK_BRANCH_TAG), + "node's block #{block_number} doesn't build upon the block #{} prior indexed", + block_number - 1 + ); + SubstrateBlockHash::set(&mut txn, block_number, &serai_block_hash); builds_upon.append( serai_client_serai::abi::BLOCK_BRANCH_TAG, Blake2b256::new_with_prefix([serai_client_serai::abi::BLOCK_LEAF_TAG]) - .chain_update(block_hash.0) + .chain_update(serai_block_hash.0) .finalize() .into(), ); BuildsUpon::set(&mut txn, &builds_upon); + let mut has_events = HasEvents::No; + let vset_events = serai_block_events.validator_sets(); + // Update the stakes - for event in events.validator_sets().allocation_events() { + for event in vset_events.allocation_events() { let Event::Allocation { validator, network, amount } = event else { - panic!("event from `allocation_events` wasn't `Event::Allocation`") + unreachable!("event from `allocation_events` wasn't `Event::Allocation`") }; - let Ok(network) = ExternalNetworkId::try_from(*network) else { continue }; + let Ok(network) = ExternalNetworkId::try_from(*network) else { + // Not an `ExternalNetworkId` and therefore would be a Serai network allocation + // safe to just skip this allocation event + continue; + }; + let existing = Stakes::get(&txn, network, *validator).unwrap_or(Amount(0)); Stakes::set(&mut txn, network, *validator, &Amount(existing.0 + amount.0)); } - for event in events.validator_sets().deallocation_events() { + for event in vset_events.deallocation_events() { let Event::Deallocation { validator, network, amount, timeline: _ } = event else { - panic!("event from `deallocation_events` wasn't `Event::Deallocation`") + unreachable!("event from `deallocation_events` wasn't `Event::Deallocation`") }; - let Ok(network) = ExternalNetworkId::try_from(*network) else { continue }; + let Ok(network) = ExternalNetworkId::try_from(*network) else { + // Not an `ExternalNetworkId` and therefore would be a Serai network allocation + // safe to skip this deallocation event + continue; + }; + let existing = Stakes::get(&txn, network, *validator).unwrap_or(Amount(0)); Stakes::set(&mut txn, network, *validator, &Amount(existing.0 - amount.0)); } // Handle decided sets - for event in events.validator_sets().set_decided_events() { + for event in vset_events.set_decided_events() { let Event::SetDecided { set, validators } = event else { - panic!("event from `set_decided_events` wasn't `Event::SetDecided`") + unreachable!("event from `set_decided_events` wasn't `Event::SetDecided`") }; let Ok(set) = ExternalValidatorSet::try_from(*set) else { continue }; + + assert!(!validators.is_empty(), "validator set from Event::SetDecided was empty"); + Validators::set( &mut txn, set, @@ -170,21 +181,48 @@ impl ContinuallyRan for CosignIntendTask { } // Handle declarations of the latest set - for event in events.validator_sets().set_keys_events() { + for event in vset_events.set_keys_events() { let Event::SetKeys { set, key_pair } = event else { - panic!("event from `set_keys_events` wasn't `Event::SetKeys`") + unreachable!("event from `set_keys_events` wasn't `Event::SetKeys`") }; - let mut stake = 0; - for validator in - Validators::take(&mut txn, *set).expect("set which wasn't decided set keys") - { - stake += Stakes::get(&txn, set.network, validator).unwrap_or(Amount(0)).0; + + /* + Clear any prior declared set. + + This is needed in case this set isn't saved due to being without stake. In that case, + if a historic set had stake, the historic set would still be regarded as latest despite + being historic. + + TODO: Handle a set retiring when not followed by a new set (currently unreachable). + */ + LatestSet::take(&mut txn, set.network); + + let validators = Validators::take(&mut txn, *set) + // critical panic: + // this is a critical issue and will not be solved after re-tries, + // missing Validators from previous blocks will remain missing until re-indexed + // if encountered halt the process + .expect("set which wasn't decided set keys"); + + let stake: u64 = validators + .iter() + .map(|v| Stakes::get(&txn, set.network, *v).unwrap_or(Amount(0)).0) + .sum(); + + // Sets with 0 stake should be skipped and not considered w.r.t. cosigning + if stake > 0 { + has_events = has_events.max(HasEvents::Notable); + LatestSet::set( + &mut txn, + set.network, + &Set { session: set.session, key: key_pair.0, stake: Amount(stake) }, + ); } - LatestSet::set( - &mut txn, - set.network, - &Set { session: set.session, key: key_pair.0, stake: Amount(stake) }, - ); + } + + // Handle burn with instruction events (makes block non-notable if not already notable) + if serai_block_events.coins().burn_with_instruction_events().next().is_some() { + has_events = has_events.max(HasEvents::NonNotable); } let global_session_for_this_block = LatestGlobalSessionIntended::get(&txn); @@ -192,26 +230,33 @@ impl ContinuallyRan for CosignIntendTask { // If this is notable, it creates a new global session, which we index into the database // now if has_events == HasEvents::Notable { - let sets_and_keys_and_stakes = cosigning_sets(&txn); - let global_session = GlobalSession::id( - sets_and_keys_and_stakes.iter().map(|(set, _key, _stake)| *set).collect(), + let new_sets_and_keys_and_stakes = cosigning_sets(&txn); + assert!( + !new_sets_and_keys_and_stakes.is_empty(), + "notable event yet no sets for cosigning" + ); + let new_global_session = GlobalSession::id( + new_sets_and_keys_and_stakes.iter().map(|(set, _key, _stake)| *set).collect(), ); - let mut sets = Vec::with_capacity(sets_and_keys_and_stakes.len()); - let mut keys = HashMap::with_capacity(sets_and_keys_and_stakes.len()); - let mut stakes = HashMap::with_capacity(sets_and_keys_and_stakes.len()); + let mut sets = Vec::with_capacity(new_sets_and_keys_and_stakes.len()); + let mut keys = HashMap::with_capacity(new_sets_and_keys_and_stakes.len()); + let mut stakes = HashMap::with_capacity(new_sets_and_keys_and_stakes.len()); let mut total_stake = 0; - for (set, key, stake) in sets_and_keys_and_stakes { + for (set, key, stake) in new_sets_and_keys_and_stakes { sets.push(set); keys.insert(set.network, key); + + // This filtering occurs when we populate `LatestSet` + assert!( + stake != Amount(0), + "set without stake was selected for cosigning when stake is required" + ); stakes.insert(set.network, stake.0); total_stake += stake.0; } - if total_stake == 0 { - Err(format!("cosigning sets for block #{block_number} had 0 stake in total"))?; - } - let global_session_info = GlobalSession { + let next_global_session_info = GlobalSession { // This session starts cosigning after this block, as this block must be cosigned by // the existing validators start_block_number: block_number + 1, @@ -220,12 +265,13 @@ impl ContinuallyRan for CosignIntendTask { stakes, total_stake, }; - GlobalSessions::set(&mut txn, global_session, &global_session_info); + + GlobalSessions::set(&mut txn, new_global_session, &next_global_session_info); if let Some(ending_global_session) = global_session_for_this_block { GlobalSessionsLastBlock::set(&mut txn, ending_global_session, &block_number); } - LatestGlobalSessionIntended::set(&mut txn, &global_session); - GlobalSessionsChannel::send(&mut txn, &(global_session, global_session_info)); + LatestGlobalSessionIntended::set(&mut txn, &new_global_session); + GlobalSessionsChannel::send(&mut txn, &(new_global_session, next_global_session_info)); } // If there isn't anyone available to cosign this block, meaning it'll never be cosigned, @@ -238,20 +284,25 @@ impl ContinuallyRan for CosignIntendTask { match has_events { HasEvents::Notable | HasEvents::NonNotable => { let global_session_for_this_block = global_session_for_this_block + // panic: invariant, this is checked above .expect("global session for this block was None but still attempting to cosign it"); - let global_session_info = GlobalSessions::get(&txn, global_session_for_this_block) - .expect("last global session intended wasn't saved to the database"); + + let global_session_for_this_block_info = + GlobalSessions::get(&txn, global_session_for_this_block) + // panic: invariant, this has to exist by this point + .expect("last global session intended wasn't saved to the database"); // Tell each set of their expectation to cosign this block - for set in global_session_info.sets { - log::debug!("{set:?} will be cosigning block #{block_number}"); + for set in global_session_for_this_block_info.sets { + serai_env::info!("{set:?} will cosign block #{block_number} ({has_events:?})"); + IntendedCosigns::send( &mut txn, set, &CosignIntent { global_session: global_session_for_this_block, block_number, - block_hash, + block_hash: serai_block_hash, notable: has_events == HasEvents::Notable, }, ); @@ -260,14 +311,16 @@ impl ContinuallyRan for CosignIntendTask { HasEvents::No => {} } - // Populate a singular feed with every block's status for the evluator to work off of + // Populate a singular feed with every block's status for the evaluator to work off of BlockEvents::send(&mut txn, &(BlockEventData { block_number, has_events })); // Mark this block as handled, meaning we should scan from the next block moving on ScanCosignFrom::set(&mut txn, &(block_number + 1)); + // Commit for every block that did progress, on failure restarts from the next block txn.commit(); + made_progress = true; } - Ok(start_block_number <= latest_block_number) + Ok(made_progress) } } } diff --git a/coordinator/cosign/src/lib.rs b/coordinator/cosign/src/lib.rs index 16d1c5d48..78e2402d9 100644 --- a/coordinator/cosign/src/lib.rs +++ b/coordinator/cosign/src/lib.rs @@ -11,12 +11,8 @@ use blake2::{Digest as _, Blake2s256}; use borsh::{BorshSerialize, BorshDeserialize}; use serai_client_serai::{ - abi::{ - primitives::{ - BlockHash, crypto::Public, network_id::ExternalNetworkId, - validator_sets::ExternalValidatorSet, - }, - Block, + abi::primitives::{ + BlockHash, crypto::Public, network_id::ExternalNetworkId, validator_sets::ExternalValidatorSet, }, Serai, }; @@ -35,6 +31,10 @@ mod delay; pub use delay::BROADCAST_FREQUENCY; use delay::LatestCosignedBlockNumber; +/// Test helpers and fixtures. +#[cfg(test)] +pub mod tests; + /// A 'global session', defined as all validator sets used for cosigning at a given moment. /// /// We evaluate cosign faults within a global session. This ensures even if cosigners cosign @@ -62,7 +62,7 @@ pub(crate) struct GlobalSession { pub(crate) total_stake: u64, } impl GlobalSession { - fn id(mut cosigners: Vec) -> [u8; 32] { + pub(crate) fn id(mut cosigners: Vec) -> [u8; 32] { cosigners.sort_by_key(|a| borsh::to_vec(a).unwrap()); Blake2s256::digest(borsh::to_vec(&cosigners).unwrap()).into() } @@ -82,6 +82,50 @@ enum HasEvents { No, } +mod has_events_ord { + use core::cmp::Ordering; + use super::HasEvents; + + impl PartialOrd for HasEvents { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + impl Ord for HasEvents { + /// A `cmp` based on the significance of the values. + /// + /// This is intended to allow using `a.max(b)` in order for the more-significant `HasEvents` to + /// resolve as the final `HasEvents`. + fn cmp(&self, other: &Self) -> Ordering { + #[allow(clippy::match_same_arms)] + match (self, other) { + (HasEvents::Notable, HasEvents::Notable) | + (HasEvents::NonNotable, HasEvents::NonNotable) | + (HasEvents::No, HasEvents::No) => Ordering::Equal, + (HasEvents::No, HasEvents::Notable | HasEvents::NonNotable) => Ordering::Less, + (HasEvents::Notable | HasEvents::NonNotable, HasEvents::No) => Ordering::Greater, + (HasEvents::NonNotable, HasEvents::Notable) => Ordering::Less, + (HasEvents::Notable, HasEvents::NonNotable) => Ordering::Greater, + } + } + } + + #[test] + fn has_events_ord() { + assert_eq!(HasEvents::Notable.cmp(&HasEvents::Notable), Ordering::Equal); + assert_eq!(HasEvents::NonNotable.cmp(&HasEvents::NonNotable), Ordering::Equal); + assert_eq!(HasEvents::No.cmp(&HasEvents::No), Ordering::Equal); + + assert!(HasEvents::No < HasEvents::NonNotable); + assert!(HasEvents::No < HasEvents::Notable); + assert!(HasEvents::NonNotable < HasEvents::Notable); + + assert!(HasEvents::Notable > HasEvents::NonNotable); + assert!(HasEvents::Notable > HasEvents::No); + assert!(HasEvents::NonNotable > HasEvents::No); + } +} + create_db! { Cosign { // The following are populated by the intend task and used throughout the library @@ -120,7 +164,7 @@ create_db! { } /// An object usable to request notable cosigns for a block. -pub trait RequestNotableCosigns: 'static + Send { +pub trait RequestNotableCosigns: 'static + Send + Sync { /// The error type which may be encountered when requesting notable cosigns. type Error: Debug; @@ -177,6 +221,14 @@ pub struct Cosigning { db: D, } impl Cosigning { + #[cfg(test)] + /// Create a cosigning handle using an already-initialized database. + /// + /// This does not spawn any background tasks; use `Cosigning::spawn` for the full service. + pub fn new(db: D) -> Self { + Self { db } + } + /// Spawn the tasks to intend and evaluate cosigns. /// /// The database specified must only be used with a singular instance of the Serai network, and @@ -185,9 +237,13 @@ impl Cosigning { db: D, serai: Arc, request: R, - tasks_to_run_upon_cosigning: Vec, + tasks_to_run_upon_cosigning_blocks: Vec, ) -> Self { - let (intend_task, _intend_task_handle) = Task::new(); + let (intend_task, intend_task_handle) = Task::new(); + // Forget the intend task handle, as dropping the handle would stop the task + // keeps all cosign tasks running in the background + core::mem::forget(intend_task_handle); + let (evaluator_task, evaluator_task_handle) = Task::new(); let (delay_task, delay_task_handle) = Task::new(); tokio::spawn( @@ -204,18 +260,19 @@ impl Cosigning { ); tokio::spawn( (delay::CosignDelayTask { db: db.clone() }) - .continually_run(delay_task, tasks_to_run_upon_cosigning), + .continually_run(delay_task, tasks_to_run_upon_cosigning_blocks), ); + Self { db } } /// The latest cosigned block number. - pub fn latest_cosigned_block_number(getter: &impl Get) -> Result { + pub fn latest_cosigned_block_number(getter: &impl Get) -> Result, Faulted> { if FaultedSession::get(getter).is_some() { Err(Faulted)?; } - Ok(LatestCosignedBlockNumber::get(getter).unwrap_or(0)) + Ok(LatestCosignedBlockNumber::get(getter)) } /// Fetch a cosigned Substrate block's hash by its block number. @@ -223,12 +280,16 @@ impl Cosigning { getter: &impl Get, block_number: u64, ) -> Result, Faulted> { - if block_number > Self::latest_cosigned_block_number(getter)? { + let Some(latest) = Self::latest_cosigned_block_number(getter)? else { + return Ok(None); + }; + if block_number > latest { return Ok(None); } Ok(Some( - SubstrateBlockHash::get(getter, block_number).expect("cosigned block but didn't index it"), + SubstrateBlockHash::get(getter, block_number) + .unwrap_or_else(|| panic!("cosigned block {block_number} but didn't index it")), )) } @@ -236,44 +297,35 @@ impl Cosigning { /// /// If this global session hasn't produced any notable cosigns, this will return the latest /// cosigns for this session. - pub fn notable_cosigns(getter: &impl Get, global_session: [u8; 32]) -> Vec { - let mut cosigns = vec![]; - for network in ExternalNetworkId::all() { - if let Some(cosign) = NetworksLatestCosignedBlock::get(getter, global_session, network) { - cosigns.push(cosign); - } - } - cosigns + pub fn notable_or_latest_cosigns( + getter: &impl Get, + global_session: [u8; 32], + ) -> Vec { + ExternalNetworkId::all() + .filter_map(|network| NetworksLatestCosignedBlock::get(getter, global_session, network)) + .collect() } /// The cosigns to rebroadcast every `BROADCAST_FREQUENCY` seconds. /// - /// This will be the most recent cosigns, in case the initial broadcast failed, or the faulty - /// cosigns, in case of a fault, to induce identification of the fault by others. + /// This will be the most recent cosigns in case the initial broadcast failed, or the faulty + /// cosigns in case of a fault, in order to induce identification of the fault by others. pub fn cosigns_to_rebroadcast(&self) -> Vec { if let Some(faulted) = FaultedSession::get(&self.db) { let mut cosigns = Faults::get(&self.db, faulted).expect("faulted with no faults"); // Also include all of our recognized-as-honest cosigns in an attempt to induce fault // identification in those who see the faulty cosigns as honest - for network in ExternalNetworkId::all() { - if let Some(cosign) = NetworksLatestCosignedBlock::get(&self.db, faulted, network) { - if cosign.cosign.global_session == faulted { - cosigns.push(cosign); - } - } - } + cosigns.extend( + Self::notable_or_latest_cosigns(&self.db, faulted) + .into_iter() + .filter(|c| c.cosign.global_session == faulted), + ); cosigns } else { let Some(global_session) = evaluator::currently_evaluated_global_session(&self.db) else { return vec![]; }; - let mut cosigns = vec![]; - for network in ExternalNetworkId::all() { - if let Some(cosign) = NetworksLatestCosignedBlock::get(&self.db, global_session, network) { - cosigns.push(cosign); - } - } - cosigns + Self::notable_or_latest_cosigns(&self.db, global_session) } } @@ -285,10 +337,10 @@ impl Cosigning { let network = cosign.cosigner; // Check our indexed blockchain includes a block with this block number - let Some(our_block_hash) = SubstrateBlockHash::get(&self.db, cosign.block_number) else { + let Some(indexed_block_hash) = SubstrateBlockHash::get(&self.db, cosign.block_number) else { Err(IntakeCosignError::NotYetIndexedBlock)? }; - let faulty = cosign.block_hash != our_block_hash; + let faulty = cosign.block_hash != indexed_block_hash; // Check this isn't a dated cosign within its global session (as it would be if rebroadcasted) if !faulty { @@ -341,8 +393,7 @@ impl Cosigning { // This global session starts the block *after* its declaration, so we want to check if the // block declaring it was cosigned if (global_session.start_block_number - 1) > latest_cosigned_block_number { - drop(txn); - return Err(IntakeCosignError::FutureGlobalSession); + Err(IntakeCosignError::FutureGlobalSession)?; } // This is safe as it's in-range and newer, as prior checked since it isn't faulty diff --git a/coordinator/cosign/src/tests/cosigning.rs b/coordinator/cosign/src/tests/cosigning.rs new file mode 100644 index 000000000..7b5c5923b --- /dev/null +++ b/coordinator/cosign/src/tests/cosigning.rs @@ -0,0 +1,953 @@ +use crate::{intend::*, evaluator::*, delay::*, tests::*, *}; + +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] +struct TestGlobalSession { + start_block_number: u64, + sets: Vec, + keys: HashMap, + stakes: HashMap, + total_stake: u64, +} + +impl TestGlobalSession { + fn id(&self) -> [u8; 32] { + GlobalSession::id(self.sets.clone()) + } + + fn to_global_session(&self) -> GlobalSession { + GlobalSession { + start_block_number: self.start_block_number, + sets: self.sets.clone(), + keys: self.keys.clone(), + stakes: self.stakes.clone(), + total_stake: self.total_stake, + } + } +} + +fn random_test_session() -> (TestGlobalSession, schnorrkel::Keypair) { + let set = default_test_validator_set(); + let (keypair, public) = random_keypair(&mut OsRng); + let stake = OsRng.gen_range(1u64 .. u64::MAX / 17); + let global_session = build_global_session(set, public, stake, u64::from(set.session.0) + 1); + + let session = TestGlobalSession { + start_block_number: global_session.start_block_number, + sets: global_session.sets, + keys: global_session.keys, + stakes: global_session.stakes, + total_stake: global_session.total_stake, + }; + (session, keypair) +} + +fn seed_minimal_state(db: &mut MemDb, random_test_session: &TestGlobalSession) { + let mut txn = db.txn(); + let id = random_test_session.id(); + + // Required by `Cosigning::intake_cosign`. + GlobalSessions::set(&mut txn, id, &random_test_session.to_global_session()); + + // Required by `Cosigning::cosigns_to_rebroadcast` in the non-faulted case. + CurrentlyEvaluatedGlobalSession::set(&mut txn, &(id, random_test_session.to_global_session())); + + // Required for `intake_cosign` to not classify a session as "future". + LatestCosignedBlockNumber::set(&mut txn, &0u64); + + txn.commit(); +} + +#[test] +fn fuzz_global_session_id() { + for _ in 0 .. 100 { + let num_sets = OsRng.gen_range(1u8 ..= 3); + let sets: Vec<_> = (0 .. num_sets).map(|_| random_validator_set(&mut OsRng)).collect(); + + let id1 = GlobalSession::id(sets.clone()); + let id2 = GlobalSession::id(sets.clone()); + + // Determinism: same input always produces same ID + assert_eq!(id1, id2); + + // Order-independence: any permutation produces the same ID + { + let mut shuffled = sets.clone(); + shuffled.shuffle(&mut OsRng); + assert_eq!(id1, GlobalSession::id(shuffled)); + } + + // Collision resistance: changing any set should change the ID + { + let mut altered = sets.clone(); + while altered[0] == sets[0] { + altered[0] = random_validator_set(&mut OsRng); + } + assert_ne!(id1, GlobalSession::id(altered)); + } + } +} + +mod intake_cosign_error { + use super::*; + + #[test] + fn temporal_returns_true_for_temporal_errors() { + assert!(IntakeCosignError::NotYetIndexedBlock.temporal()); + assert!(IntakeCosignError::StaleCosign.temporal()); + assert!(IntakeCosignError::UnrecognizedGlobalSession.temporal()); + assert!(IntakeCosignError::FutureGlobalSession.temporal()); + } + + #[test] + fn temporal_returns_false_for_non_temporal_errors() { + assert!(!IntakeCosignError::BeforeGlobalSessionStart.temporal()); + assert!(!IntakeCosignError::AfterGlobalSessionEnd.temporal()); + assert!(!IntakeCosignError::NonParticipatingNetwork.temporal()); + assert!(!IntakeCosignError::InvalidSignature.temporal()); + } +} + +// More cases are tested in `tests/full_stack.rs` with fuzzing for different event type blocks +#[tokio::test] +async fn spawn_end_to_end() { + let db = MemDb::new(); + let (shim_serai, serai) = setup_shim_serai().await; + let (request, _calls) = TestRequest::new(false); + + /// Create a trivial task that logs and sets a flag when triggered whose handle is passed to the + /// cosigning pipeline. + struct LogOnTrigger(Arc); + impl ContinuallyRan for LogOnTrigger { + type Error = std::convert::Infallible; + fn run_iteration( + &mut self, + ) -> impl Send + std::future::Future> { + async { + serai_env::info!("dependent task triggered by cosigning pipeline"); + self.0.store(true, Ordering::SeqCst); + Ok(false) + } + } + } + let (dependent_task, dependent_handle) = Task::new(); + let triggered = Arc::new(AtomicBool::new(false)); + tokio::spawn(LogOnTrigger(triggered.clone()).continually_run(dependent_task, vec![])); + + // Spawn cosigning tasks with the dependent task handle + let cosigning = Cosigning::spawn(db.clone(), serai, request, vec![dependent_handle]); + + // Just started: results are empty + { + assert!(cosigning.cosigns_to_rebroadcast().is_empty()); + let latest = Cosigning::::latest_cosigned_block_number(&db); + assert_eq!(latest.unwrap(), None); + } + + // Run block production and pipeline polling concurrently + let total_blocks = 10; + tokio::join!( + // Produce blocks with no events (passes all tasks and is marked as cosigned at the end) + async { + for _ in 0 ..= total_blocks { + shim_serai.add_block_with_events(vec![]).await; + tokio::time::sleep(Duration::from_millis(50)).await; + } + }, + // Poll until the pipeline has processed all blocks + async { + loop { + let latest = Cosigning::::latest_cosigned_block_number(&db); + if latest.ok().flatten().is_some_and(|n| n >= total_blocks) { + break; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + ); + + let latest = Cosigning::::latest_cosigned_block_number(&db).unwrap(); + assert_eq!(latest, Some(total_blocks)); + + // Verify the dependent task was triggered by the cosign pipeline + assert!(triggered.load(Ordering::SeqCst)); +} + +#[test] +fn latest_finalized_block() { + // Defaults to zero + { + let db = MemDb::new(); + assert_eq!(Cosigning::::latest_cosigned_block_number(&db).unwrap(), None); + } + + // Errors when faulted session exists + { + let mut db = MemDb::new(); + { + let mut txn = db.txn(); + FaultedSession::set(&mut txn, &random_global_session(&mut OsRng)); + txn.commit(); + } + assert!(matches!(Cosigning::::latest_cosigned_block_number(&db), Err(Faulted))); + } + + // Returns stored value + { + let mut db = MemDb::new(); + let latest_finalized_block = OsRng.next_u64(); + { + let mut txn = db.txn(); + LatestCosignedBlockNumber::set(&mut txn, &latest_finalized_block); + txn.commit(); + } + assert_eq!( + Cosigning::::latest_cosigned_block_number(&db).unwrap(), + Some(latest_finalized_block) + ); + } +} + +#[test] +fn cosigned_block() { + // Returns None beyond latest finalized block + { + let mut db = MemDb::new(); + assert_eq!(Cosigning::::cosigned_block(&db, 0).unwrap(), None); + + let latest_finalized_block = OsRng.next_u64(); + { + let mut txn = db.txn(); + LatestCosignedBlockNumber::set(&mut txn, &latest_finalized_block); + txn.commit(); + } + assert_eq!(Cosigning::::cosigned_block(&db, latest_finalized_block + 1).unwrap(), None); + } + + // Returns hash when block is in range + { + let mut db = MemDb::new(); + let latest_finalized_block = OsRng.next_u64(); + let block_hash = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + LatestCosignedBlockNumber::set(&mut txn, &latest_finalized_block); + SubstrateBlockHash::set(&mut txn, latest_finalized_block - 1, &block_hash); + txn.commit(); + } + assert_eq!( + Cosigning::::cosigned_block(&db, latest_finalized_block - 1).unwrap(), + Some(block_hash) + ); + } + + // Errors when faulted session exists + { + let mut db = MemDb::new(); + { + let mut txn = db.txn(); + FaultedSession::set(&mut txn, &random_global_session(&mut OsRng)); + txn.commit(); + } + assert!(matches!(Cosigning::::cosigned_block(&db, OsRng.next_u64()), Err(Faulted))); + } +} + +#[test] +fn notable_cosigns() { + // Empty without cosigns + { + let db = MemDb::new(); + let cosigns = + Cosigning::::notable_or_latest_cosigns(&db, random_global_session(&mut OsRng)); + assert!(cosigns.is_empty()); + } + + // Returns cosigns for session + { + let (session, keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_number = OsRng.next_u64(); + let block_hash = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number, &block_hash); + txn.commit(); + } + + let cosign = Cosign { global_session: id, block_number, block_hash, cosigner: network }; + let signed = sign_cosign(cosign, &keypair); + + let mut cosigning = Cosigning::new(db.clone()); + cosigning.intake_cosign(&signed).unwrap(); + + let notable = Cosigning::::notable_or_latest_cosigns(&db, id); + assert_eq!(notable.len(), 1); + + let SignedCosign { cosign, .. } = ¬able[0]; + let Cosign { + global_session, + block_number: cosign_block_number, + block_hash: cosign_block_hash, + cosigner, + } = cosign; + assert_eq!(global_session, &id); + assert_eq!(cosign_block_number, &block_number); + assert_eq!(cosign_block_hash, &block_hash); + assert_eq!(cosigner, &network); + } +} + +#[test] +fn cosigns_to_rebroadcast() { + // Excludes cosigns from different global session + { + let (session, keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_number = OsRng.next_u64(); + let our_hash = random_block_hash(&mut OsRng); + let faulty_hash = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number, &our_hash); + txn.commit(); + } + + let faulty_cosign = + Cosign { global_session: id, block_number, block_hash: faulty_hash, cosigner: network }; + let faulty_signed = sign_cosign(faulty_cosign, &keypair); + + let mut cosigning = Cosigning::new(db.clone()); + cosigning.intake_cosign(&faulty_signed).unwrap(); + + let different_session_id = random_global_session(&mut OsRng); + let different_cosign = Cosign { + global_session: different_session_id, + block_number, + block_hash: our_hash, + cosigner: network, + }; + let different_signed = sign_cosign(different_cosign, &keypair); + { + let mut txn = db.txn(); + NetworksLatestCosignedBlock::set(&mut txn, id, network, &different_signed); + txn.commit(); + } + + let cosigning = Cosigning::new(db); + let rebroadcast = cosigning.cosigns_to_rebroadcast(); + + assert_eq!(rebroadcast.len(), 1,); + assert_eq!(rebroadcast[0].cosign.block_hash, faulty_hash); + assert_eq!(rebroadcast[0].cosign.global_session, id); + } + + // Returns latest cosigns when not faulted + { + let (session, keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_number = OsRng.next_u64(); + let block_hash = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number, &block_hash); + txn.commit(); + } + + let cosign = Cosign { global_session: id, block_number, block_hash, cosigner: network }; + let signed = sign_cosign(cosign, &keypair); + + let mut cosigning = Cosigning::new(db.clone()); + cosigning.intake_cosign(&signed).unwrap(); + + let rebroadcast = cosigning.cosigns_to_rebroadcast(); + assert_eq!(rebroadcast.len(), 1); + assert_eq!(rebroadcast[0].cosign.block_number, block_number); + assert_eq!(rebroadcast[0].cosign.block_hash, block_hash); + } + + // Returns faults and honest cosigns when faulted + { + let (session, keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_number = OsRng.next_u64(); + let our_hash = random_block_hash(&mut OsRng); + let faulty_hash = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number, &our_hash); + txn.commit(); + } + + let faulty_cosign = + Cosign { global_session: id, block_number, block_hash: faulty_hash, cosigner: network }; + let faulty_signed = sign_cosign(faulty_cosign, &keypair); + + let mut cosigning = Cosigning::new(db.clone()); + cosigning.intake_cosign(&faulty_signed).unwrap(); + + let honest_cosign = + Cosign { global_session: id, block_number, block_hash: our_hash, cosigner: network }; + let honest_signed = sign_cosign(honest_cosign, &keypair); + { + let mut txn = db.txn(); + NetworksLatestCosignedBlock::set(&mut txn, id, network, &honest_signed); + txn.commit(); + } + + let cosigning = Cosigning::new(db); + let rebroadcast = cosigning.cosigns_to_rebroadcast(); + + assert!(rebroadcast.iter().any(|c| c.cosign.block_hash == faulty_hash)); + assert!(rebroadcast.iter().any(|c| c.cosign.block_hash == our_hash)); + } +} + +mod intake_cosign { + use super::*; + + mod errors { + use super::*; + + #[test] + fn rejects_not_yet_indexed_block() { + let db = MemDb::new(); + let (keypair, _) = random_keypair(&mut OsRng); + + let signed = sign_cosign(random_cosign(&mut OsRng), &keypair); + + let mut cosigning = Cosigning::new(db); + assert!(matches!( + cosigning.intake_cosign(&signed), + Err(IntakeCosignError::NotYetIndexedBlock) + )); + } + + #[test] + fn rejects_stale_cosign() { + let (session, keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let base_block = OsRng.next_u64() / 2; + let block_hash_1 = random_block_hash(&mut OsRng); + let block_hash_2 = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, base_block, &block_hash_1); + SubstrateBlockHash::set(&mut txn, base_block + 1, &block_hash_2); + txn.commit(); + } + + let first_cosign = Cosign { + global_session: id, + block_number: base_block + 1, + block_hash: block_hash_2, + cosigner: network, + }; + let first_signed = sign_cosign(first_cosign, &keypair); + + let mut cosigning = Cosigning::new(db.clone()); + cosigning.intake_cosign(&first_signed).unwrap(); + + let stale_cosign = Cosign { + global_session: id, + block_number: base_block, + block_hash: block_hash_1, + cosigner: network, + }; + let stale_signed = sign_cosign(stale_cosign, &keypair); + + assert!(matches!( + cosigning.intake_cosign(&stale_signed), + Err(IntakeCosignError::StaleCosign) + )); + } + + #[test] + fn rejects_unrecognized_global_session() { + let (keypair, _) = random_keypair(&mut OsRng); + + let mut db = MemDb::new(); + let block_number = OsRng.next_u64(); + let block_hash = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number, &block_hash); + txn.commit(); + } + + let cosign = Cosign { + global_session: random_global_session(&mut OsRng), + block_number, + block_hash, + cosigner: random_validator_set(&mut OsRng).network, + }; + let signed = sign_cosign(cosign, &keypair); + + let mut cosigning = Cosigning::new(db); + assert!(matches!( + cosigning.intake_cosign(&signed), + Err(IntakeCosignError::UnrecognizedGlobalSession) + )); + } + + #[test] + fn rejects_before_global_session_start() { + let (mut session, keypair) = random_test_session(); + let network = session.sets[0].network; + session.start_block_number = OsRng.next_u64(); + let id = session.id(); + + let block_hash = random_block_hash(&mut OsRng); + let mut db = MemDb::new(); + { + let mut txn = db.txn(); + GlobalSessions::set(&mut txn, id, &session.to_global_session()); + CurrentlyEvaluatedGlobalSession::set(&mut txn, &(id, session.to_global_session())); + LatestCosignedBlockNumber::set(&mut txn, &session.start_block_number); + SubstrateBlockHash::set(&mut txn, session.start_block_number - 1, &block_hash); + txn.commit(); + } + + let cosign = Cosign { + global_session: id, + block_number: session.start_block_number - 1, + block_hash, + cosigner: network, + }; + let signed = sign_cosign(cosign, &keypair); + + let mut cosigning = Cosigning::new(db); + assert!(matches!( + cosigning.intake_cosign(&signed), + Err(IntakeCosignError::BeforeGlobalSessionStart) + )); + } + + #[test] + fn rejects_after_global_session_end() { + let (session, keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_hash = random_block_hash(&mut OsRng); + let block_number = OsRng.next_u64(); + { + let mut txn = db.txn(); + GlobalSessionsLastBlock::set(&mut txn, id, &(block_number - 1)); + SubstrateBlockHash::set(&mut txn, block_number, &block_hash); + txn.commit(); + } + + let cosign = Cosign { global_session: id, block_number, block_hash, cosigner: network }; + let signed = sign_cosign(cosign, &keypair); + + let mut cosigning = Cosigning::new(db); + assert!(matches!( + cosigning.intake_cosign(&signed), + Err(IntakeCosignError::AfterGlobalSessionEnd) + )); + } + + #[test] + fn rejects_invalid_signature() { + let (session, _keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + let (wrong_keypair, _) = random_keypair(&mut OsRng); + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_number = OsRng.next_u64(); + let block_hash = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number, &block_hash); + txn.commit(); + } + + let cosign = Cosign { global_session: id, block_number, block_hash, cosigner: network }; + let signed = sign_cosign(cosign, &wrong_keypair); + + let mut cosigning = Cosigning::new(db); + assert!(matches!(cosigning.intake_cosign(&signed), Err(IntakeCosignError::InvalidSignature))); + } + + #[test] + fn rejects_future_global_session() { + let (mut session, keypair) = random_test_session(); + let network = session.sets[0].network; + session.start_block_number = OsRng.next_u64(); + let id = session.id(); + + let block_hash = random_block_hash(&mut OsRng); + let mut db = MemDb::new(); + { + let mut txn = db.txn(); + GlobalSessions::set(&mut txn, id, &session.to_global_session()); + CurrentlyEvaluatedGlobalSession::set(&mut txn, &(id, session.to_global_session())); + LatestCosignedBlockNumber::set(&mut txn, &(session.start_block_number - 2)); + SubstrateBlockHash::set(&mut txn, session.start_block_number, &block_hash); + txn.commit(); + } + + let cosign = Cosign { + global_session: id, + block_number: session.start_block_number, + block_hash, + cosigner: network, + }; + let signed = sign_cosign(cosign, &keypair); + + let mut cosigning = Cosigning::new(db); + assert!(matches!( + cosigning.intake_cosign(&signed), + Err(IntakeCosignError::FutureGlobalSession) + )); + } + + #[test] + fn rejects_non_participating_network() { + let (session, _keypair) = random_test_session(); + let id = session.id(); + let session_network = session.sets[0].network; + + let non_participating = ExternalNetworkId::all().find(|n| *n != session_network).unwrap(); + let (other_keypair, _) = random_keypair(&mut OsRng); + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_number = OsRng.next_u64(); + let block_hash = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number, &block_hash); + txn.commit(); + } + + let cosign = + Cosign { global_session: id, block_number, block_hash, cosigner: non_participating }; + let signed = sign_cosign(cosign, &other_keypair); + + let mut cosigning = Cosigning::new(db); + assert!(matches!( + cosigning.intake_cosign(&signed), + Err(IntakeCosignError::NonParticipatingNetwork) + )); + } + } + + #[test] + fn accepts_valid_cosign() { + let (session, keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_number = OsRng.next_u64(); + let block_hash = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number, &block_hash); + txn.commit(); + } + + let cosign = Cosign { global_session: id, block_number, block_hash, cosigner: network }; + let signed = sign_cosign(cosign, &keypair); + + let mut cosigning = Cosigning::new(db); + cosigning.intake_cosign(&signed).unwrap(); + } + + #[test] + fn handles_faulty_cosign() { + let (session, keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_number = OsRng.next_u64(); + let our_hash = random_block_hash(&mut OsRng); + let faulty_hash = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number, &our_hash); + txn.commit(); + } + + let cosign = + Cosign { global_session: id, block_number, block_hash: faulty_hash, cosigner: network }; + let signed = sign_cosign(cosign, &keypair); + + let mut cosigning = Cosigning::new(db.clone()); + cosigning.intake_cosign(&signed).unwrap(); + + let faults: Option> = Faults::get(&db, id); + assert!(faults.is_some()); + assert_eq!(faults.as_ref().unwrap().len(), 1); + assert_eq!(faults.unwrap()[0].cosign.block_hash, faulty_hash); + + let faulted: Option<[u8; 32]> = FaultedSession::get(&db); + assert_eq!(faulted, Some(id)); + } + + #[test] + fn accepts_newer_cosign_when_existing_is_older() { + let (session, keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_number1 = OsRng.next_u64(); + let block_hash_1 = random_block_hash(&mut OsRng); + let block_number2 = block_number1 + 1; + let block_hash_2 = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number1, &block_hash_1); + SubstrateBlockHash::set(&mut txn, block_number2, &block_hash_2); + txn.commit(); + } + + let first_cosign = Cosign { + global_session: id, + block_number: block_number1, + block_hash: block_hash_1, + cosigner: network, + }; + let first_signed = sign_cosign(first_cosign, &keypair); + + let mut cosigning = Cosigning::new(db.clone()); + cosigning.intake_cosign(&first_signed).unwrap(); + + let newer_cosign = Cosign { + global_session: id, + block_number: block_number2, + block_hash: block_hash_2, + cosigner: network, + }; + let newer_signed = sign_cosign(newer_cosign, &keypair); + + cosigning.intake_cosign(&newer_signed).unwrap(); + + let latest = NetworksLatestCosignedBlock::get(&db, id, network).unwrap(); + assert_eq!(latest.cosign.block_number, block_number2); + + // TODO: Check rebroadcasted cosigns updates + } + + #[test] + fn accepts_cosign_at_global_session_last_block() { + let (session, keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let last_block = u64::from(OsRng.next_u32() % 100) + 1; // any from 1 to 100 + let mut block_hashes = Vec::new(); + { + let mut txn = db.txn(); + GlobalSessionsLastBlock::set(&mut txn, id, &last_block); + for i in 0 ..= last_block { + let hash = random_block_hash(&mut OsRng); + SubstrateBlockHash::set(&mut txn, i, &hash); + block_hashes.push(hash); + } + txn.commit(); + } + + let mut cosigning = Cosigning::new(db.clone()); + + let cosign = Cosign { + global_session: id, + block_number: last_block, + block_hash: block_hashes[usize::try_from(last_block).unwrap()], + cosigner: network, + }; + let signed = sign_cosign(cosign, &keypair); + + cosigning.intake_cosign(&signed).unwrap(); + + let latest = NetworksLatestCosignedBlock::get(&db, id, network).unwrap(); + assert_eq!(latest.cosign.block_number, last_block); + } + + #[test] + fn ignores_duplicate_fault_from_same_network() { + let (session, keypair) = random_test_session(); + let id = session.id(); + let network = session.sets[0].network; + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_number = OsRng.next_u64(); + let our_hash = random_block_hash(&mut OsRng); + let faulty_hash_1 = random_block_hash(&mut OsRng); + let faulty_hash_2 = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number, &our_hash); + txn.commit(); + } + + let faulty_cosign_1 = + Cosign { global_session: id, block_number, block_hash: faulty_hash_1, cosigner: network }; + let faulty_signed_1 = sign_cosign(faulty_cosign_1, &keypair); + + let mut cosigning = Cosigning::new(db.clone()); + cosigning.intake_cosign(&faulty_signed_1).unwrap(); + + let faults_after_first = Faults::get(&db, id).unwrap(); + assert_eq!(faults_after_first.len(), 1); + assert_eq!(faults_after_first[0].cosign.block_hash, faulty_hash_1); + + let faulty_cosign_2 = + Cosign { global_session: id, block_number, block_hash: faulty_hash_2, cosigner: network }; + let faulty_signed_2 = sign_cosign(faulty_cosign_2, &keypair); + + cosigning.intake_cosign(&faulty_signed_2).unwrap(); + + let faults_after_second = Faults::get(&db, id).unwrap(); + assert_eq!( + faults_after_second.len(), + 1, + "duplicate fault from same network should not be added" + ); + assert_eq!(faults_after_second[0].cosign.block_hash, faulty_hash_1); + } + + #[test] + fn records_fault_below_threshold() { + let set1 = random_validator_set(&mut OsRng); + let network1 = set1.network; + // Ensure we pick a distinct second network + let network2 = ExternalNetworkId::all().find(|n| *n != network1).unwrap(); + let set2 = ExternalValidatorSet { network: network2, session: Session(OsRng.next_u32()) }; + + let (keypair1, public1) = random_keypair(&mut OsRng); + let (_, public2) = random_keypair(&mut OsRng); + + let mut keys = HashMap::new(); + let mut stakes = HashMap::new(); + + keys.insert(network1, public1); + keys.insert(network2, public2); + + // stake1 must be below the 17% threshold: stake1 < (total_stake * 17) / 100 + let total_stake = OsRng.gen_range(100u64 .. 10_000); + let max_below_threshold = (total_stake * 17) / 100; + let stake1 = OsRng.gen_range(1 .. max_below_threshold.max(2)); + let stake2 = total_stake - stake1; + + stakes.insert(network1, stake1); + stakes.insert(network2, stake2); + + let session = TestGlobalSession { + start_block_number: u64::from(set1.session.0) + 1, + sets: vec![set1, set2], + keys, + stakes, + total_stake, + }; + let id = session.id(); + + let mut db = MemDb::new(); + seed_minimal_state(&mut db, &session); + + let block_number = OsRng.next_u64(); + let our_hash = random_block_hash(&mut OsRng); + let faulty_hash = random_block_hash(&mut OsRng); + { + let mut txn = db.txn(); + SubstrateBlockHash::set(&mut txn, block_number, &our_hash); + txn.commit(); + } + + let faulty_cosign = + Cosign { global_session: id, block_number, block_hash: faulty_hash, cosigner: network1 }; + let faulty_signed = sign_cosign(faulty_cosign, &keypair1); + + let mut cosigning = Cosigning::new(db.clone()); + cosigning.intake_cosign(&faulty_signed).unwrap(); + + let faults = Faults::get(&db, id).unwrap(); + assert_eq!(faults.len(), 1); + assert_eq!(faults[0].cosign.block_hash, faulty_hash); + + let faulted = FaultedSession::get(&db); + assert_eq!(faulted, None, "session should not be faulted when weight is below 17% threshold"); + } +} + +#[test] +fn intended_cosigns() { + // Empty returns empty + { + let mut db = MemDb::new(); + let set = random_validator_set(&mut OsRng); + let mut txn = db.txn(); + assert!(Cosigning::::intended_cosigns(&mut txn, set).is_empty()); + txn.commit(); + } + + // Receives sent intent + { + let mut db = MemDb::new(); + let set = random_validator_set(&mut OsRng); + let intent = random_cosign_intent(&mut OsRng); + + { + let mut txn = db.txn(); + IntendedCosigns::send(&mut txn, set, &intent); + txn.commit(); + } + + { + let mut txn = db.txn(); + let got = Cosigning::::intended_cosigns(&mut txn, set); + txn.commit(); + assert_eq!(got.len(), 1); + assert_eq!(got[0].global_session, intent.global_session); + assert_eq!(got[0].block_number, intent.block_number); + assert_eq!(got[0].block_hash, intent.block_hash); + assert_eq!(got[0].notable, intent.notable); + } + } +} diff --git a/coordinator/cosign/src/tests/delay.rs b/coordinator/cosign/src/tests/delay.rs new file mode 100644 index 000000000..65e975687 --- /dev/null +++ b/coordinator/cosign/src/tests/delay.rs @@ -0,0 +1,215 @@ +use crate::{delay::*, evaluator::*, tests::*}; + +fn now_secs() -> u64 { + now_timestamp().as_secs() +} + +struct DelayTest { + db: MemDb, +} + +impl Default for DelayTest { + fn default() -> Self { + Self { db: MemDb::new() } + } +} + +impl IntoTask for DelayTest { + type Task = CosignDelayTask; + + fn task(&self) -> Self::Task { + CosignDelayTask { db: self.db.clone() } + } +} + +impl DelayTest { + pub fn new() -> (Self, Instant) { + let start = std::time::Instant::now(); + (Self::default(), start) + } +} + +/// Verify delay's post-run DB invariants. +/// +/// After a successful task run, `CosignedBlocks` should be consumed and +/// `LatestCosignedBlockNumber` point to the expected last block number. +fn verify_db_invariants(db: &MemDb, expected_latest_block: Option) { + let actual = LatestCosignedBlockNumber::get(db); + let cosigned_pending = CosignedBlocks::peek(db).is_some(); + + assert_eq!(actual, expected_latest_block, "LatestCosignedBlockNumber mismatch"); + assert!(!cosigned_pending, "CosignedBlocks should be fully consumed"); +} + +#[tokio::test] +async fn updates_latest_finalized_block_after_ack_delay() { + *INIT_LOGGER; + let (mut test, start) = DelayTest::new(); + + // Returns false (made no progress) on no CosignedBlocks + { + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, false).await; + verify_db_invariants(&test.db, None); + } + + // Multiple blocks with the same evaluated_time sleep only for ACKNOWLEDGEMENT_DELAY + { + let mut txn = test.db.txn(); + + { + let now = now_secs(); + CosignedBlocks::send(&mut txn, &(0, now, true)); + CosignedBlocks::send(&mut txn, &(1, now, true)); + CosignedBlocks::send(&mut txn, &(2, now, true)); + txn.commit(); + } + + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&test.db, Some(2)); + } + + serai_env::log::info!("Blocks 0-2 processed in {:?}", start.elapsed()); + let start = Instant::now(); + + // Timestamps decreasing, given their time_valid already passed during + // the 1st sleep, none need to sleep after the 1st block + { + let mut txn = test.db.txn(); + + { + let now = now_secs(); + CosignedBlocks::send(&mut txn, &(3, now, true)); + CosignedBlocks::send(&mut txn, &(4, now - 5, true)); + CosignedBlocks::send(&mut txn, &(5, now - 10, true)); + txn.commit(); + } + + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&test.db, Some(5)); + } + + serai_env::log::info!("Blocks 3-5 processed in {:?}", start.elapsed()); + let start = Instant::now(); + + // Timestamps increasing in order + // each block sleeps for a slight amount + { + let mut txn = test.db.txn(); + + { + let now = now_secs(); + CosignedBlocks::send(&mut txn, &(6, now, true)); + CosignedBlocks::send(&mut txn, &(7, now + 5, true)); + CosignedBlocks::send(&mut txn, &(8, now + 10, true)); + txn.commit(); + } + + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&test.db, Some(8)); + } + + serai_env::log::info!("Blocks 6-8 processed in {:?}", start.elapsed()); + + // has_events=false blocks are marked as cosigned immediately, no sleep + { + let mut txn = test.db.txn(); + // the time_evaluated timestamp doesn't matter here since it will be skipped + CosignedBlocks::send(&mut txn, &(9, OsRng.next_u64(), false)); + CosignedBlocks::send(&mut txn, &(10, OsRng.next_u64(), false)); + CosignedBlocks::send(&mut txn, &(11, OsRng.next_u64(), false)); + txn.commit(); + + let start = Instant::now(); + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&test.db, Some(11)); + assert!( + start.elapsed() < Duration::from_secs(1), + "no-events blocks should not sleep, took {:?}", + start.elapsed() + ); + } +} + +#[tokio::test] +async fn does_not_regress_and_skips_if_not_a_later_block() { + let mut test = DelayTest::default(); + + // Does not regress + { + { + let mut txn = test.db.txn(); + CosignedBlocks::send(&mut txn, &(1, now_secs(), true)); + CosignedBlocks::send(&mut txn, &(2, now_secs(), true)); + + // Sent out of order below + CosignedBlocks::send(&mut txn, &(4, now_secs(), true)); + // 3 will be skipped after 4 was processed + CosignedBlocks::send(&mut txn, &(3, now_secs(), true)); + + txn.commit(); + } + + let mut task = test.task(); + // returns made_progress as true + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&test.db, Some(4)); + } + + // Skip if not a later block + { + { + let mut txn = test.db.txn(); + // Sends the same previous block number + CosignedBlocks::send(&mut txn, &(4, now_secs(), true)); + txn.commit(); + } + + let mut task = test.task(); + // No progress was made since the same block number was also skipped, + // made_progress returns false + TaskTest::task_runs_once_and_matches_progress(&mut task, false).await; + verify_db_invariants(&test.db, Some(4)); + } +} + +#[tokio::test] +async fn respects_acknowledgement_delay() { + let mut test = DelayTest::default(); + let block_number = OsRng.next_u64(); + + let now = now_secs(); + { + let mut txn = test.db.txn(); + CosignedBlocks::send(&mut txn, &(block_number, now, true)); + txn.commit(); + } + + let mut task = test.task(); + + // Run the task in the background (it will sleep internally for ACKNOWLEDGEMENT_DELAY) + let task_handle = tokio::spawn(async move { task.run_iteration().await }); + + // Well before ACKNOWLEDGEMENT_DELAY, the block must not be acknowledged + tokio::time::sleep(Duration::from_secs(ACKNOWLEDGEMENT_DELAY.as_secs().saturating_sub(2))).await; + assert!(LatestCosignedBlockNumber::get(&test.db).is_none()); + + // Wait for the task to complete + let made_progress = task_handle.await.unwrap().unwrap(); + assert!(made_progress); + + // Block is now finalized + verify_db_invariants(&test.db, Some(block_number)); + + // The elapsed time must be at least ACKNOWLEDGEMENT_DELAY + let new_now = now_secs(); + assert!( + Duration::from_secs(new_now) >= (Duration::from_secs(now) + ACKNOWLEDGEMENT_DELAY), + "completed in {:?}, expected at least {ACKNOWLEDGEMENT_DELAY:?}", + new_now - now, + ); +} diff --git a/coordinator/cosign/src/tests/evaluator.rs b/coordinator/cosign/src/tests/evaluator.rs new file mode 100644 index 000000000..7563cd98f --- /dev/null +++ b/coordinator/cosign/src/tests/evaluator.rs @@ -0,0 +1,609 @@ +#![expect(clippy::unchecked_time_subtraction)] + +use crate::{intend::*, evaluator::*, tests::*, *}; + +struct EvaluatorTest { + db: MemDb, +} + +impl Default for EvaluatorTest { + fn default() -> Self { + Self { db: MemDb::new() } + } +} + +impl IntoTask for EvaluatorTest { + type Task = CosignEvaluatorTask; + + fn task(&self) -> Self::Task { + let (request, _calls) = TestRequest::new(false); + CosignEvaluatorTask { db: self.db.clone(), request, last_request_for_cosigns: Instant::now() } + } +} + +impl EvaluatorTest { + fn init_global_session(&mut self, start_block_number: u64) -> ([u8; 32], ExternalNetworkId) { + let global_session = random_global_session(&mut OsRng); + let set = random_validator_set(&mut OsRng); + let info = build_global_session( + set, + random_public(&mut OsRng), + OsRng.gen_range(1 ..= u64::MAX), + start_block_number, + ); + + let mut txn = self.db.txn(); + GlobalSessionsChannel::send(&mut txn, &(global_session, info)); + txn.commit(); + + (global_session, set.network) + } + + /// Like `init_global_session` but with empty stakes, for testing the "didn't have its stake" + /// error. + fn init_stakeless_global_session( + &mut self, + start_block_number: u64, + ) -> ([u8; 32], ExternalNetworkId) { + let global_session = random_global_session(&mut OsRng); + let network = random_external_network_id(&mut OsRng); + let set = ExternalValidatorSet { network, session: Session(OsRng.gen()) }; + + let mut keys = HashMap::new(); + keys.insert(network, random_public(&mut OsRng)); + + let info = GlobalSession { + start_block_number, + sets: vec![set], + keys, + stakes: HashMap::new(), + // total_stake is not important, + // the 0 stake test fails before it is used + total_stake: OsRng.next_u64(), + }; + + let mut txn = self.db.txn(); + GlobalSessionsChannel::send(&mut txn, &(global_session, info)); + txn.commit(); + + (global_session, network) + } +} + +/// Verify evaluator's post-run DB invariants. +/// +/// After a successful task run, all input channels should be consumed and the +/// `CosignedBlocks` output channel should contain exactly the expected block range. +fn verify_db_invariants(db: &mut MemDb, expected_cosigned_range: Option<(u64, u64)>) { + use serai_env::log::debug; + + let current_session = CurrentlyEvaluatedGlobalSession::get(db); + let block_events_pending = BlockEvents::peek(db).is_some(); + let sessions_pending = GlobalSessionsChannel::peek(db).is_some(); + let cosigned_pending = CosignedBlocks::peek(db).is_some(); + + debug!( + "CurrentlyEvaluatedGlobalSession: {:?}", + current_session.as_ref().map(|(id, gs)| (hex::encode(id), gs.start_block_number)) + ); + debug!("BlockEvents pending: {block_events_pending}"); + debug!("GlobalSessionsChannel pending: {sessions_pending}"); + debug!("CosignedBlocks pending: {cosigned_pending}"); + + // All input channels should be fully consumed + assert!(!block_events_pending, "BlockEvents should be fully consumed"); + assert!(!sessions_pending, "GlobalSessionsChannel should be consumed"); + + let has_session = current_session.is_some(); + + let mut txn = db.txn(); + + // Verify cosigned blocks output + match expected_cosigned_range { + Some((start, end)) => { + assert!(has_session, "CurrentlyEvaluatedGlobalSession should exist after processing blocks"); + + for expected_block in start ..= end { + let (block_number, _time, _has_events) = CosignedBlocks::try_recv(&mut txn) + .unwrap_or_else(|| panic!("expected cosigned block {expected_block}")); + debug!("CosignedBlock: block_number={block_number}"); + assert_eq!(block_number, expected_block, "cosigned block mismatch"); + } + assert!(CosignedBlocks::try_recv(&mut txn).is_none(), "unexpected extra cosigned block"); + } + None => { + assert!(!has_session, "no session should exist when no blocks were processed"); + assert!(CosignedBlocks::try_recv(&mut txn).is_none(), "expected no cosigned blocks"); + } + } + txn.commit(); +} + +fn signed_cosign( + global_session: [u8; 32], + cosigner: ExternalNetworkId, + block_number: u64, +) -> SignedCosign { + SignedCosign { + cosign: Cosign { + global_session, + block_number, + block_hash: random_block_hash(&mut OsRng), + cosigner, + }, + signature: random_bytes_64(&mut OsRng), + } +} + +#[tokio::test] +async fn processes_blocks_with_no_events() { + let mut test = EvaluatorTest::default(); + + // Returns false (made no progress) on no blocks to evaluate + { + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, false).await; + verify_db_invariants(&mut test.db, None); + } + + test.init_global_session(0); + + // Sent BlockEvents progress and with no events are sent to CosignedBlocks + { + let mut txn = test.db.txn(); + BlockEvents::send(&mut txn, &BlockEventData { block_number: 0, has_events: HasEvents::No }); + BlockEvents::send(&mut txn, &BlockEventData { block_number: 1, has_events: HasEvents::No }); + BlockEvents::send(&mut txn, &BlockEventData { block_number: 2, has_events: HasEvents::No }); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&mut test.db, Some((0, 2))); + } + + // Advances to the next global session when blocks reach its start_block_number + { + let (session2, _) = test.init_global_session(6); + + { + let mut txn = test.db.txn(); + for block_number in 3 ..= 6 { + BlockEvents::send(&mut txn, &BlockEventData { block_number, has_events: HasEvents::No }); + } + txn.commit(); + } + + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&mut test.db, Some((3, 6))); + + let current = + CurrentlyEvaluatedGlobalSession::get(&test.db).expect("should have current session"); + assert_eq!(current.0, session2, "should have transitioned to session 2"); + assert_eq!(current.1.start_block_number, 6, "session 2 should start at block 6"); + } +} + +#[tokio::test] +async fn processes_notable_events_when_cosigned() { + let mut test = EvaluatorTest::default(); + let (global_session, network) = test.init_global_session(0); + + // Notable block with no NetworksLatestCosignedBlock set fails + { + let mut txn = test.db.txn(); + BlockEvents::send(&mut txn, &BlockEventData { block_number: 0, has_events: HasEvents::No }); + BlockEvents::send( + &mut txn, + &BlockEventData { block_number: 1, has_events: HasEvents::Notable }, + ); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "wasn't yet cosigned").await; + assert!(GlobalSessionsChannel::peek(&test.db).is_none(), "global session should be consumed"); + assert!(BlockEvents::peek(&test.db).is_some(), "block events should remain for retry"); + + // Still fails on retry even with enough time elapsed to re-request cosigns + let mut task = test.task(); + task.last_request_for_cosigns = Instant::now() - Duration::from_secs(5); + TaskTest::task_runs_and_fails_with(&mut task, "wasn't yet cosigned").await; + assert!(BlockEvents::peek(&test.db).is_some(), "block events should remain for retry"); + } + + // Same block succeeds once cosign is intake + { + let mut txn = test.db.txn(); + NetworksLatestCosignedBlock::set( + &mut txn, + global_session, + network, + &signed_cosign(global_session, network, 1), + ); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&mut test.db, Some((0, 1))); + } + + // Cosign for a later block doesn't satisfy Notable (requires exact block_number match) + { + let mut txn = test.db.txn(); + NetworksLatestCosignedBlock::set( + &mut txn, + global_session, + network, + &signed_cosign(global_session, network, 5), + ); + BlockEvents::send( + &mut txn, + &BlockEventData { block_number: 2, has_events: HasEvents::Notable }, + ); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "wasn't yet cosigned").await; + assert!(BlockEvents::peek(&test.db).is_some(), "block events should remain for retry"); + } + + // Cosign for an earlier block doesn't satisfy Notable either + // (BlockEvents already had sent block 2 from the branch above + // but the cosign is for block 1) + { + let mut txn = test.db.txn(); + NetworksLatestCosignedBlock::set( + &mut txn, + global_session, + network, + &signed_cosign(global_session, network, 1), + ); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "wasn't yet cosigned").await; + assert!(BlockEvents::peek(&test.db).is_some(), "block events should remain for retry"); + } + + // Each Notable block succeeds when cosigned with its exact block number + { + for block_number in 2 .. 4 { + let mut txn = test.db.txn(); + NetworksLatestCosignedBlock::set( + &mut txn, + global_session, + network, + &signed_cosign(global_session, network, block_number), + ); + // (block 2 is already in BlockEvents) + if block_number > 2 { + BlockEvents::send( + &mut txn, + &BlockEventData { block_number, has_events: HasEvents::Notable }, + ); + } + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&mut test.db, Some((block_number, block_number))); + } + } + + // Cosigned Notable block without stakes fails + { + let mut test = EvaluatorTest::default(); + let (global_session, network) = test.init_stakeless_global_session(0); + + let mut txn = test.db.txn(); + NetworksLatestCosignedBlock::set( + &mut txn, + global_session, + network, + &signed_cosign(global_session, network, 1), + ); + BlockEvents::send( + &mut txn, + &BlockEventData { block_number: 1, has_events: HasEvents::Notable }, + ); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "didn't have its stake").await; + } + + // request_notable_cosigns failure propagates + { + let mut test = EvaluatorTest::default(); + test.init_global_session(0); + + let mut txn = test.db.txn(); + BlockEvents::send( + &mut txn, + &BlockEventData { block_number: 1, has_events: HasEvents::Notable }, + ); + txn.commit(); + + let (request, calls) = TestRequest::new(true); + let mut task = CosignEvaluatorTask { + db: test.db.clone(), + request, + last_request_for_cosigns: Instant::now() - REQUEST_COSIGNS_SPACING - Duration::from_secs(5), + }; + + TaskTest::task_runs_and_fails_with(&mut task, "RequestError").await; + assert_eq!(calls.load(Ordering::SeqCst), 1, "request_notable_cosigns should have been called"); + } +} + +#[tokio::test] +async fn processes_non_notable_events_when_cosigned() { + let mut test = EvaluatorTest::default(); + let (global_session, network) = test.init_global_session(0); + + // NonNotable block with no NetworksLatestCosignedBlock set fails + { + let mut txn = test.db.txn(); + BlockEvents::send(&mut txn, &BlockEventData { block_number: 0, has_events: HasEvents::No }); + BlockEvents::send( + &mut txn, + &BlockEventData { block_number: 1, has_events: HasEvents::NonNotable }, + ); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "wasn't yet cosigned").await; + assert!(GlobalSessionsChannel::peek(&test.db).is_none(), "global session should be consumed"); + assert!(BlockEvents::peek(&test.db).is_some(), "block events should remain for retry"); + + // Still fails on retry even with enough time elapsed to re-request cosigns + let mut task = test.task(); + task.last_request_for_cosigns = Instant::now() - Duration::from_secs(5); + TaskTest::task_runs_and_fails_with(&mut task, "wasn't yet cosigned").await; + assert!(BlockEvents::peek(&test.db).is_some(), "block events should remain for retry"); + } + + // Same block succeeds once cosign is present + { + let mut txn = test.db.txn(); + NetworksLatestCosignedBlock::set( + &mut txn, + global_session, + network, + &signed_cosign(global_session, network, 1), + ); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&mut test.db, Some((0, 1))); + } + + // Unlike Notable, a cosign for a later block satisfies NonNotable (uses >=) + { + let mut txn = test.db.txn(); + NetworksLatestCosignedBlock::set( + &mut txn, + global_session, + network, + &signed_cosign(global_session, network, 5), + ); + BlockEvents::send( + &mut txn, + &BlockEventData { block_number: 2, has_events: HasEvents::NonNotable }, + ); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&mut test.db, Some((2, 2))); + } + + // Cosign for an earlier block doesn't satisfy NonNotable (uses >=) + { + let mut txn = test.db.txn(); + NetworksLatestCosignedBlock::set( + &mut txn, + global_session, + network, + &signed_cosign(global_session, network, 1), + ); + BlockEvents::send( + &mut txn, + &BlockEventData { block_number: 3, has_events: HasEvents::NonNotable }, + ); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "wasn't yet cosigned").await; + assert!(BlockEvents::peek(&test.db).is_some(), "block events should remain for retry"); + } + + // Multiple NonNotable blocks in one run via cached known_cosign + // (block 3 is already in BlockEvents from the failed branch above) + { + let mut txn = test.db.txn(); + NetworksLatestCosignedBlock::set( + &mut txn, + global_session, + network, + &signed_cosign(global_session, network, 10), + ); + BlockEvents::send( + &mut txn, + &BlockEventData { block_number: 4, has_events: HasEvents::NonNotable }, + ); + BlockEvents::send( + &mut txn, + &BlockEventData { block_number: 5, has_events: HasEvents::NonNotable }, + ); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&mut test.db, Some((3, 5))); + } + + // Cosigned NonNotable block without stakes fails + { + let mut test = EvaluatorTest::default(); + let (global_session, network) = test.init_stakeless_global_session(0); + + let mut txn = test.db.txn(); + NetworksLatestCosignedBlock::set( + &mut txn, + global_session, + network, + &signed_cosign(global_session, network, 5), + ); + BlockEvents::send( + &mut txn, + &BlockEventData { block_number: 1, has_events: HasEvents::NonNotable }, + ); + txn.commit(); + + let mut task = test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "didn't have its stake").await; + } + + // request_notable_cosigns failure propagates + { + let mut test = EvaluatorTest::default(); + test.init_global_session(0); + + let mut txn = test.db.txn(); + BlockEvents::send( + &mut txn, + &BlockEventData { block_number: 1, has_events: HasEvents::NonNotable }, + ); + txn.commit(); + + let (request, calls) = TestRequest::new(true); + let mut task = CosignEvaluatorTask { + db: test.db.clone(), + request, + last_request_for_cosigns: Instant::now() - REQUEST_COSIGNS_SPACING - Duration::from_secs(5), + }; + + TaskTest::task_runs_and_fails_with(&mut task, "RequestError").await; + assert_eq!(calls.load(Ordering::SeqCst), 1, "request_notable_cosigns should have been called"); + } +} + +mod errors { + use super::*; + + #[tokio::test] + #[should_panic(expected = "candidate's start block number ")] + async fn panics_when_session_starts_after_block() { + let mut test = EvaluatorTest::default(); + let start_block_number: u64 = OsRng.gen_range(2 ..= 100); + test.init_global_session(start_block_number); + + // Move the session from the channel into CurrentlyEvaluatedGlobalSession + let mut txn = test.db.txn(); + let session = GlobalSessionsChannel::try_recv(&mut txn).unwrap(); + CurrentlyEvaluatedGlobalSession::set(&mut txn, &session); + BlockEvents::send( + &mut txn, + &BlockEventData { + block_number: OsRng.gen_range(0 .. start_block_number), + has_events: HasEvents::No, + }, + ); + txn.commit(); + + let mut task = test.task(); + // will panic + let _ = task.run_iteration().await; + } + + #[tokio::test] + #[should_panic( + expected = "currently_evaluated_global_session_strict wasn't called incrementally" + )] + async fn panics_when_called_non_incrementally() { + let mut test = EvaluatorTest::default(); + test.init_global_session(0); + test.init_global_session(5); + + { + let mut txn = test.db.txn(); + BlockEvents::send(&mut txn, &BlockEventData { block_number: 10, has_events: HasEvents::No }); + txn.commit(); + } + + let mut task = test.task(); + // will panic + let _ = task.run_iteration().await; + } + + fn setup_weight_overflow(has_events: HasEvents) -> CosignEvaluatorTask { + let mut test = EvaluatorTest::default(); + let cosign_block = match has_events { + HasEvents::Notable => 1u64, + HasEvents::NonNotable => 5u64, + HasEvents::No => unreachable!(), + }; + + let overflowing_stake_global_session = { + let sets = vec![ + ExternalValidatorSet { network: ExternalNetworkId::Bitcoin, session: Session(0) }, + ExternalValidatorSet { network: ExternalNetworkId::Ethereum, session: Session(0) }, + ]; + + let mut keys = HashMap::new(); + keys.insert(ExternalNetworkId::Bitcoin, random_public(&mut OsRng)); + keys.insert(ExternalNetworkId::Ethereum, random_public(&mut OsRng)); + + let mut stakes = HashMap::new(); + stakes.insert(ExternalNetworkId::Bitcoin, u64::MAX); + stakes.insert(ExternalNetworkId::Ethereum, OsRng.next_u64()); + + let info = GlobalSession { + start_block_number: 0, + sets, + keys, + stakes, + // total_stake is not important, + // the overflow will panic before it is used + total_stake: u64::MAX, + }; + + let mut txn = test.db.txn(); + let id = random_global_session(&mut OsRng); + GlobalSessionsChannel::send(&mut txn, &(id, info)); + txn.commit(); + + id + }; + + let mut txn = test.db.txn(); + for network in [ExternalNetworkId::Bitcoin, ExternalNetworkId::Ethereum] { + NetworksLatestCosignedBlock::set( + &mut txn, + overflowing_stake_global_session, + network, + &signed_cosign(overflowing_stake_global_session, network, cosign_block), + ); + } + BlockEvents::send(&mut txn, &BlockEventData { block_number: 1, has_events }); + txn.commit(); + + test.task() + } + + #[tokio::test] + #[should_panic(expected = "attempt to add with overflow")] + async fn panics_on_weight_overflow_notable() { + let mut task = setup_weight_overflow(HasEvents::Notable); + TaskTest::task_runs_and_fails_with(&mut task, "weight_cosigned overflow").await; + } + + #[tokio::test] + #[should_panic(expected = "attempt to add with overflow")] + async fn panics_on_weight_overflow_non_notable() { + let mut task = setup_weight_overflow(HasEvents::NonNotable); + TaskTest::task_runs_and_fails_with(&mut task, "weight_cosigned overflow").await; + } +} diff --git a/coordinator/cosign/src/tests/full_stack.rs b/coordinator/cosign/src/tests/full_stack.rs new file mode 100644 index 000000000..73d41545c --- /dev/null +++ b/coordinator/cosign/src/tests/full_stack.rs @@ -0,0 +1,572 @@ +//! Full-stack integration tests for the cosign library's public API. +//! +//! While the individual components (intend, evaluate, delay) are unit-tested in their +//! respective modules, these tests verify how they integrate together as a production +//! pipeline. State is injected via the Serai node shim (`serai_shim_rpc`), exercising +//! the same `pub` API surface a real coordinator would use. + +use crate::{evaluator::*, tests::*, *}; + +/// Drain all pending cosign intents from every keyed session the fuzzer knows about. +fn drain_intents( + txn: &mut impl DbTxn, + event_fuzzer: &EventFuzzer, +) -> Vec<(ExternalNetworkId, CosignIntent)> { + let mut intents = Vec::new(); + for network in ExternalNetworkId::all() { + let max_session = event_fuzzer.next_session.get(&network).copied().unwrap_or(0); + for session_num in 0 .. max_session { + let set = ExternalValidatorSet { network, session: Session(session_num) }; + for intent in Cosigning::::intended_cosigns(txn, set) { + intents.push((network, intent)); + } + } + } + intents +} + +/// Sign an intent and intake it. Returns `Ok(())` on success or non-retryable error, +/// `Err((network, intent))` if the intent should be retried later. +fn sign_and_intake( + db: &MemDb, + cosigning: &mut Cosigning, + event_fuzzer: &EventFuzzer, + network: ExternalNetworkId, + intent: CosignIntent, +) -> Result<(), (ExternalNetworkId, CosignIntent)> { + let cosign = intent.into_cosign(network); + let Some(global_session) = GlobalSessions::get(db, intent.global_session) else { + return Err((network, intent)); + }; + let Some(public) = global_session.keys.get(&network) else { return Ok(()) }; + let Some(keypair) = event_fuzzer.keypairs.get(&public.0) else { return Ok(()) }; + let signed = sign_cosign(cosign, keypair); + match cosigning.intake_cosign(&signed) { + Ok(()) | Err(IntakeCosignError::StaleCosign) => Ok(()), + Err(e) if e.temporal() => Err((network, intent)), + Err(ref e) => { + serai_env::log::warn!( + "intake_cosign error: block={}, network={network:?}, err={e:?}", + intent.block_number, + ); + Ok(()) + } + } +} + +/// Wrapper for `run_honest_cosigning_capped` with no block cap. +async fn run_honest_cosigning( + db: &MemDb, + cosigning: &mut Cosigning, + event_fuzzer: &EventFuzzer, + should_break: impl FnMut(Option) -> bool, +) { + run_honest_cosigning_capped(db, cosigning, event_fuzzer, should_break, None, &mut Vec::new()) + .await; +} + +/// Run the honest cosigning loop: drain intents from all keyed sessions, sign them +/// with the EventFuzzer's keypairs, intake them, and repeat until `should_break` returns `true`. +/// +/// `should_break` is called each iteration with the current `latest_cosigned_block_number`. +/// +/// Intents for blocks beyond `max_block` are deferred into `deferred_intents` instead of +/// being signed. This prevents the evaluator from using high-block cosigns submitted during +/// an early phase to advance past the point where a later phase expects a stall. +async fn run_honest_cosigning_capped( + db: &MemDb, + cosigning: &mut Cosigning, + event_fuzzer: &EventFuzzer, + mut should_break: impl FnMut(Option) -> bool, + max_block: Option, + deferred_intents: &mut Vec<(ExternalNetworkId, CosignIntent)>, +) { + let mut pending_intents = Vec::<(ExternalNetworkId, CosignIntent)>::new(); + loop { + { + let mut db = db.clone(); + let mut txn = db.txn(); + for (network, intent) in drain_intents(&mut txn, event_fuzzer) { + if max_block.is_some_and(|cap| intent.block_number > cap) { + deferred_intents.push((network, intent)); + } else { + pending_intents.push((network, intent)); + } + } + txn.commit(); + } + + pending_intents = pending_intents + .drain(..) + .filter_map(|(network, intent)| { + sign_and_intake(db, cosigning, event_fuzzer, network, intent).err() + }) + .collect(); + + let latest = match Cosigning::::latest_cosigned_block_number(db) { + Ok(Some(n)) => Some(n), + _ => None, + }; + if should_break(latest) { + break; + } + + tokio::time::sleep(Duration::from_millis(50)).await; + } +} + +/// Full-stack fuzz test: intend -> evaluator -> delay pipeline with random events. +/// +/// Uses the `EventFuzzer` to generate random blocks, spawns the full `Cosigning` pipeline, +/// then simulates the cosigner role by draining intended cosigns, signing them, and feeding +/// them back via `intake_cosign`. Waits for all blocks to be cosigned. +/// +/// The shim RPC has a random failure rate enabled so that RPC calls from the intend task +/// occasionally fail, exercising the `ContinuallyRan` error/retry paths. +#[tokio::test] +async fn full_stack_fuzzed() { + *INIT_LOGGER; + + let iterations = 5; + for i in 1 ..= iterations { + let num_blocks = OsRng.gen_range(5 .. 20); + let mut event_fuzzer = EventFuzzer::new(); + let blocks = event_fuzzer.generate_blocks_with_keygen(num_blocks); + + serai_env::log::info!( + "Starting full-stack fuzz: 0..{} blocks, {} validators ({i}/{iterations})", + num_blocks - 1, + event_fuzzer.validators.len(), + ); + + let (shim, serai) = setup_shim_serai().await; + for (i, events) in blocks.into_iter().enumerate() { + shim.make_block(u64::try_from(i).unwrap(), events).await; + } + + // Random RPC failure rate between 5% and 30%, unless disabled via env var + shim.set_failure_rate(OsRng.gen_range(5 ..= 30)).await; + + let db = MemDb::new(); + + let (request, _calls) = TestRequest::new(false); + let mut cosigning = Cosigning::spawn(db.clone(), serai, request, vec![]); + + let target = u64::try_from(num_blocks - 1).unwrap(); + + run_honest_cosigning( + &db, + &mut cosigning, + &event_fuzzer, + |latest| matches!(latest, Some(n) if n >= target), + ) + .await; + + let latest = Cosigning::::latest_cosigned_block_number(&db).unwrap().unwrap(); + assert!(latest >= target, "expected latest cosigned block >= {target}, got {latest}"); + + serai_env::log::info!("Full-stack fuzz completed: all {num_blocks} blocks cosigned"); + } +} + +/// Fuzzed full-stack equivocation test. +/// +/// Mirrors `full_stack_fuzzed`, random events via `EventFuzzer`, full `Cosigning` pipeline +/// but at a random point during honest cosigning, one or more networks equivocate by signing +/// a block with a different hash. Once the faulty stake reaches the 17% threshold the protocol +/// must halt immediately, and all subsequent operations must reflect the fault. +#[tokio::test] +async fn equivocation_halts_protocol() { + *INIT_LOGGER; + + let iterations = 5; + for iteration in 1 ..= iterations { + let num_blocks = OsRng.gen_range(5 .. 20); + let mut event_fuzzer = EventFuzzer::new(); + let blocks = event_fuzzer.generate_blocks_with_keygen(num_blocks); + + serai_env::log::info!( + "equivocation fuzz: 0..{} blocks, {} validators ({iteration}/{iterations})", + num_blocks - 1, + event_fuzzer.validators.len(), + ); + + let (shim, serai) = setup_shim_serai().await; + for (i, events) in blocks.into_iter().enumerate() { + shim.make_block(u64::try_from(i).unwrap(), events).await; + } + + let mut db = MemDb::new(); + let (request, _calls) = TestRequest::new(false); + let mut cosigning = Cosigning::spawn(db.clone(), serai, request, vec![]); + + let target = u64::try_from(num_blocks - 1).unwrap(); + + // Pick a random target for when to attempt equivocation: after cosigning block N. + // We pick from the lower half so there's room for honest progress first. + let equivocation_after_block: u64 = OsRng.gen_range(2 ..= target / 2); + + let mut reached_equivocation_point = false; + let deadline = tokio::time::Instant::now() + Duration::from_mins(5); + + // Step 1: run the honest pipeline until we've cosigned enough blocks to equivocate + // We need at least one global session to exist and at least one block cosigned under it. + run_honest_cosigning(&db, &mut cosigning, &event_fuzzer, |latest| { + assert!( + tokio::time::Instant::now() < deadline, + "timed out waiting to reach equivocation point (target cosigned block \ + {equivocation_after_block}, latest={latest:?})", + ); + match latest { + Some(n) if n >= equivocation_after_block => { + reached_equivocation_point = true; + true + } + Some(n) if n >= target => true, + _ => false, + } + }) + .await; + + if !reached_equivocation_point { + serai_env::log::info!( + "equivocation fuzz ({iteration}/{iterations}): no global session formed, skipping" + ); + continue; + } + + assert!(FaultedSession::get(&db).is_none(), "should not be faulted before equivocation"); + + // Step 2: inject equivocation + + let equivocation_block = equivocation_after_block; + let indexed_hash = SubstrateBlockHash::get(&db, equivocation_block) + .expect("equivocation block should be indexed"); + + // Find the global session that covers this block via the evaluator's current session + let Some(global_session_id) = currently_evaluated_global_session(&db) else { + serai_env::log::info!( + "equivocation fuzz ({iteration}/{iterations}): no evaluated global session, skipping" + ); + continue; + }; + let global_session = + GlobalSessions::get(&db, global_session_id).expect("evaluated session should exist in DB"); + if equivocation_block < global_session.start_block_number { + serai_env::log::info!( + "equivocation fuzz ({iteration}/{iterations}): equivocation block \ + {equivocation_block} predates session start {}, skipping", + global_session.start_block_number, + ); + continue; + } + + // Pick which networks equivocate: 1 to all networks in this session + let session_networks: Vec = global_session.keys.keys().copied().collect(); + let num_faulty = OsRng.gen_range(1 ..= session_networks.len()); + let faulty_networks: Vec = + session_networks.choose_multiple(&mut OsRng, num_faulty).copied().collect(); + + let fault_threshold = (global_session.total_stake * 17) / 100; + let faulty_stake: u64 = + faulty_networks.iter().map(|n| global_session.stakes.get(n).copied().unwrap_or(0)).sum(); + + // Generate a hash that differs from the indexed one + let mut faulty_block_hash = random_block_hash(&mut OsRng); + if faulty_block_hash == indexed_hash { + faulty_block_hash = random_block_hash(&mut OsRng); + } + + serai_env::log::info!( + "equivocation fuzz ({iteration}/{iterations}): block={equivocation_block}, \ + faulty={faulty_networks:?}, faulty_stake={faulty_stake}, threshold={fault_threshold}, \ + will_fault={}", + faulty_stake >= fault_threshold, + ); + + // Submit equivocating cosigns one at a time, tracking cumulative fault weight + let mut cumulative_faulty_stake: u64 = 0; + for (fi, &faulty_net) in faulty_networks.iter().enumerate() { + let faulty_cosign = Cosign { + global_session: global_session_id, + block_number: equivocation_block, + block_hash: faulty_block_hash, + cosigner: faulty_net, + }; + let public = + global_session.keys.get(&faulty_net).expect("faulty network not in global session"); + let keypair = + event_fuzzer.keypairs.get(&public.0).expect("missing keypair for faulty network"); + let faulty_signed = sign_cosign(faulty_cosign, keypair); + cosigning.intake_cosign(&faulty_signed).unwrap(); + + let net_stake = global_session.stakes.get(&faulty_net).copied().unwrap_or(0); + cumulative_faulty_stake += net_stake; + let faulted_now = cumulative_faulty_stake >= fault_threshold; + + serai_env::log::info!( + "faulty cosign {}/{num_faulty} from {faulty_net:?} (stake={net_stake}): \ + cumulative={cumulative_faulty_stake}, threshold={fault_threshold}, faulted={faulted_now}", + fi + 1, + ); + + if faulted_now { + assert_eq!( + FaultedSession::get(&db), + Some(global_session_id), + "session should be faulted after {faulty_net:?}: cumulative stake \ + {cumulative_faulty_stake} >= threshold {fault_threshold}" + ); + } else { + assert!( + FaultedSession::get(&db).is_none(), + "session should NOT be faulted after {faulty_net:?}: cumulative stake \ + {cumulative_faulty_stake} < threshold {fault_threshold}" + ); + } + } + + if faulty_stake < fault_threshold { + serai_env::log::info!( + "equivocation fuzz ({iteration}/{iterations}): faulty stake {faulty_stake} below \ + threshold {fault_threshold}, verifying protocol continues" + ); + assert!(FaultedSession::get(&db).is_none()); + continue; + } + + // Step 3: verify the protocol is halted + + assert!( + matches!(Cosigning::::latest_cosigned_block_number(&db), Err(Faulted)), + "latest_cosigned_block_number should return Faulted" + ); + + // Verify cosigns_to_rebroadcast includes the faulty cosign(s) + let rebroadcast = cosigning.cosigns_to_rebroadcast(); + assert!( + rebroadcast.iter().any(|c| c.cosign.block_hash == faulty_block_hash), + "rebroadcast should include the faulty cosign" + ); + + // Verify that the protocol remains permanently faulted: drain any remaining intents + // and confirm latest_cosigned_block_number is still Err(Faulted). + { + let mut txn = db.txn(); + drop(drain_intents(&mut txn, &event_fuzzer)); + txn.commit(); + } + + assert!( + matches!(Cosigning::::latest_cosigned_block_number(&db), Err(Faulted)), + "latest_cosigned_block_number should remain Faulted after further operations" + ); + + serai_env::log::info!( + "equivocation fuzz ({iteration}/{iterations}): protocol halted as expected" + ); + } +} + +/// DoS test modeling the README's "5.67% practical attack": +/// If a set has >= 17% of total non-Serai stake +/// and an attacker controls 1/3 of that set's stake and goes offline, +/// that prevents the set from producing threshold signatures, leaving +/// the remaining sets not able reach the 83% commit threshold, +/// stalling but not halting the protocol. +#[tokio::test] +async fn dos_stall_offline_set() { + *INIT_LOGGER; + + let iterations = 5; + for iteration in 1 ..= iterations { + serai_env::log::info!("dos_stall_offline_set iteration {iteration}/{iterations}"); + + let num_blocks = OsRng.gen_range(10 .. 25); + let mut event_fuzzer = EventFuzzer::new(); + let mut blocks = event_fuzzer.generate_blocks_with_keygen(num_blocks); + + // Ensure at least one block in the latter half has events (a burn), + // so blocks with HasEvents::No don't let the pipeline sail through uncosigned. + let mid = num_blocks / 2; + if blocks[mid ..].iter().all(Vec::is_empty) { + #[expect(clippy::as_conversions, clippy::cast_possible_truncation)] + let burn_index = mid + (OsRng.next_u64() as usize % (num_blocks - mid)); + blocks[burn_index] = vec![vec![event_fuzzer.random_burn()]]; + } + + serai_env::log::info!( + "dos_stall fuzz: 0..{} blocks, {} validators ({iteration}/{iterations})", + num_blocks - 1, + event_fuzzer.validators.len(), + ); + + let (shim, serai) = setup_shim_serai().await; + for (i, events) in blocks.into_iter().enumerate() { + shim.make_block(u64::try_from(i).unwrap(), events).await; + } + + let db = MemDb::new(); + let (request, _calls) = TestRequest::new(false); + let mut cosigning = Cosigning::spawn(db.clone(), serai, request, vec![]); + + let target = u64::try_from(num_blocks - 1).unwrap(); + + // Step 1: honest cosigning until we have a global session to analyze. + // Cap cosign submission at step1_target so the offline network's high-water mark + // doesn't cover blocks beyond where we'll test the stall. + let step1_target: u64 = OsRng.gen_range(3 ..= target / 3); + let mut deferred_intents: Vec<(ExternalNetworkId, CosignIntent)> = Vec::new(); + run_honest_cosigning_capped( + &db, + &mut cosigning, + &event_fuzzer, + |latest| matches!(latest, Some(n) if n >= step1_target), + Some(step1_target), + &mut deferred_intents, + ) + .await; + + // Find the current global session and identify a network to take offline + let global_session_id = currently_evaluated_global_session(&db).unwrap(); + let global_session = GlobalSessions::get(&db, global_session_id).unwrap(); + let threshold = cosign_threshold(global_session.total_stake); + + // Find a network whose absence prevents reaching the threshold + let (&offline_network, &offline_stake) = global_session + .stakes + .iter() + .find(|(_, &stake)| global_session.total_stake - stake < threshold) + .unwrap(); + let online_weight: u64 = global_session + .stakes + .iter() + .filter(|(&net, _)| net != offline_network) + .map(|(_, &s)| s) + .sum(); + + let stakes_summary: Vec<_> = + global_session.stakes.iter().map(|(net, &s)| format!("{net:?}={s}")).collect(); + + serai_env::log::info!( + "dos_stall ({iteration}/{iterations}): offline={offline_network:?} \ + (stake={offline_stake}), online_weight={online_weight}, threshold={threshold}, \ + all_stakes=[{}]", + stakes_summary.join(", ") + ); + + assert!(FaultedSession::get(&db).is_none()); + let step1_latest = LatestCosignedBlockNumber::get(&db).unwrap_or(0); + + // Step 2: offline network stops signing. + // Drain all intents but only sign+submit online ones. Offline intents are kept + // in `offline_buffer` so step 3 can replay them when the network comes back. + let mut offline_buffer: Vec<(ExternalNetworkId, CosignIntent)> = Vec::new(); + let mut pending_intents: Vec<(ExternalNetworkId, CosignIntent)> = Vec::new(); + for (network, intent) in deferred_intents.drain(..) { + if network == offline_network { + offline_buffer.push((network, intent)); + } else { + pending_intents.push((network, intent)); + } + } + + loop { + { + let mut db_clone = db.clone(); + let mut txn = db_clone.txn(); + for (network, intent) in drain_intents(&mut txn, &event_fuzzer) { + if network == offline_network { + offline_buffer.push((network, intent)); + } else { + pending_intents.push((network, intent)); + } + } + txn.commit(); + } + + let before = LatestCosignedBlockNumber::get(&db).unwrap_or(0); + + pending_intents = pending_intents + .drain(..) + .filter_map(|(network, intent)| { + sign_and_intake(&db, &mut cosigning, &event_fuzzer, network, intent).err() + }) + .collect(); + + tokio::time::sleep(Duration::from_millis(100)).await; + let after = LatestCosignedBlockNumber::get(&db).unwrap_or(0); + + serai_env::log::info!( + "dos_stall ({iteration}/{iterations}) loop: before={before}, after={after}, \ + pending={}, offline_buf={}", + pending_intents.len(), + offline_buffer.len(), + ); + + // Stall detected: no progress. Any stuck pending intents (e.g. FutureGlobalSession + // for a session whose declaring block needs the offline network) go to the offline + // buffer for recovery. + if after == before { + offline_buffer.append(&mut pending_intents); + assert!(pending_intents.is_empty(), "`append` on a vector drains from it"); + break; + } + } + + let stalled_at = LatestCosignedBlockNumber::get(&db).unwrap_or(0); + assert!( + stalled_at < target, + "pipeline should be stalled before block {target}, but reached {stalled_at}" + ); + assert!(FaultedSession::get(&db).is_none(), "absence is not equivocation"); + + serai_env::log::info!( + "dos_stall ({iteration}/{iterations}): STALL verified at block {stalled_at} \ + (was {step1_latest} after step 1), online_weight={online_weight} < threshold={threshold}" + ); + + // Step 3: offline network comes back: submit buffered offline intents with retry, + // then cosign all remaining blocks via run_honest_cosigning. + // Temporal errors (FutureGlobalSession) are retried: cosigns for blocks after a + // notable block can't be accepted until the declaring block is cosigned. + while !offline_buffer.is_empty() { + offline_buffer = offline_buffer + .drain(..) + .filter_map(|(network, intent)| { + sign_and_intake(&db, &mut cosigning, &event_fuzzer, network, intent).err() + }) + .collect(); + if !offline_buffer.is_empty() { + tokio::time::sleep(Duration::from_millis(50)).await; + } + } + + let recovery_deadline = tokio::time::Instant::now() + Duration::from_mins(2); + run_honest_cosigning(&db, &mut cosigning, &event_fuzzer, |latest| { + if tokio::time::Instant::now() >= recovery_deadline { + serai_env::log::warn!( + "dos_stall ({iteration}/{iterations}): recovery timed out, latest={latest:?}" + ); + return true; + } + matches!(latest, Some(n) if n >= target) + }) + .await; + + assert!(FaultedSession::get(&db).is_none()); + let final_latest = Cosigning::::latest_cosigned_block_number(&db).unwrap().unwrap(); + if final_latest < target { + serai_env::log::warn!( + "dos_stall ({iteration}/{iterations}): recovery incomplete, \ + stalled_at={stalled_at}, final={final_latest}, target={target}, skipping" + ); + continue; + } + + serai_env::log::info!( + "dos_stall ({iteration}/{iterations}): RECOVERED, \ + stalled_at={stalled_at}, final={final_latest}" + ); + } +} diff --git a/coordinator/cosign/src/tests/intend.rs b/coordinator/cosign/src/tests/intend.rs new file mode 100644 index 000000000..3a3e7c6b5 --- /dev/null +++ b/coordinator/cosign/src/tests/intend.rs @@ -0,0 +1,374 @@ +use serai_shim_rpc::test_helpers::*; + +use crate::{intend::*, tests::*, *}; + +struct IntendTestStruct { + serai: Arc, + db: MemDb, +} + +impl IntoTask for IntendTestStruct { + type Task = CosignIntendTask; + + fn task(&self) -> Self::Task { + CosignIntendTask { db: self.db.clone(), serai: self.serai.clone() } + } +} + +/// Create a [`SeraiShimRpc`] and a [`IntendTestStruct`] connected to its Serai RPC. +async fn setup_mock_test() -> (SeraiShimRpc, IntendTestStruct) { + let (shim, serai) = setup_shim_serai().await; + (shim, IntendTestStruct { serai, db: MemDb::new() }) +} + +/// Verify all of intend's post-run DB invariants by replaying events from the Serai node. +async fn verify_db_invariants(db: &MemDb, serai: &Serai, num_blocks: usize) { + use serai_env::log::debug; + + let num_blocks_u64 = u64::try_from(num_blocks).unwrap(); + + // ScanCosignFrom should point to the block after the last processed + let scan_from = ScanCosignFrom::get(db); + debug!("ScanCosignFrom: {scan_from:?}"); + assert_eq!( + scan_from, + Some(num_blocks_u64), + "ScanCosignFrom should be {num_blocks} after processing blocks 0..={}", + num_blocks - 1 + ); + + // Replay events from the shim node to compute expected DB state. + let mut expected_stakes = HashMap::<(ExternalNetworkId, SeraiAddress), u64>::new(); + let mut expected_latest_set = HashMap::::new(); + let mut decided_validators = HashMap::>::new(); + let mut next_session = HashMap::::new(); + let mut set_keys_count = 0usize; + + for block_num in 0 .. num_blocks_u64 { + let block = serai.block_by_number(block_num).await.unwrap().unwrap(); + let hash = block.header.hash(); + let events = serai.events(hash).await.unwrap(); + let vset = events.validator_sets(); + + for event in vset.allocation_events() { + let Event::Allocation { validator, network, amount } = event else { continue }; + let Ok(net) = ExternalNetworkId::try_from(*network) else { continue }; + *expected_stakes.entry((net, *validator)).or_insert(0) += amount.0; + } + for event in vset.deallocation_events() { + let Event::Deallocation { validator, network, amount, .. } = event else { continue }; + let Ok(net) = ExternalNetworkId::try_from(*network) else { continue }; + *expected_stakes.entry((net, *validator)).or_insert(0) -= amount.0; + } + for event in vset.set_decided_events() { + let Event::SetDecided { set, validators } = event else { continue }; + let Ok(set) = ExternalValidatorSet::try_from(*set) else { continue }; + decided_validators.insert(set, validators.iter().map(|(v, _)| *v).collect()); + } + for event in vset.set_keys_events() { + let Event::SetKeys { set, .. } = event else { continue }; + let validators = decided_validators.get(set).cloned().unwrap(); + let mut total_stake = 0u64; + for v in &validators { + total_stake += expected_stakes.get(&(set.network, *v)).copied().unwrap_or(0); + } + if total_stake > 0 { + set_keys_count += 1; + expected_latest_set.insert(set.network, (set.session, total_stake)); + } + *next_session.entry(set.network).or_insert(0) += 1; + } + } + + // Verify Stakes match the expected. + for (&(network, validator), &expected_amount) in &expected_stakes { + let db_stake = Stakes::get(db, network, validator); + debug!("Stakes[{network:?}, {validator:?}]: db={db_stake:?}, expected={expected_amount}"); + assert_eq!( + db_stake, + Some(Amount(expected_amount)), + "stake mismatch for ({network:?}, {validator:?}): db={db_stake:?}, expected={expected_amount}" + ); + } + + // Verify LatestSet matches the expected. + for (&network, &(session, stake)) in &expected_latest_set { + let latest = LatestSet::get(db, network); + debug!("LatestSet[{network:?}]: db={latest:?}, expected=(session={session:?}, stake={stake})"); + assert!(latest.is_some(), "LatestSet should exist for {network:?}"); + let latest = latest.unwrap(); + assert_eq!(latest.session, session, "LatestSet session mismatch for {network:?}"); + assert_eq!(latest.stake.0, stake, "LatestSet stake mismatch for {network:?}"); + } + + // No pending Validators entries (all should have been taken by SetKeys). + for &network in expected_latest_set.keys() { + let session_num = next_session.get(&network).copied().unwrap_or(0); + if session_num > 0 { + let last_set = ExternalValidatorSet { network, session: Session(session_num - 1) }; + let validators = Validators::get(db, last_set); + debug!("Validators[{last_set:?}]: {validators:?} (should be None)"); + assert_eq!( + validators, None, + "Validators for {last_set:?} should have been consumed by SetKeys" + ); + } + } + + // Log and verify LatestGlobalSessionIntended + let latest_session_id = LatestGlobalSessionIntended::get(db); + debug!("LatestGlobalSessionIntended: {:?}", latest_session_id.map(hex::encode)); + + // If any SetKeys happened, a GlobalSession should exist with consistent total_stake + if set_keys_count > 0 { + assert!( + latest_session_id.is_some(), + "LatestGlobalSessionIntended should exist after {set_keys_count} SetKeys events", + ); + let session_id = latest_session_id.unwrap(); + let session = GlobalSessions::get(db, session_id); + assert!(session.is_some(), "GlobalSession should exist"); + let session = session.unwrap(); + + debug!( + "GlobalSession {}: start_block_number={}, total_stake={}, sets={:?}, stakes={:?}", + &hex::encode(&session_id[.. 8]), + session.start_block_number, + session.total_stake, + session.sets, + session.stakes, + ); + debug!("last_block: {:?}", GlobalSessionsLastBlock::get(db, session_id)); + + let sum: u64 = session.stakes.values().sum(); + assert_eq!( + session.total_stake, sum, + "GlobalSession total_stake should equal sum of individual stakes" + ); + } + + // SubstrateBlockHash index + let max_block = scan_from.unwrap_or(0); + if max_block > 0 { + debug!("SubstrateBlockHash index ({max_block} blocks):"); + for b in 0 .. max_block { + if let Some(hash) = SubstrateBlockHash::get(db, b) { + debug!(" #{b}: {}…", &hex::encode(&hash.0[.. 8])); + } + } + } + + serai_env::info!( + "DB invariants verified: {} blocks, {} stake entries, {} LatestSets, {} SetKeys events", + num_blocks, + expected_stakes.len(), + expected_latest_set.len(), + set_keys_count + ); + + /* + TODO: + + BuildsUpon: () -> IncrementalUnbalancedMerkleTree, + GlobalSessionsChannel: () -> ([u8; 32], GlobalSession), + BlockEvents: () -> BlockEventData, + IntendedCosigns: (set: ExternalValidatorSet) -> CosignIntent, + */ +} + +mod errors { + use super::*; + + #[tokio::test] + #[should_panic(expected = "node's block #2 doesn't build upon the block #1 prior indexed")] + async fn panics_if_chain_is_not_linear() { + let (shim, task_test) = setup_mock_test().await; + + shim.make_block(0, vec![]).await; + shim.make_block(1, vec![]).await; + shim.make_non_linear_block(2, vec![]).await; + + let mut task = task_test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "doesn't build upon").await; + } + + #[tokio::test] + #[should_panic(expected = "couldn't get block #0 which should've been finalized")] + async fn panics_if_block_not_found() { + let (_shim, task_test) = setup_mock_test().await; + + // No blocks yet, fails because serai.latest_finalized_block_number() defaults to 0 + // even without block 0 existing yet, so it fails when getting block 0 + let mut task = task_test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "").await; + } + + #[tokio::test] + #[should_panic(expected = "couldn't get block #2 which should've been finalized")] + async fn panics_if_later_block_not_found() { + let (shim, task_test) = setup_mock_test().await; + + shim.make_block(0, vec![]).await; + shim.make_block(1, vec![]).await; + shim.make_block(2, vec![]).await; + shim.set_block_missing(2).await; + + let mut task = task_test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "").await; + } + + #[tokio::test] + async fn handles_rpc_error_on_block_fetch() { + let (shim, task_test) = setup_mock_test().await; + + shim.make_block(0, vec![]).await; + shim.make_block(1, vec![]).await; + shim.make_block(2, vec![]).await; + shim.set_block_number_error("blockchain/block", 2, "connection refused").await; + + let mut task = task_test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "RPC error fetching block").await; + + assert_eq!(ScanCosignFrom::get(&task_test.db), Some(2)); + + shim.clear_block_number_error("blockchain/block", 2).await; + + let mut task = task_test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&task_test.db, &task_test.serai, 3).await; + } + + #[tokio::test] + async fn handles_rpc_error_on_events_fetch() { + let (shim, task_test) = setup_mock_test().await; + + shim.make_block(0, vec![]).await; + shim.make_block(1, vec![]).await; + let block2_hash = shim.make_block(2, vec![]).await; + shim.set_block_hash_error("blockchain/events", block2_hash, "timeout").await; + + let mut task = task_test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "RPC error fetching events").await; + + assert_eq!(ScanCosignFrom::get(&task_test.db), Some(2)); + + shim.clear_block_hash_error("blockchain/events", block2_hash).await; + + let mut task = task_test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&task_test.db, &task_test.serai, 3).await; + } + + #[tokio::test] + #[should_panic(expected = "validator set from Event::SetDecided was empty")] + async fn errors_if_set_decided_has_empty_validators() { + let (shim, task_test) = setup_mock_test().await; + shim.make_block(0, vec![]).await; + + let empty_set_decided = set_decided_event( + ValidatorSet { + network: NetworkId::External(ExternalNetworkId::Bitcoin), + session: Session(0), + }, + vec![], + ); + shim.make_block(1, vec![vec![empty_set_decided]]).await; + + let mut task = task_test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, false).await; + } + + #[tokio::test] + async fn handles_rpc_error_on_latest_finalized() { + let (shim, task_test) = setup_mock_test().await; + + shim.make_block(0, vec![]).await; + shim.make_block(1, vec![]).await; + shim.set_error("blockchain/latest_finalized_block_number", "network error").await; + + let mut task = task_test.task(); + TaskTest::task_runs_and_fails_with(&mut task, "RPC error fetching latest finalized").await; + + // No blocks processed, error happened before scanning + assert_eq!(ScanCosignFrom::get(&task_test.db), None); + + shim.clear_error("blockchain/latest_finalized_block_number").await; + + let mut task = task_test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + verify_db_invariants(&task_test.db, &task_test.serai, 2).await; + } +} + +#[tokio::test] +async fn deallocating_zero_is_a_noop() { + let (shim, task_test) = setup_mock_test().await; + + let network = NetworkId::External(ExternalNetworkId::Bitcoin); + + { + let validator = random_serai_address(&mut OsRng); + + // Block 0: allocate 0 stake to the validator + shim.make_block(0, vec![vec![allocation_event(validator, network, 0)]]).await; + // Block 1: deallocate 0 from the same validator + shim.make_block(1, vec![vec![deallocation_event(validator, network, 0)]]).await; + + let mut task = task_test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + + // Verify it works and the stake is unchanged after the 0-deallocation + let stake = Stakes::get(&task_test.db, ExternalNetworkId::Bitcoin, validator); + assert_eq!(stake, Some(Amount(0)), "stake should be unchanged after deallocating 0"); + } + + { + let validator = random_serai_address(&mut OsRng); + let amount = OsRng.next_u64(); + // Block 2: allocate stake to the validator + shim.make_block(2, vec![vec![allocation_event(validator, network, amount)]]).await; + // Block 3: deallocate 0 from the same validator + shim.make_block(3, vec![vec![deallocation_event(validator, network, 0)]]).await; + + let mut task = task_test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + + // Verify the 0-deallocation works and the stake is unchanged after it + let stake = Stakes::get(&task_test.db, ExternalNetworkId::Bitcoin, validator); + assert_eq!(stake, Some(Amount(amount)), "stake should be unchanged after deallocating 0"); + } + + { + let validator = random_serai_address(&mut OsRng); + // deallocate 0 from a non-existent validator + shim.make_block(4, vec![vec![deallocation_event(validator, network, 0)]]).await; + + let mut task = task_test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + let stake = Stakes::get(&task_test.db, ExternalNetworkId::Bitcoin, validator); + // TODO: For a Stake which is `Amount(0)`, should we optimize the state by setting to `None`? + assert_eq!(stake, Some(Amount(0))); + } +} + +#[tokio::test] +async fn fuzzed_event_processing() { + *INIT_LOGGER; + + let num_blocks = 1000; + + let mut fuzzer = EventFuzzer::new(); + let blocks = fuzzer.generate_blocks(num_blocks); + + serai_env::log::info!("Fuzz test: {} blocks, {} validators", num_blocks, fuzzer.validators.len()); + + let (shim, task_test) = setup_mock_test().await; + for (i, events) in blocks.into_iter().enumerate() { + shim.make_block(u64::try_from(i).unwrap(), events).await; + } + + let mut task = task_test.task(); + TaskTest::task_runs_once_and_matches_progress(&mut task, true).await; + + verify_db_invariants(&task_test.db, &task_test.serai, num_blocks).await; +} diff --git a/coordinator/cosign/src/tests/mod.rs b/coordinator/cosign/src/tests/mod.rs new file mode 100644 index 000000000..21a14dc11 --- /dev/null +++ b/coordinator/cosign/src/tests/mod.rs @@ -0,0 +1,120 @@ +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, LazyLock, + }, + time::{Duration, Instant}, +}; + +use borsh::{BorshSerialize, BorshDeserialize}; + +use rand_core::{RngCore, CryptoRng, OsRng}; +use rand::{Rng as _, seq::SliceRandom as _}; + +use serai_client_serai::{ + abi::{ + primitives::{ + crypto::*, address::SeraiAddress, network_id::*, balance::*, validator_sets::*, + test_helpers::*, + }, + validator_sets::Event, + }, + Serai, +}; + +use serai_db::{DbTxn, Db as _, MemDb}; +use serai_task::{ + ContinuallyRan, Task, + test_helpers::{IntoTask, TaskTest}, +}; +use serai_cosign_types::{ + SignedCosign, + tests::{ + random_external_network_id, random_global_session, random_cosign_intent, random_cosign, + sign_cosign, + }, +}; + +use serai_shim_rpc::{*, event_fuzzer::*}; + +use crate::{GlobalSession, RequestNotableCosigns}; + +mod intend; +mod evaluator; +mod delay; +mod cosigning; +mod full_stack; + +static INIT_LOGGER: LazyLock<()> = LazyLock::new(|| { + serai_env::init_logger(); +}); + +#[derive(Clone)] +struct TestRequest { + calls: Arc, + should_error: bool, +} + +#[derive(Debug)] +struct RequestError; + +impl TestRequest { + fn new(should_error: bool) -> (Self, Arc) { + let calls = Arc::new(AtomicUsize::new(0)); + (Self { calls: calls.clone(), should_error }, calls) + } +} + +impl RequestNotableCosigns for TestRequest { + type Error = RequestError; + + fn request_notable_cosigns( + &self, + _global_session: [u8; 32], + ) -> impl Send + core::future::Future> { + let calls = self.calls.clone(); + let should_error = self.should_error; + async move { + calls.fetch_add(1, Ordering::SeqCst); + if should_error { + Err(RequestError) + } else { + Ok(()) + } + } + } +} + +/// Create a [`SeraiShimRpc`] and an [`Arc`] to use it. +async fn setup_shim_serai() -> (SeraiShimRpc, Arc) { + let shim_serai = SeraiShimRpc::start(ShimState::default()).await; + let serai = Arc::new(Serai::new(shim_serai.url()).unwrap()); + (shim_serai, serai) +} + +fn default_test_validator_set() -> ExternalValidatorSet { + ExternalValidatorSet { network: ExternalNetworkId::Bitcoin, session: Session(0) } +} +fn random_validator_set(rng: &mut R) -> ExternalValidatorSet { + ExternalValidatorSet { + network: random_external_network_id(rng), + session: Session(rng.next_u32()), + } +} + +/// Build a single-network [`GlobalSession`] from the given components. +fn build_global_session( + set: ExternalValidatorSet, + public: Public, + stake: u64, + start_block_number: u64, +) -> GlobalSession { + GlobalSession { + start_block_number, + sets: vec![set], + keys: HashMap::from([(set.network, public)]), + stakes: HashMap::from([(set.network, stake)]), + total_stake: stake, + } +} diff --git a/coordinator/cosign/types/Cargo.toml b/coordinator/cosign/types/Cargo.toml index c7d3dc0f5..66a90d22e 100644 --- a/coordinator/cosign/types/Cargo.toml +++ b/coordinator/cosign/types/Cargo.toml @@ -17,8 +17,14 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [dependencies] +rand_core = { version = "0.6", default-features = false, features = ["std"], optional = true } schnorrkel = { version = "0.11", default-features = false, features = ["std"] } - borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } - serai-primitives = { path = "../../../substrate/primitives", default-features = false, features = ["std"] } + +[dev-dependencies] +rand_core = { version = "0.6", default-features = false, features = ["std"] } +serai-primitives = { path = "../../../substrate/primitives", features = ["test-helpers"] } + +[features] +test-helpers = ["rand_core", "serai-primitives/test-helpers"] diff --git a/coordinator/cosign/types/src/lib.rs b/coordinator/cosign/types/src/lib.rs index 8ef592d7d..01499eba3 100644 --- a/coordinator/cosign/types/src/lib.rs +++ b/coordinator/cosign/types/src/lib.rs @@ -5,6 +5,10 @@ use borsh::{BorshSerialize, BorshDeserialize}; use serai_primitives::{BlockHash, crypto::Public, network_id::ExternalNetworkId}; +#[cfg(any(test, feature = "test-helpers"))] +/// Test helpers and fixtures. +pub mod tests; + /// The schnorrkel context to used when signing a cosign. pub const COSIGN_CONTEXT: &[u8] = b"/serai/coordinator/cosign"; diff --git a/coordinator/cosign/types/src/tests/mod.rs b/coordinator/cosign/types/src/tests/mod.rs new file mode 100644 index 000000000..6246de3e0 --- /dev/null +++ b/coordinator/cosign/types/src/tests/mod.rs @@ -0,0 +1,155 @@ +use rand_core::{RngCore, CryptoRng}; + +use serai_primitives::{network_id::ExternalNetworkId, test_helpers::random_block_hash}; + +use crate::{COSIGN_CONTEXT, CosignIntent, Cosign, SignedCosign}; + +/// Sign a [`Cosign`] with a schnorrkel keypair, producing a [`SignedCosign`]. +pub fn sign_cosign(cosign: Cosign, keypair: &schnorrkel::Keypair) -> SignedCosign { + SignedCosign { + signature: keypair.sign_simple(COSIGN_CONTEXT, &cosign.signature_message()).to_bytes(), + cosign, + } +} + +/// Generate a random [`ExternalNetworkId`] for testing. +pub fn random_external_network_id(rng: &mut (impl RngCore + CryptoRng)) -> ExternalNetworkId { + let all: Vec<_> = ExternalNetworkId::all().collect(); + #[expect(clippy::as_conversions, clippy::cast_possible_truncation)] + all[(rng.next_u64() as usize) % all.len()] +} + +/// Generate a random global session ID (`[u8; 32]`). +pub fn random_global_session(rng: &mut R) -> [u8; 32] { + serai_primitives::test_helpers::random_bytes_32(rng) +} + +/// Generate a random [`Cosign`] for testing. +pub fn random_cosign(rng: &mut (impl RngCore + CryptoRng)) -> Cosign { + Cosign { + global_session: random_global_session(rng), + block_number: rng.next_u64(), + block_hash: random_block_hash(rng), + cosigner: random_external_network_id(rng), + } +} + +/// Generate a random [`CosignIntent`] for testing. +pub fn random_cosign_intent(rng: &mut (impl RngCore + CryptoRng)) -> CosignIntent { + CosignIntent { + global_session: random_global_session(rng), + block_number: rng.next_u64(), + block_hash: random_block_hash(rng), + notable: rng.next_u32() % 2 == 0, + } +} + +#[test] +fn cosign_intent_into_cosign() { + use rand_core::OsRng; + + let intent = random_cosign_intent(&mut OsRng); + let network = random_external_network_id(&mut OsRng); + let Cosign { global_session, block_number, block_hash, cosigner } = intent.into_cosign(network); + + assert_eq!(intent.global_session, global_session); + assert_eq!(intent.block_number, block_number); + assert_eq!(intent.block_hash, block_hash); + assert_eq!(cosigner, network); +} + +#[test] +fn deterministic_and_comprehensive_signature_message() { + use rand_core::OsRng; + + let cosign = random_cosign(&mut OsRng); + let msg = cosign.signature_message(); + + // Deterministic + assert_eq!(msg, cosign.signature_message(), "signature_message should be deterministic"); + + // Comprehensive + { + let Cosign { global_session, block_number, block_hash, cosigner } = cosign; + let mut expected = Vec::new(); + expected.extend(borsh::to_vec(&(global_session, block_number, block_hash, cosigner)).unwrap()); + assert_eq!(msg, expected, "signature_message should include all fields in Borsh order"); + } + + // Changing any single field must produce a different message + let Cosign { global_session, block_number, block_hash, cosigner } = cosign; + { + let mut other_session = global_session; + other_session[0] ^= 1; + let other = Cosign { global_session: other_session, ..cosign }; + assert_ne!(msg, other.signature_message(), "different global_session must change message"); + } + { + let other = Cosign { block_number: block_number.wrapping_add(1), ..cosign }; + assert_ne!(msg, other.signature_message(), "different block_number must change message"); + } + { + let mut other_hash = block_hash; + other_hash.0[0] ^= 1; + let other = Cosign { block_hash: other_hash, ..cosign }; + assert_ne!(msg, other.signature_message(), "different block_hash must change message"); + } + { + let other_cosigner = ExternalNetworkId::all().find(|n| *n != cosigner).unwrap(); + let other = Cosign { cosigner: other_cosigner, ..cosign }; + assert_ne!(msg, other.signature_message(), "different cosigner must change message"); + } +} + +#[test] +fn signed_cosign_verify_signature() { + use rand_core::OsRng; + use serai_primitives::test_helpers::random_keypair; + + { + let (keypair, public) = random_keypair(&mut OsRng); + let cosign = random_cosign(&mut OsRng); + let signed = sign_cosign(cosign, &keypair); + assert!(signed.verify_signature(public), "valid signature should verify"); + } + + { + let (keypair1, _) = random_keypair(&mut OsRng); + let (_, public2) = random_keypair(&mut OsRng); + let cosign = random_cosign(&mut OsRng); + let signed = sign_cosign(cosign, &keypair1); + assert!(!signed.verify_signature(public2), "invalid signature should not verify"); + } + + { + let (keypair, _) = random_keypair(&mut OsRng); + let cosign = random_cosign(&mut OsRng); + let signed = sign_cosign(cosign, &keypair); + let invalid_bytes = [255u8; 32]; + assert!( + schnorrkel::PublicKey::from_bytes(&invalid_bytes).is_err(), + "test precondition: bytes should be invalid for schnorrkel" + ); + + let invalid_pubkey = serai_primitives::crypto::Public(invalid_bytes); + assert!( + !signed.verify_signature(invalid_pubkey), + "invalid public key bytes should return false" + ); + } + + { + let cosign = random_cosign(&mut OsRng); + + let invalid_sig_bytes = [255u8; 64]; + assert!( + schnorrkel::Signature::from_bytes(&invalid_sig_bytes).is_err(), + "test precondition: signature bytes should be invalid for schnorrkel" + ); + + let signed = SignedCosign { cosign, signature: invalid_sig_bytes }; + + let (_, valid_public) = random_keypair(&mut OsRng); + assert!(!signed.verify_signature(valid_public), "invalid signature bytes should return false"); + } +} diff --git a/coordinator/p2p/src/lib.rs b/coordinator/p2p/src/lib.rs index e6726492f..275eb950c 100644 --- a/coordinator/p2p/src/lib.rs +++ b/coordinator/p2p/src/lib.rs @@ -93,7 +93,7 @@ fn handle_notable_cosigns_request( global_session: [u8; 32], channel: oneshot::Sender>, ) { - let cosigns = Cosigning::::notable_cosigns(db, global_session); + let cosigns = Cosigning::::notable_or_latest_cosigns(db, global_session); channel.send(cosigns).expect("channel listening for cosign oneshot response was dropped?"); } diff --git a/coordinator/src/db.rs b/coordinator/src/db.rs index 87d59c0b3..2bdc9f3ee 100644 --- a/coordinator/src/db.rs +++ b/coordinator/src/db.rs @@ -59,7 +59,7 @@ pub(crate) fn tributary_db(set: ExternalValidatorSet) -> Db { } pub(crate) fn prune_tributary_db(set: ExternalValidatorSet) { - log::info!("pruning data directory for tributary {set:?}"); + serai_env::info!("pruning data directory for tributary {set:?}"); let db = tributary_db_folder(set); if fs::exists(&db).expect("couldn't check if tributary DB exists") { fs::remove_dir_all(db).unwrap(); diff --git a/coordinator/src/main.rs b/coordinator/src/main.rs index d3882d8e9..74c641786 100644 --- a/coordinator/src/main.rs +++ b/coordinator/src/main.rs @@ -1,6 +1,6 @@ #![allow(clippy::std_instead_of_alloc, clippy::std_instead_of_core)] -use core::{ops::Deref as _, str::FromStr as _, time::Duration}; +use core::{ops::Deref as _, time::Duration}; use std::{sync::Arc, collections::HashMap, time::Instant}; use zeroize::{Zeroize as _, Zeroizing}; @@ -69,12 +69,12 @@ async fn serai() -> Arc { "http://{}:9944", serai_env::var("SERAI_HOSTNAME").expect("Serai hostname wasn't provided") )) else { - log::error!("couldn't connect to the Serai node"); + serai_env::error!("couldn't connect to the Serai node"); tokio::time::sleep(delay).await; delay = (delay + SERAI_CONNECTION_DELAY).min(MAX_SERAI_CONNECTION_DELAY); continue; }; - log::info!("made initial connection to Serai node"); + serai_env::info!("made initial connection to Serai node"); return Arc::new(serai); } } @@ -94,7 +94,7 @@ fn spawn_cosigning( loop { // Intake our own cosigns match Cosigning::::latest_cosigned_block_number(&db) { - Ok(latest_cosigned_block_number) => { + Ok(Some(latest_cosigned_block_number)) => { let mut txn = db.txn(); // The cosigns we prior tried to intake yet failed to let mut cosigns = ErroneousCosigns::get(&txn).unwrap_or(vec![]); @@ -126,10 +126,11 @@ fn spawn_cosigning( txn.commit(); } + Ok(None) => {} Err(Faulted) => { // We don't panic here as the following code rebroadcasts our cosigns which is // necessary to inform other coordinators of the faulty cosigns - log::error!("cosigning faulted"); + serai_env::error!("cosigning faulted"); } } @@ -330,13 +331,8 @@ async fn handle_network( #[tokio::main] async fn main() { // Initialize the logger - env_logger::builder() - .filter_level( - log::LevelFilter::from_str(&serai_env::var("RUST_LOG").unwrap_or_else(|| "info".to_owned())) - .expect("`RUST_LOG` environment variable had an invalid filter"), - ) - .init(); - log::info!("starting coordinator service..."); + serai_env::init_logger(); + serai_env::info!("starting coordinator service..."); // Read the Serai key from the env let serai_key = { diff --git a/coordinator/src/tributary.rs b/coordinator/src/tributary.rs index 083554570..f23ee95b8 100644 --- a/coordinator/src/tributary.rs +++ b/coordinator/src/tributary.rs @@ -68,7 +68,7 @@ async fn provide_transaction( // present on-chain, and this enters an infinite loop to block the calling task from // advancing Err(ProvidedError::LocalMismatchesOnChain) => loop { - log::error!( + serai_env::error!( "Tributary {set:?} was supposed to provide {tx:?} but peers disagree, halting Tributary", ); // Print this every five minutes as this does need to be handled @@ -106,11 +106,11 @@ impl ContinuallyRan let latest = match Cosigning::::latest_cosigned_block_number(&txn) { Ok(latest) => latest, Err(Faulted) => { - log::error!("cosigning faulted"); + serai_env::error!("cosigning faulted"); Err("cosigning faulted")? } }; - if latest < cosign.block_number { + if latest < Some(cosign.block_number) { break; } diff --git a/coordinator/substrate/src/canonical.rs b/coordinator/substrate/src/canonical.rs index 02f5069b0..3b9ec86c8 100644 --- a/coordinator/substrate/src/canonical.rs +++ b/coordinator/substrate/src/canonical.rs @@ -43,8 +43,11 @@ impl ContinuallyRan for CanonicalEventStream { fn run_iteration(&mut self) -> impl Send + Future> { async move { let next_block = NextBlock::get(&self.db).unwrap_or(0); - let latest_finalized_block = - Cosigning::::latest_cosigned_block_number(&self.db).map_err(|e| format!("{e:?}"))?; + let Some(latest_finalized_block) = + Cosigning::::latest_cosigned_block_number(&self.db).map_err(|e| format!("{e:?}"))? + else { + return Ok(false); + }; // These are all the events which generate canonical messages struct CanonicalEvents { diff --git a/coordinator/substrate/src/ephemeral.rs b/coordinator/substrate/src/ephemeral.rs index dc2de117a..a7df64483 100644 --- a/coordinator/substrate/src/ephemeral.rs +++ b/coordinator/substrate/src/ephemeral.rs @@ -53,8 +53,11 @@ impl ContinuallyRan for EphemeralEventStream { fn run_iteration(&mut self) -> impl Send + Future> { async move { let next_block = NextBlock::get(&self.db).unwrap_or(0); - let latest_finalized_block = - Cosigning::::latest_cosigned_block_number(&self.db).map_err(|e| format!("{e:?}"))?; + let Some(latest_finalized_block) = + Cosigning::::latest_cosigned_block_number(&self.db).map_err(|e| format!("{e:?}"))? + else { + return Ok(false); + }; // These are all the events which generate canonical messages struct EphemeralEvents { diff --git a/crypto/dkg/evrf/Cargo.toml b/crypto/dkg/evrf/Cargo.toml index 296c4b9a5..4524f9503 100644 --- a/crypto/dkg/evrf/Cargo.toml +++ b/crypto/dkg/evrf/Cargo.toml @@ -28,16 +28,16 @@ std-shims = { version = "0.1", path = "../../../common/std-shims", default-featu transcript = { package = "flexible-transcript", path = "../../transcript", version = "0.3.2", default-features = false, features = ["recommended"] } ciphersuite = { path = "../../ciphersuite", version = "0.4.1", default-features = false, features = ["alloc"] } -multiexp = { path = "../../multiexp", version = "0.5", default-features = false } +multiexp = { path = "../../multiexp", version = "0.4", default-features = false } generic-array = { version = "1", default-features = false, features = ["alloc"] } blake2 = { version = "0.11.0-rc.5", default-features = false } rand_chacha = { version = "0.3", default-features = false } -generalized-bulletproofs = { git = "https://github.com/monero-oxide/monero-oxide", rev = "dc1b3dbe436aae61ec363505052d4715d38ce1df", default-features = false } -ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "dc1b3dbe436aae61ec363505052d4715d38ce1df", default-features = false } -generalized-bulletproofs-circuit-abstraction = { git = "https://github.com/monero-oxide/monero-oxide", rev = "dc1b3dbe436aae61ec363505052d4715d38ce1df", default-features = false } -generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "dc1b3dbe436aae61ec363505052d4715d38ce1df", default-features = false } +generalized-bulletproofs = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15", default-features = false } +ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15", default-features = false } +generalized-bulletproofs-circuit-abstraction = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15", default-features = false } +generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15", default-features = false } dkg = { path = "..", default-features = false } @@ -52,7 +52,7 @@ rand = { version = "0.8", default-features = false, features = ["std"] } ciphersuite = { path = "../../ciphersuite", default-features = false, features = ["std"] } embedwards25519 = { path = "../../embedwards25519", default-features = false, features = ["std"] } dalek-ff-group = { path = "../../dalek-ff-group", default-features = false, features = ["std"] } -generalized-bulletproofs = { git = "https://github.com/monero-oxide/monero-oxide", rev = "dc1b3dbe436aae61ec363505052d4715d38ce1df", features = ["tests"] } +generalized-bulletproofs = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15", features = ["tests"] } dkg-recovery = { path = "../recovery" } [features] diff --git a/crypto/dkg/musig/Cargo.toml b/crypto/dkg/musig/Cargo.toml index eda09ea7e..7f9286852 100644 --- a/crypto/dkg/musig/Cargo.toml +++ b/crypto/dkg/musig/Cargo.toml @@ -25,7 +25,7 @@ zeroize = { version = "^1.5", default-features = false, features = ["zeroize_der std-shims = { version = "0.1", path = "../../../common/std-shims", default-features = false, features = ["alloc"] } -multiexp = { path = "../../multiexp", version = "0.5", default-features = false } +multiexp = { path = "../../multiexp", version = "0.4", default-features = false } ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false } dkg = { path = "../", version = "0.6", default-features = false } diff --git a/crypto/ed448/Cargo.toml b/crypto/ed448/Cargo.toml index 03644cbf1..3b9bb6289 100644 --- a/crypto/ed448/Cargo.toml +++ b/crypto/ed448/Cargo.toml @@ -19,7 +19,7 @@ workspace = true [dependencies] zeroize = { version = "1", default-features = false, features = ["zeroize_derive"] } -sha3 = { version = "0.11.0-rc.7", default-features = false } +sha3 = { version = "0.11", default-features = false } crypto-bigint = { version = "0.7", default-features = false, features = ["zeroize"] } prime-field = { path = "../prime-field", default-features = false } diff --git a/crypto/embedwards25519/Cargo.toml b/crypto/embedwards25519/Cargo.toml index 53bc941a4..5ef61dbec 100644 --- a/crypto/embedwards25519/Cargo.toml +++ b/crypto/embedwards25519/Cargo.toml @@ -27,7 +27,7 @@ curve25519-dalek = { version = "4", default-features = false, features = ["legac blake2 = { version = "0.11.0-rc.5", default-features = false } ciphersuite = { path = "../ciphersuite", version = "0.4", default-features = false } -generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "dc1b3dbe436aae61ec363505052d4715d38ce1df", default-features = false, optional = true } +generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15", default-features = false, optional = true } [dev-dependencies] hex = "0.4" diff --git a/crypto/frost/Cargo.toml b/crypto/frost/Cargo.toml index a11591d71..861c8a796 100644 --- a/crypto/frost/Cargo.toml +++ b/crypto/frost/Cargo.toml @@ -39,7 +39,7 @@ sha2 = { version = "0.10", default-features = false, optional = true } elliptic-curve = { version = "0.13", default-features = false, features = ["hash2curve"], optional = true } ciphersuite-kp256 = { path = "../ciphersuite/kp256", version = "0.4", default-features = false, features = ["alloc"], optional = true } -multiexp = { path = "../multiexp", version = "0.5", default-features = false, features = ["alloc", "batch"] } +multiexp = { path = "../multiexp", version = "0.4", default-features = false, features = ["alloc", "batch"] } schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "0.5.1", default-features = false, features = ["alloc"] } diff --git a/crypto/multiexp/Cargo.toml b/crypto/multiexp/Cargo.toml index a65ae5790..adff31b64 100644 --- a/crypto/multiexp/Cargo.toml +++ b/crypto/multiexp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multiexp" -version = "0.5.0" +version = "0.4.2" description = "Multiexponentiation algorithms for ff/group" license = "MIT" repository = "https://github.com/serai-dex/serai/tree/develop/crypto/multiexp" diff --git a/crypto/schnorr/Cargo.toml b/crypto/schnorr/Cargo.toml index 146b335b6..27e940799 100644 --- a/crypto/schnorr/Cargo.toml +++ b/crypto/schnorr/Cargo.toml @@ -26,7 +26,7 @@ zeroize = { version = "1.5", default-features = false, features = ["zeroize_deri transcript = { package = "flexible-transcript", path = "../transcript", version = "0.3.2", default-features = false, optional = true } ciphersuite = { path = "../ciphersuite", version = "0.4.1", default-features = false } -multiexp = { path = "../multiexp", version = "0.5", default-features = false } +multiexp = { path = "../multiexp", version = "0.4", default-features = false } [dev-dependencies] hex = "0.4" diff --git a/crypto/secq256k1/Cargo.toml b/crypto/secq256k1/Cargo.toml index 2638dad3f..e89ea295c 100644 --- a/crypto/secq256k1/Cargo.toml +++ b/crypto/secq256k1/Cargo.toml @@ -22,7 +22,7 @@ prime-field = { path = "../prime-field", default-features = false } short-weierstrass = { path = "../short-weierstrass", default-features = false } ciphersuite = { path = "../ciphersuite", version = "0.4", default-features = false } -generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "dc1b3dbe436aae61ec363505052d4715d38ce1df", default-features = false, optional = true } +generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15", default-features = false, optional = true } [dev-dependencies] hex = "0.4" diff --git a/crypto/short-weierstrass/Cargo.toml b/crypto/short-weierstrass/Cargo.toml index 573859245..5d96c7f54 100644 --- a/crypto/short-weierstrass/Cargo.toml +++ b/crypto/short-weierstrass/Cargo.toml @@ -21,7 +21,7 @@ rand_core = { version = "0.6", default-features = false } ff = { version = "0.13", default-features = false, features = ["bits"] } group = { version = "0.13", default-features = false } -ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "dc1b3dbe436aae61ec363505052d4715d38ce1df", default-features = false, optional = true } +ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "0d6f5e840ad1f955e4e4dec00c5165f134815b15", default-features = false, optional = true } [features] alloc = ["zeroize/alloc", "rand_core/alloc", "ff/alloc", "group/alloc", "ec-divisors"] diff --git a/deny.toml b/deny.toml index 36ab759d3..adce820f4 100644 --- a/deny.toml +++ b/deny.toml @@ -103,6 +103,7 @@ exceptions = [ { allow = ["AGPL-3.0-only"], name = "serai-orchestrator" }, + { allow = ["AGPL-3.0-only"], name = "serai-shim-rpc" }, { allow = ["AGPL-3.0-only"], name = "serai-docker-tests" }, { allow = ["AGPL-3.0-only"], name = "serai-message-queue-tests" }, { allow = ["AGPL-3.0-only"], name = "serai-processor-tests" }, diff --git a/message-queue/Cargo.toml b/message-queue/Cargo.toml index bb28e85db..19df277fe 100644 --- a/message-queue/Cargo.toml +++ b/message-queue/Cargo.toml @@ -31,10 +31,6 @@ dalek-ff-group = { path = "../crypto/dalek-ff-group", default-features = false, ciphersuite = { path = "../crypto/ciphersuite", default-features = false, features = ["std"] } schnorr-signatures = { path = "../crypto/schnorr", default-features = false, features = ["std"] } -# Application -log = { version = "0.4", default-features = false, features = ["std"] } -env_logger = { version = "0.10", default-features = false, features = ["humantime"] } - # Uses a single threaded runtime since this shouldn't ever be CPU-bound tokio = { version = "1", default-features = false, features = ["rt", "time", "io-util", "net", "macros"] } diff --git a/message-queue/src/client.rs b/message-queue/src/client.rs index b40575f27..459f61148 100644 --- a/message-queue/src/client.rs +++ b/message-queue/src/client.rs @@ -131,15 +131,15 @@ impl MessageQueue { } first = false; - log::trace!("opening socket to message-queue for next"); + serai_env::trace!("opening socket to message-queue for next"); let mut socket = match TcpStream::connect(&self.url).await { Ok(socket) => socket, Err(e) => { - log::warn!("couldn't connect to message-queue server: {e:?}"); + serai_env::warn!("couldn't connect to message-queue server: {e:?}"); continue; } }; - log::trace!("opened socket for next"); + serai_env::trace!("opened socket for next"); loop { if Self::send(&mut socket, msg.clone()).await.is_err() { @@ -148,7 +148,7 @@ impl MessageQueue { let status = match socket.read_u8().await { Ok(status) => status, Err(e) => { - log::warn!("couldn't read status u8: {e:?}"); + serai_env::warn!("couldn't read status u8: {e:?}"); continue 'outer; } }; @@ -168,14 +168,14 @@ impl MessageQueue { let len = match socket.read_u32_le().await { Ok(len) => len, Err(e) => { - log::warn!("couldn't read len: {e:?}"); + serai_env::warn!("couldn't read len: {e:?}"); return vec![]; } }; let mut buf = vec![0; usize::try_from(len).unwrap()]; // Read the message let Ok(_) = socket.read_exact(&mut buf).await else { - log::warn!("couldn't read the message"); + serai_env::warn!("couldn't read the message"); return vec![]; }; buf diff --git a/message-queue/src/main.rs b/message-queue/src/main.rs index b6819f4a5..267ab622b 100644 --- a/message-queue/src/main.rs +++ b/message-queue/src/main.rs @@ -89,7 +89,7 @@ pub(crate) fn queue_message( let mut txn = db.txn(); let intent_key = intent_key(meta.from, meta.to, &meta.intent); if Get::get(&txn, &intent_key).is_some() { - log::warn!( + serai_env::warn!( "Prior queued message attempted to be queued again. From: {:?} To: {:?} Intent: {}", meta.from, meta.to, @@ -111,7 +111,7 @@ pub(crate) fn queue_message( }, ); - log::info!("Queued message. From: {:?} To: {:?} ID: {id}", meta.from, meta.to); + serai_env::info!("Queued message. From: {:?} To: {:?} ID: {id}", meta.from, meta.to); DbTxn::commit(txn); } @@ -148,18 +148,15 @@ pub(crate) fn ack_message(from: Service, to: Service, id: u64, sig: SchnorrSigna // It's the second if we acknowledge messages before saving them as acknowledged // TODO: Check only a proper message is being acked - log::info!("Acknowledging From: {from:?} To: {to:?} ID: {id}"); + serai_env::info!("Acknowledging From: {from:?} To: {to:?} ID: {id}"); QUEUES.read().unwrap()[&(from, to)].write().unwrap().ack_message(id); } #[tokio::main(flavor = "current_thread")] async fn main() { - // TODO: `env_logger::Env` for `serai-env` and `Builder::from_env`? - env_logger::Builder::from_default_env() - .parse_filters(&serai_env::var("RUST_LOG").unwrap_or_else(|| "info".to_owned())) - .init(); - log::info!("Starting message-queue service..."); + serai_env::init_logger(); + serai_env::info!("Starting message-queue service..."); // Open the DB #[expect(unused_variables, unreachable_code)] diff --git a/networks/ethereum/relayer/Cargo.toml b/networks/ethereum/relayer/Cargo.toml index 89d8e99e7..d2720f6c0 100644 --- a/networks/ethereum/relayer/Cargo.toml +++ b/networks/ethereum/relayer/Cargo.toml @@ -17,9 +17,6 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [dependencies] -log = { version = "0.4", default-features = false, features = ["std"] } -env_logger = { version = "0.10", default-features = false, features = ["humantime"] } - tokio = { version = "1", default-features = false, features = ["rt", "time", "io-util", "net", "macros"] } serai-env = { path = "../../../common/env" } diff --git a/networks/ethereum/relayer/src/main.rs b/networks/ethereum/relayer/src/main.rs index 1290ae089..de7d4f51a 100644 --- a/networks/ethereum/relayer/src/main.rs +++ b/networks/ethereum/relayer/src/main.rs @@ -1,5 +1,3 @@ -use core::str::FromStr as _; - pub(crate) use tokio::{ io::{AsyncReadExt as _, AsyncWriteExt as _}, net::TcpListener, @@ -9,13 +7,8 @@ use serai_db::{Get as _, DbTxn as _, Db as _}; #[tokio::main(flavor = "current_thread")] async fn main() { - env_logger::builder() - .filter_level( - log::LevelFilter::from_str(&serai_env::var("RUST_LOG").unwrap_or_else(|| "info".to_owned())) - .expect("`RUST_LOG` environment variable had an invalid filter"), - ) - .init(); - log::info!("Starting Ethereum relayer server..."); + serai_env::init_logger(); + serai_env::info!("Starting Ethereum relayer server..."); // Open the DB #[expect(unused_variables, unreachable_code)] @@ -59,7 +52,7 @@ async fn main() { let Ok(()) = socket.write_all(&[1]).await else { break }; - log::info!("received transaction to publish (nonce {nonce})"); + serai_env::info!("received transaction to publish (nonce {nonce})"); } }); } diff --git a/networks/ethereum/schnorr/Cargo.toml b/networks/ethereum/schnorr/Cargo.toml index f927557d5..1300308ad 100644 --- a/networks/ethereum/schnorr/Cargo.toml +++ b/networks/ethereum/schnorr/Cargo.toml @@ -19,9 +19,9 @@ workspace = true std-shims = { path = "../../../common/std-shims", version = "0.1", default-features = false } subtle = { version = "2", default-features = false } -sha3 = { version = "0.10", default-features = false } +sha3 = { version = "0.11", default-features = false } group = { version = "0.13", default-features = false } -k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic"] } +k256 = { version = "0.13.1", default-features = false, features = ["arithmetic"] } [build-dependencies] build-solidity-contracts = { path = "../build-contracts", version = "0.1" } @@ -29,7 +29,7 @@ build-solidity-contracts = { path = "../build-contracts", version = "0.1" } [dev-dependencies] rand_core = { version = "0.6", default-features = false, features = ["std"] } -k256 = { version = "^0.13.1", default-features = false, features = ["ecdsa"] } +k256 = { version = "0.13.1", default-features = false, features = ["ecdsa"] } alloy-core = { version = "1", default-features = false } alloy-sol-types = { version = "1", default-features = false } @@ -44,6 +44,6 @@ alloy-node-bindings = { version = "1", default-features = false } tokio = { version = "1", default-features = false, features = ["macros"] } [features] -alloc = ["std-shims/alloc", "group/alloc"] -std = ["alloc", "std-shims/std", "subtle/std", "sha3/std", "k256/std"] +alloc = ["std-shims/alloc", "sha3/alloc", "group/alloc"] +std = ["alloc", "std-shims/std", "subtle/std", "k256/std"] default = ["std"] diff --git a/networks/ethereum/schnorr/src/tests/premise.rs b/networks/ethereum/schnorr/src/tests/premise.rs index dc753d6a4..2d520ca55 100644 --- a/networks/ethereum/schnorr/src/tests/premise.rs +++ b/networks/ethereum/schnorr/src/tests/premise.rs @@ -35,18 +35,19 @@ fn test_ecrecover() { // Sign the signature const MESSAGE: &[u8] = b"Hello, World!"; + let hashed_message = <[u8; 32]>::from(Keccak256::digest(MESSAGE)); let (sig, recovery_id) = private .as_nonzero_scalar() - .try_sign_prehashed(Scalar::random(&mut OsRng), &Keccak256::digest(MESSAGE)) + .try_sign_prehashed(Scalar::random(&mut OsRng), &hashed_message.into()) .unwrap(); // Sanity check the signature verifies - assert_eq!(public.verify_prehash(&Keccak256::digest(MESSAGE), &sig).unwrap(), ()); + assert_eq!(public.verify_prehash(&hashed_message, &sig).unwrap(), ()); // Perform the ecrecover assert_eq!( ecrecover( - >::reduce_bytes(&Keccak256::digest(MESSAGE)), + >::reduce_bytes(&hashed_message.into()), u8::from(recovery_id.unwrap().is_y_odd()) == 1, *sig.r(), *sig.s() diff --git a/patches/dalek-ff-group/LICENSE b/patches/LICENSE similarity index 96% rename from patches/dalek-ff-group/LICENSE rename to patches/LICENSE index 2abd038b4..a63b9a474 100644 --- a/patches/dalek-ff-group/LICENSE +++ b/patches/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022-2026 Serai Contributors +Copyright (c) 2023-2026 Serai Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 000000000..7e2967f7c --- /dev/null +++ b/patches/README.md @@ -0,0 +1,10 @@ +# Patches + +These patches are to improve Serai's supply chain in various manners. Some +patches update to a crate's latest version (after reviewing no breaking changes +occurred) to de-duplicate our tree, some patches consolidate dependencies +around equivalents, and some are stubs to prove they aren't actually compiled +(allowing pruning our `Cargo.lock` of their sub-tree). + +The reasons for each patch are included with the associated entry within +`Cargo.toml`. diff --git a/patches/core2/Cargo.toml b/patches/core2/Cargo.toml new file mode 100644 index 000000000..985b13eaa --- /dev/null +++ b/patches/core2/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "core2" +version = "0.4.99" +description = "core2 which patches to std-shims" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/patches/core2" +authors = ["Luke Parker "] +keywords = ["nostd", "no_std", "alloc", "io"] +edition = "2021" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[workspace] + +[dependencies] +std-shims = { path = "../../common/std-shims", default-features = false } + +[features] +alloc = ["std-shims/alloc"] +std = ["std-shims/std"] +default = ["std"] diff --git a/patches/core2/src/lib.rs b/patches/core2/src/lib.rs new file mode 100644 index 000000000..b7708f85a --- /dev/null +++ b/patches/core2/src/lib.rs @@ -0,0 +1,3 @@ +#![no_std] + +pub use std_shims::*; diff --git a/patches/darling/Cargo.toml b/patches/cpufeatures/Cargo.toml similarity index 78% rename from patches/darling/Cargo.toml rename to patches/cpufeatures/Cargo.toml index 357c34116..0616ff428 100644 --- a/patches/darling/Cargo.toml +++ b/patches/cpufeatures/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "darling" -version = "0.20.99" +name = "cpufeatures" +version = "0.2.99" description = "Patch to the latest version" license = "MIT" -repository = "https://github.com/serai-dex/serai/tree/develop/patches/darling" +repository = "https://github.com/serai-dex/serai/tree/develop/patches/cpufeatures" authors = ["Luke Parker "] keywords = [] edition = "2021" @@ -15,4 +15,4 @@ rustdoc-args = ["--cfg", "docsrs"] [workspace] [dependencies] -darling = { version = "0.21" } +cpufeatures = { version = "0.3" } diff --git a/patches/cpufeatures/src/lib.rs b/patches/cpufeatures/src/lib.rs new file mode 100644 index 000000000..55348105c --- /dev/null +++ b/patches/cpufeatures/src/lib.rs @@ -0,0 +1 @@ +pub use cpufeatures::*; diff --git a/patches/dalek-ff-group/Cargo.toml b/patches/dalek-ff-group/Cargo.toml deleted file mode 100644 index c9bc1c6c3..000000000 --- a/patches/dalek-ff-group/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "dalek-ff-group" -version = "0.5.99" -description = "ff/group bindings around curve25519-dalek" -license = "MIT" -repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dalek-ff-group" -authors = ["Luke Parker "] -keywords = ["curve25519", "ed25519", "ristretto", "dalek", "group"] -edition = "2021" - -[package.metadata.docs.rs] -all-features = true -rustdoc-args = ["--cfg", "docsrs"] - -[workspace] - -[dependencies] -dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false } - -crypto-bigint-05 = { package = "crypto-bigint", version = "0.5", default-features = false, features = ["zeroize"] } -crypto-bigint = { version = "0.7", default-features = false, features = ["zeroize"] } -prime-field = { path = "../../crypto/prime-field", default-features = false } - -[features] -alloc = ["dalek-ff-group/alloc", "prime-field/alloc"] -std = ["alloc", "dalek-ff-group/std", "prime-field/std"] -default = ["std"] diff --git a/patches/dalek-ff-group/README.md b/patches/dalek-ff-group/README.md deleted file mode 100644 index 5a25d05bf..000000000 --- a/patches/dalek-ff-group/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Dalek FF/Group - -Patch for the `crates.io` `dalek-ff-group` to use the in-tree `dalek-ff-group`, -resolving relevant breaking changes made since. diff --git a/patches/dalek-ff-group/src/lib.rs b/patches/dalek-ff-group/src/lib.rs deleted file mode 100644 index 875519609..000000000 --- a/patches/dalek-ff-group/src/lib.rs +++ /dev/null @@ -1,43 +0,0 @@ -#![allow(deprecated)] -#![cfg_attr(docsrs, feature(doc_cfg))] -#![no_std] // Prevents writing new code, in what should be a simple wrapper, which requires std -#![doc = include_str!("../README.md")] -#![allow(clippy::redundant_closure_call)] - -pub use dalek_ff_group::{Scalar, EdwardsPoint, RistrettoPoint, Ed25519, Ristretto}; - -type ThirtyTwoArray = [u8; 32]; -prime_field::odd_prime_field_with_specific_repr!( - FieldElement, - "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed", - "02", - false, - crate::ThirtyTwoArray -); - -impl FieldElement { - /// Create a FieldElement from a `crypto_bigint::U256`. - /// - /// This will reduce the `U256` by the modulus, into a member of the field. - #[deprecated] - pub const fn from_u256(u256: &crypto_bigint_05::U256) -> Self { - const MODULUS: crypto_bigint::U256 = crypto_bigint::U256::from_be_hex( - "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed", - ); - let mut u256 = crypto_bigint::U256::from_words(*u256.as_words()); - loop { - let result = FieldElement::from_bytes(&{ - // This is an `EncodedUint` - let encoded_uint = u256.to_le_bytes(); - // The conversion to `[u8; 32]` is not provided in a `const` context, so we do it ourselves - let mut bytes = [0; 32]; - bytes.copy_from_slice(encoded_uint.as_slice()); - bytes - }); - if let Some(result) = result { - return result; - } - u256 = u256.wrapping_sub(&MODULUS); - } - } -} diff --git a/patches/darling/src/lib.rs b/patches/darling/src/lib.rs deleted file mode 100644 index 62d918082..000000000 --- a/patches/darling/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub use darling::*; diff --git a/patches/hex_lit/Cargo.toml b/patches/hex_lit/Cargo.toml new file mode 100644 index 000000000..bc3dc8e3b --- /dev/null +++ b/patches/hex_lit/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "hex_lit" +version = "0.1.99" +description = "hex_lit which patches to hex-literal" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/patches/hex_lit" +authors = ["Luke Parker "] +keywords = ["nostd", "no_std", "alloc", "io"] +edition = "2021" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[workspace] + +[dependencies] +hex-literal = { version = "1", default-features = false } + +[features] diff --git a/patches/hex_lit/src/lib.rs b/patches/hex_lit/src/lib.rs new file mode 100644 index 000000000..8c6eb1861 --- /dev/null +++ b/patches/hex_lit/src/lib.rs @@ -0,0 +1,3 @@ +#![no_std] + +pub use hex_literal::*; diff --git a/patches/indexmap/Cargo.toml b/patches/indexmap/Cargo.toml new file mode 100644 index 000000000..d15bc6a19 --- /dev/null +++ b/patches/indexmap/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "indexmap" +version = "1.99.99" +description = "Patch to an empty crate" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/patches/indexmap" +authors = ["Luke Parker "] +keywords = [] +edition = "2021" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[workspace] + +[features] +std = [] +serde-1 = [] diff --git a/patches/indexmap/src/lib.rs b/patches/indexmap/src/lib.rs new file mode 100644 index 000000000..0670b71f6 --- /dev/null +++ b/patches/indexmap/src/lib.rs @@ -0,0 +1 @@ +const _NEVER_COMPILED: [(); 0 - 1] = [(); 0 - 1]; diff --git a/patches/keccak-asm/Cargo.toml b/patches/keccak-asm/Cargo.toml index 1afe6d951..f6c554460 100644 --- a/patches/keccak-asm/Cargo.toml +++ b/patches/keccak-asm/Cargo.toml @@ -15,8 +15,8 @@ rustdoc-args = ["--cfg", "docsrs"] [workspace] [dependencies] -sha3 = { version = "0.10", default-features = false } +sha3 = { version = "0.11", default-features = false } [features] zeroize = ["sha3/zeroize"] -std = ["sha3/std"] +std = ["sha3/alloc"] diff --git a/patches/lazy_static/Cargo.toml b/patches/lazy_static/Cargo.toml index aaaa18697..e2560628e 100644 --- a/patches/lazy_static/Cargo.toml +++ b/patches/lazy_static/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lazy_static" -version = "1.99.0" +version = "1.99.99" description = "`lazy_static` which patches to `std::sync::LazyLock`" license = "MIT" repository = "https://github.com/serai-dex/serai/tree/develop/patches/lazy_static" diff --git a/patches/nonzero_ext/Cargo.toml b/patches/nonzero_ext/Cargo.toml new file mode 100644 index 000000000..3b01744ed --- /dev/null +++ b/patches/nonzero_ext/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nonzero_ext" +version = "0.3.99" +description = "Minimal re-implementation of the necessary subset of `nonzero_ext`" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/patches/nonzero_ext" +authors = ["Luke Parker "] +keywords = [] +edition = "2021" +rust-version = "1.83" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[workspace] + +[features] +std = [] diff --git a/patches/nonzero_ext/src/lib.rs b/patches/nonzero_ext/src/lib.rs new file mode 100644 index 000000000..027b5ba66 --- /dev/null +++ b/patches/nonzero_ext/src/lib.rs @@ -0,0 +1,10 @@ +#![no_std] + +#[macro_export] +macro_rules! nonzero { + ($n: literal) => { + const { + core::num::NonZero::new($n).unwrap() + } + } +} diff --git a/patches/option-ext/Cargo.toml b/patches/option-ext/Cargo.toml index bd6635eb8..c7c8a11ca 100644 --- a/patches/option-ext/Cargo.toml +++ b/patches/option-ext/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "option-ext" -version = "0.2.0" +version = "0.2.99" description = "Non-MPL option-ext with only the API necessary for directories" license = "MIT" repository = "https://github.com/serai-dex/serai/tree/develop/patches/option-ext" diff --git a/patches/schemars-09/Cargo.toml b/patches/schemars-09/Cargo.toml new file mode 100644 index 000000000..7f6e6b9b8 --- /dev/null +++ b/patches/schemars-09/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "schemars" +version = "0.9.99" +description = "Patch to an empty crate" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/patches/schemars-09" +authors = ["Luke Parker "] +keywords = [] +edition = "2021" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[workspace] + +[features] +std = [] diff --git a/patches/schemars-09/src/lib.rs b/patches/schemars-09/src/lib.rs new file mode 100644 index 000000000..0670b71f6 --- /dev/null +++ b/patches/schemars-09/src/lib.rs @@ -0,0 +1 @@ +const _NEVER_COMPILED: [(); 0 - 1] = [(); 0 - 1]; diff --git a/patches/schemars-10/Cargo.toml b/patches/schemars-10/Cargo.toml new file mode 100644 index 000000000..0fc890379 --- /dev/null +++ b/patches/schemars-10/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "schemars" +version = "1.99.99" +description = "Patch to an empty crate" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/patches/schemars-10" +authors = ["Luke Parker "] +keywords = [] +edition = "2021" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[workspace] + +[features] +std = [] diff --git a/patches/schemars-10/src/lib.rs b/patches/schemars-10/src/lib.rs new file mode 100644 index 000000000..0670b71f6 --- /dev/null +++ b/patches/schemars-10/src/lib.rs @@ -0,0 +1 @@ +const _NEVER_COMPILED: [(); 0 - 1] = [(); 0 - 1]; diff --git a/processor/bin/Cargo.toml b/processor/bin/Cargo.toml index b91a9973b..7814837f4 100644 --- a/processor/bin/Cargo.toml +++ b/processor/bin/Cargo.toml @@ -28,8 +28,6 @@ dkg = { package = "dkg-evrf", path = "../../crypto/dkg/evrf", default-features = serai-primitives = { path = "../../substrate/primitives", default-features = false, features = ["std"] } serai-cosign = { package = "serai-cosign-types", path = "../../coordinator/cosign/types" } -log = { version = "0.4", default-features = false, features = ["std"] } -env_logger = { version = "0.10", default-features = false, features = ["humantime"] } tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] } serai-env = { path = "../../common/env" } diff --git a/processor/bin/src/lib.rs b/processor/bin/src/lib.rs index 1a4289173..c6f0ac2e7 100644 --- a/processor/bin/src/lib.rs +++ b/processor/bin/src/lib.rs @@ -1,6 +1,6 @@ #![allow(clippy::std_instead_of_alloc, clippy::std_instead_of_core)] -use core::{cmp::Ordering, str::FromStr as _}; +use core::cmp::Ordering; use zeroize::{Zeroize as _, Zeroizing}; @@ -47,13 +47,8 @@ pub type Db = serai_db::RocksDB; /// /// Yields the database. pub fn init() -> Db { - env_logger::builder() - .filter_level( - log::LevelFilter::from_str(&serai_env::var("RUST_LOG").unwrap_or_else(|| "info".to_owned())) - .expect("`RUST_LOG` environment variable had an invalid filter"), - ) - .init(); - log::info!("Starting processor service..."); + serai_env::init_logger(); + serai_env::info!("Starting processor service..."); #[cfg(all(feature = "parity-db", not(feature = "rocksdb")))] let db = @@ -137,10 +132,14 @@ async fn first_block_after_time(feed: &S, serai_time: u64) -> u6 match first_block_after_time_iteration(feed, serai_time).await { Ok(Some(block)) => return block, Ok(None) => { - log::info!("waiting for block to activate at (a block with timestamp >= {serai_time})"); + serai_env::info!( + "waiting for block to activate at (a block with timestamp >= {serai_time})" + ); } Err(e) => { - log::error!("couldn't find the first block Serai should scan due to an RPC error: {e:?}"); + serai_env::error!( + "couldn't find the first block Serai should scan due to an RPC error: {e:?}" + ); } } tokio::time::sleep(core::time::Duration::from_secs(5)).await; diff --git a/processor/ethereum/Cargo.toml b/processor/ethereum/Cargo.toml index 1d510e3fd..357411b4e 100644 --- a/processor/ethereum/Cargo.toml +++ b/processor/ethereum/Cargo.toml @@ -43,7 +43,6 @@ serai-primitives = { path = "../../substrate/primitives", default-features = fal serai-client-ethereum = { path = "../../substrate/client/ethereum", default-features = false } zalloc = { path = "../../common/zalloc" } -log = { version = "0.4", default-features = false, features = ["std"] } tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "sync", "time", "macros"] } serai-env = { path = "../../common/env" } diff --git a/processor/ethereum/src/main.rs b/processor/ethereum/src/main.rs index d9f7197da..59f6ce690 100644 --- a/processor/ethereum/src/main.rs +++ b/processor/ethereum/src/main.rs @@ -71,7 +71,7 @@ async fn main() { match provider.get_chain_id().await { Ok(chain_id) => break chain_id, Err(e) => { - log::error!("failed to fetch the chain ID on boot: {e:?}"); + serai_env::error!("failed to fetch the chain ID on boot: {e:?}"); tokio::time::sleep(delay).await; delay = (delay + Duration::from_secs(5)).max(Duration::from_mins(2)); } diff --git a/substrate/node/Cargo.toml b/substrate/node/Cargo.toml index bf1ab9d8b..38016f2b0 100644 --- a/substrate/node/Cargo.toml +++ b/substrate/node/Cargo.toml @@ -23,7 +23,6 @@ rand_core = "0.6" zeroize = "1" base16ct = { version = "1", default-features = false, features = ["alloc"] } hex = "0.4" -log = "0.4" schnorrkel = "0.11" @@ -44,6 +43,7 @@ sp-keystore = { git = "https://github.com/serai-dex/patch-polkadot-sdk" } sp-io = { git = "https://github.com/serai-dex/patch-polkadot-sdk" } sp-runtime = { git = "https://github.com/serai-dex/patch-polkadot-sdk" } sp-consensus-babe = { git = "https://github.com/serai-dex/patch-polkadot-sdk" } +sp-tracing = { git = "https://github.com/serai-dex/patch-polkadot-sdk" } frame-benchmarking = { git = "https://github.com/serai-dex/patch-polkadot-sdk" } diff --git a/substrate/node/src/chain_spec.rs b/substrate/node/src/chain_spec.rs index 05d4e505e..b751e4a46 100644 --- a/substrate/node/src/chain_spec.rs +++ b/substrate/node/src/chain_spec.rs @@ -94,13 +94,13 @@ fn wasm_binary(dev: bool) -> Vec { const DEFAULT_WASM_PATH: &str = "/runtime/serai.wasm"; let path = serai_env::var("SERAI_WASM_PATH").unwrap_or(DEFAULT_WASM_PATH.to_owned()); if let Ok(binary) = fs::read(&path) { - log::info!("using {path} for the WASM"); + sp_tracing::info!("using {path} for the WASM"); return binary; } assert!(dev, "could not read WASM for the runtime and this is not a dev network"); - log::info!("using built-in wasm"); + sp_tracing::info!("using built-in wasm"); serai_runtime::WASM.to_vec() } diff --git a/substrate/node/src/rpc/p2p_validators.rs b/substrate/node/src/rpc/p2p_validators.rs index 7b9e6e683..105225435 100644 --- a/substrate/node/src/rpc/p2p_validators.rs +++ b/substrate/node/src/rpc/p2p_validators.rs @@ -4,7 +4,7 @@ pub(crate) fn module( bootnodes: &[MultiaddrWithPeerId], client: Arc, authority_discovery: sc_authority_discovery::Service, -) -> Result, Box> { +) -> Result, Box> { let bootnodes = bootnodes.iter().map(ToString::to_string).collect::>(); let mut module = RpcModule::new((bootnodes, client, tokio::sync::RwLock::new(authority_discovery))); diff --git a/substrate/primitives/Cargo.toml b/substrate/primitives/Cargo.toml index e356364de..06cec625e 100644 --- a/substrate/primitives/Cargo.toml +++ b/substrate/primitives/Cargo.toml @@ -64,4 +64,6 @@ std = [ "bech32/std" ] scale = ["dep:scale", "sp-core/serde"] +# This feature is not intended for public consumption and is not covered by any guarantees (SemVer, safety) +test-helpers = ["std"] default = ["std"] diff --git a/substrate/primitives/src/dex/mod.rs b/substrate/primitives/src/dex/mod.rs index df4a7324f..b640864ce 100644 --- a/substrate/primitives/src/dex/mod.rs +++ b/substrate/primitives/src/dex/mod.rs @@ -1,5 +1,3 @@ -use borsh::{BorshDeserialize, BorshSerialize}; - use crate::{ coin::{ExternalCoin, Coin}, balance::Amount, @@ -61,15 +59,13 @@ impl Premise { } /// The reserves for a liquidity pool. -#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Reserves { /// The amount of SRI already present. pub sri: Amount, /// The amount of the external coin already present. pub external_coin: Amount, } -#[cfg(feature = "scale")] -crate::borsh_as_scale!(Reserves); impl Reserves { /// The product of two amounts. diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index 72b36d2d0..e58c516b6 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -53,6 +53,11 @@ pub mod instructions; /// Merkle trees. pub mod merkle; +/// Test helpers for generating random instances of primitive types. +#[cfg(any(test, feature = "test-helpers"))] +#[doc(hidden)] +pub mod test_helpers; + /// The type used to identify block numbers. /// /// A block's number is its zero-indexed position on the list of blocks which form a blockchain. @@ -75,7 +80,7 @@ impl From for BlockNumber { level so this is fine for our use-case. If we do ever see a 64-byte block hash, we can simply hash it into a 32-byte hash or truncate it. */ -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize)] pub struct BlockHash(pub [u8; 32]); #[cfg(feature = "scale")] crate::borsh_as_scale!(BlockHash); diff --git a/substrate/primitives/src/test_helpers.rs b/substrate/primitives/src/test_helpers.rs new file mode 100644 index 000000000..9113bad50 --- /dev/null +++ b/substrate/primitives/src/test_helpers.rs @@ -0,0 +1,77 @@ +//! Test helpers for generating random instances of primitive types. + +use alloc::vec; + +use rand_core::{RngCore, CryptoRng}; + +use crate::{ + BlockHash, + address::{SeraiAddress, ExternalAddress}, + crypto::{Public, ExternalKey}, +}; + +/// Generate a random 32-byte array. +pub fn random_bytes_32(rng: &mut R) -> [u8; 32] { + let mut bytes = [0u8; 32]; + rng.fill_bytes(&mut bytes); + bytes +} + +/// Generate a random 64-byte array. +pub fn random_bytes_64(rng: &mut R) -> [u8; 64] { + let mut bytes = [0u8; 64]; + rng.fill_bytes(&mut bytes); + bytes +} + +/// Generate a random [`ExternalAddress`]. +pub fn random_external_address(rng: &mut R) -> ExternalAddress { + let len = usize::try_from(rng.next_u32() % ExternalAddress::MAX_SIZE).unwrap(); + let mut external_address = vec![0; len]; + rng.fill_bytes(&mut external_address); + ExternalAddress::try_from(external_address).unwrap() +} + +#[test] +fn random_external_address_is_in_range() { + for _ in 0 .. (128 * ExternalAddress::MAX_SIZE) { + random_external_address(&mut rand_core::OsRng); + } +} + +/// Generate a random [`SeraiAddress`]. +pub fn random_serai_address(rng: &mut R) -> SeraiAddress { + SeraiAddress(random_bytes_32(rng)) +} + +/// Generate a random [`Public`]. +pub fn random_public(rng: &mut R) -> Public { + Public(random_bytes_32(rng)) +} + +/// Generate a random schnorrkel keypair and its [`Public`] wrapper. +pub fn random_keypair(rng: &mut R) -> (schnorrkel::Keypair, Public) { + let keypair = schnorrkel::Keypair::generate_with(rng); + let public = Public(keypair.public.to_bytes()); + (keypair, public) +} + +/// Generate a random [`ExternalKey`]. +pub fn random_external_key(rng: &mut R) -> ExternalKey { + let len = usize::try_from(rng.next_u32() % ExternalKey::MAX_SIZE).unwrap(); + let mut external_key = vec![0; len]; + rng.fill_bytes(&mut external_key); + ExternalKey(external_key.try_into().unwrap()) +} + +#[test] +fn random_external_key_is_in_range() { + for _ in 0 .. (128 * ExternalKey::MAX_SIZE) { + random_external_key(&mut rand_core::OsRng); + } +} + +/// Generate a random [`BlockHash`]. +pub fn random_block_hash(rng: &mut R) -> BlockHash { + BlockHash(random_bytes_32(rng)) +} diff --git a/substrate/runtime/src/common.rs b/substrate/runtime/src/common.rs index ab9064090..0ccd36533 100644 --- a/substrate/runtime/src/common.rs +++ b/substrate/runtime/src/common.rs @@ -3,7 +3,7 @@ use serai_abi::primitives::{ crypto::{EmbeddedEllipticCurveKeys, SignedEmbeddedEllipticCurveKeys, KeyPair}, network_id::NetworkId, validator_sets::{Session, ExternalValidatorSet}, - coin::{Coin, ExternalCoin}, + coin::{ExternalCoin, Coin}, balance::{Amount, Balance}, address::SeraiAddress, }; diff --git a/supply-chain/LICENSE b/supply-chain/LICENSE new file mode 100644 index 000000000..b51e2b621 --- /dev/null +++ b/supply-chain/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Serai Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/supply-chain/README.md b/supply-chain/README.md new file mode 100644 index 000000000..48a3b5708 --- /dev/null +++ b/supply-chain/README.md @@ -0,0 +1,40 @@ +# `cargo vet` Statements for Serai's Supply Chain + +This folder contains the `cargo vet` statements for Serai's supply chain, an +active [work in progress](https://github.com/serai-dex/serai/issues/315) for +Serai. + +The primary goal for Serai to reduce its supply-chain risk is via minimizing +the supply chain itself. While high-quality dependencies are better than +low-quality in-house replacements, dependencies should not be eagerly reached +for. For more information on this goal specifically, please see +[`patches/`](/patches). + +For the rest of our supply chain, we are working towards comprehensive vetting +statements. + +### Installation + +At this time, Serai makes use of a fork of `cargo-vet` with support for declaring +non-`crates.io` dependencies as third party and therefore requiring vet +statements. This is due to Serai's non-trivial use of `git` dependencies. For +context, please see https://github.com/mozilla/cargo-vet/issues/683. + +``` +cargo install --locked cargo-vet --git https://github.com/kayabaNerve/cargo-vet --rev d2fb27daaeb839e5fa4f6b28c5cdd4a9185542b5 +``` + +### Policies + +- `audits-as-crates-io` must only be explicitly set to `false` for crates + within this repository. +- `safe-to-run`, `safe-to-deploy` mean the code has been reviewed to not be + actively malicious and to not contain any binary artifacts. It does not mean + they have been professionally audited nor reviewed to lack bugs/security + issues. +- `reviewed` means the code has been reviewed for security issues and the + associated bugs, and found in generally good standing, even if not free of + problems/concerns. +- `audited` means the code has been professionally reviewed for security issues + and bugs at some point during its life. It is used for metadata and is not + actively explicitly required for any dependencies. diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml new file mode 100644 index 000000000..5a18b15c4 --- /dev/null +++ b/supply-chain/audits.toml @@ -0,0 +1,542 @@ + +# cargo-vet audits file + +[criteria.audited] +description = "This crate was professionally audited." + +[criteria.reviewed] +description = "This crate was reviewed by a member of Serai and found to be in good standing." +implies = "safe-to-deploy" + +[[audits.ark-ff]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.3.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.ark-ff]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.4.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.aurora-engine-modexp]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "1.99.99" +notes = "Re-implementation of the necessary subset of the `aurora-engine-modexp` API to consolidate arithmetic implementations" + +[[audits.c-kzg]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "2.99.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.ciphersuite]] +who = "Luke Parker " +criteria = "audited" +version = "0.2.0" +notes = "`/audits/crypto/Cypher Stack March 2023`, excluding the Ed448 ciphersuite" + +[[audits.const-random]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.1.18" +notes = "While this is as it says, a separate question is if anyone should use this" + +[[audits.const-random-macro]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.1.16" +notes = "While this is as it says, a separate question is if anyone should use this" + +[[audits.core-json]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.4.0" +notes = "Written by kayabaNerve" + +[[audits.core-json-derive]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.4.1" +notes = "Written by kayabaNerve" + +[[audits.core-json-traits]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.4.1" +notes = "Written by kayabaNerve" + +[[audits.cpufeatures]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.2.99" +notes = "Patches from `0.2` to `0.3` whose only breaking change was a MSRV bump" + +[[audits.crypto-bigint]] +who = "Luke Parker " +criteria = "audited" +version = "0.5.3" +notes = "https://www.nccgroup.com/research/public-report-entropyrust-cryptography-review reviewed 0.5.2, with the fixes (and some other, presumably unaudited commits) released with 0.5.3" + +[[audits.dalek-ff-group]] +who = "Luke Parker " +criteria = "audited" +version = "0.2.0" +notes = "`/audits/crypto/Cypher Stack March 2023`" + +[[audits.dkg]] +who = "Luke Parker " +criteria = "audited" +version = "0.3.0" +notes = "`/audits/crypto/Cypher Stack March 2023`" + +[[audits.dtoa]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "1.99.99" +notes = "Patches to `zmij` which is by the same author, faster, and has an identical API" + +[[audits.ec-divisors]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.1.0@git:0d6f5e840ad1f955e4e4dec00c5165f134815b15" +importable = false + +[[audits.exit-future]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.2.0" + +[[audits.fastrlp]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.3.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.fastrlp]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.4.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.ff-group-tests]] +who = "Luke Parker " +criteria = "audited" +version = "0.12.1" +notes = "`/audits/crypto/Cypher Stack March 2023`" + +[[audits.flexible-transcript]] +who = "Luke Parker " +criteria = "audited" +version = "0.3.0" +notes = "`/audits/crypto/Cypher Stack March 2023`" + +[[audits.generalized-bulletproofs]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.1.0@git:0d6f5e840ad1f955e4e4dec00c5165f134815b15" +importable = false +notes = "Written by kayabaNerve. The ZK proof itself needs further review" + +[[audits.generalized-bulletproofs-circuit-abstraction]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.1.0@git:0d6f5e840ad1f955e4e4dec00c5165f134815b15" +importable = false +notes = "Written by kayabaNerve" + +[[audits.generalized-bulletproofs-ec-gadgets]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.1.0@git:0d6f5e840ad1f955e4e4dec00c5165f134815b15" +importable = false +notes = "Written by kayabaNerve" + +[[audits.impl-codec]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.7.1" + +[[audits.indexmap]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "1.99.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.k256]] +who = "Luke Parker " +criteria = "audited" +version = "0.13.2" +notes = "https://www.nccgroup.com/research/public-report-entropyrust-cryptography-review reviewed 0.13.1, with the fixes (and some other, presumably unaudited commits) released with 0.13.2" + +[[audits.lazy_static]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "1.99.99" +notes = "Re-implementation of a sufficient approximation of the `lazy_static` API around `std::sync::LazyLock`" + +[[audits.modular-frost]] +who = "Luke Parker " +criteria = "audited" +version = "0.6.0" +notes = "`/audits/crypto/Cypher Stack March 2023`, excluding the Ed448 ciphersuite" + +[[audits.monero-address]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-base58]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-borromean]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-bulletproofs]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-bulletproofs-generators]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-clsag]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-daemon-rpc]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-ed25519]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-epee]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.2.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-interface]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-io]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-mlsag]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-oxide]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.4-alpha@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-primitives]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-simple-request-rpc]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.monero-wallet]] +who = "Luke Parker " +criteria = "reviewed" +version = "0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47" +importable = false +notes = "kayabaNerve is a maintainer of `monero-oxide`" + +[[audits.multiexp]] +who = "Luke Parker " +criteria = "audited" +version = "0.3.0" +notes = "`/audits/crypto/Cypher Stack March 2023`" + +[[audits.nonzero_ext]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.3.99" + +[[audits.num-conv]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.2.1" + +[[audits.opaque-debug]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.3.1" + +[[audits.option-ext]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.2.99" +notes = "MIT re-implementation of the necessary subset of the copyleft `option-ext`" + +[[audits.primitive-types]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.12.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.rapidhash]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "4.99.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.rlp]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.5.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.ryu]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "1.99.99" +notes = "Patches to `zmij` which is by the same author, faster, and has a nearly-identical API. The unsafe `raw` module is omitted, but the fact this codebase compiles proves it had no active consumers" + +[[audits.schemars]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.9.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.schemars]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "1.99.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.schnorr-signatures]] +who = "Luke Parker " +criteria = "audited" +version = "0.3.0" +notes = "`/audits/crypto/Cypher Stack March 2023`" + +[[audits.secp256k1]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.30.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[audits.stable_deref_trait]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "1.2.1" + +[[audits.want]] +who = "Luke Parker " +criteria = "safe-to-run" +version = "0.3.1" +notes = "Undocumented panic prevents `safe-to-deploy`, see https://github.com/seanmonstar/want/pull/5" + +[[audits.wit-bindgen-rust-macro]] +who = "Luke Parker " +criteria = "safe-to-deploy" +version = "0.51.99" +notes = "Stub which never compiles to manually trim its subtree from our `Cargo.lock`" + +[[trusted.windows]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-01-15" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-collections]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2025-02-06" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-core]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-11-15" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-future]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2025-02-10" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-implement]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-01-27" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-interface]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-02-18" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-link]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2024-07-17" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-numerics]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2023-05-15" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-registry]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2024-02-15" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-result]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2024-02-02" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-strings]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2024-02-02" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-sys]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-11-15" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-targets]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-09-09" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows-threading]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2025-04-29" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows_aarch64_gnullvm]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-09-01" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows_aarch64_msvc]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-11-05" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows_i686_gnu]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-28" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows_i686_gnullvm]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2024-04-02" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows_i686_msvc]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-27" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows_x86_64_gnu]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-28" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows_x86_64_gnullvm]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-09-01" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" + +[[trusted.windows_x86_64_msvc]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-27" +end = "2027-04-06" +notes = "Trusted as reasonable and due to infeasibility of auditing" diff --git a/supply-chain/config.toml b/supply-chain/config.toml new file mode 100644 index 000000000..9ca442aa1 --- /dev/null +++ b/supply-chain/config.toml @@ -0,0 +1,3776 @@ + +# cargo-vet config file + +[cargo-vet] +version = "0.10" + +[policy."alloy-eip2124:0.2.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy.alloy-simple-request-transport] +audit-as-crates-io = false +first-party = true + +[policy."ark-ff:0.3.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."ark-ff:0.4.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."aurora-engine-modexp:1.99.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy.bitcoin-serai] +audit-as-crates-io = false +first-party = true + +[policy.build-solidity-contracts] +audit-as-crates-io = false +first-party = true + +[policy."c-kzg:2.99.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy.ciphersuite-kp256] +audit-as-crates-io = false +first-party = true + +[policy."ciphersuite:0.4.2"] +audit-as-crates-io = false +first-party = true + +[policy."ciphersuite:0.4.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."core2:0.4.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."cpufeatures:0.2.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy.dalek-ff-group] +audit-as-crates-io = false +first-party = true + +[policy.dkg] +audit-as-crates-io = false +first-party = true + +[policy.dkg-dealer] +audit-as-crates-io = false +first-party = true + +[policy.dkg-musig] +audit-as-crates-io = false +first-party = true + +[policy.dkg-recovery] +audit-as-crates-io = false +first-party = true + +[policy."dtoa:1.99.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."ec-divisors:0.1.0@git:0d6f5e840ad1f955e4e4dec00c5165f134815b15"] +first-party = false + +[policy.embedwards25519] +audit-as-crates-io = false +first-party = true + +[policy.ethereum-schnorr-contract] +audit-as-crates-io = false +first-party = true + +[policy."fastrlp:0.3.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."fastrlp:0.4.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy.ff-group-tests] +audit-as-crates-io = false +first-party = true + +[policy.flexible-transcript] +audit-as-crates-io = false +first-party = true + +[policy."fork-tree:13.0.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."frame-benchmarking:46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."frame-executive:46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."frame-support-procedural-tools-derive:12.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."frame-support-procedural-tools:13.0.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."frame-support-procedural:37.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."frame-support:46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."frame-system:46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."frame-try-runtime:0.52.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy.frost-schnorrkel] +audit-as-crates-io = false +first-party = true + +[policy."generalized-bulletproofs-circuit-abstraction:0.1.0@git:0d6f5e840ad1f955e4e4dec00c5165f134815b15"] +first-party = false + +[policy."generalized-bulletproofs-ec-gadgets:0.1.0@git:0d6f5e840ad1f955e4e4dec00c5165f134815b15"] +first-party = false + +[policy."generalized-bulletproofs:0.1.0@git:0d6f5e840ad1f955e4e4dec00c5165f134815b15"] +first-party = false + +[policy."getrandom:0.2.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."getrandom:0.3.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."hex_lit:0.1.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."hickory-proto:0.25.2@git:265df12bc084df83f67733ab99b9fb17ad48bf7a"] +first-party = false + +[policy."hickory-resolver:0.25.2@git:265df12bc084df83f67733ab99b9fb17ad48bf7a"] +first-party = false + +[policy."home:0.5.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."indexmap:1.99.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."k256:0.13.4@git:8293be3f9855eea0936523a9b100f1f69261ea4b"] +first-party = false + +[policy."keccak-asm:0.1.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."lazy_static:1.99.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy.minimal-ed448] +audit-as-crates-io = false +first-party = true + +[policy.modular-frost] +audit-as-crates-io = false +first-party = true + +[policy."monero-address:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-base58:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-borromean:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-bulletproofs-generators:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-bulletproofs:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-clsag:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-daemon-rpc:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-ed25519:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-epee:0.2.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-interface:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-io:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-mlsag:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-oxide:0.1.4-alpha@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-primitives:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-simple-request-rpc:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy."monero-wallet:0.1.0@git:c8be5d3d1287669946a83fbfcb296ce2a8852e47"] +first-party = false + +[policy.multiexp] +audit-as-crates-io = false +first-party = true + +[policy."nonzero_ext:0.3.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."option-ext:0.2.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."p256:0.13.2@git:8293be3f9855eea0936523a9b100f1f69261ea4b"] +first-party = false + +[policy."pallet-authorship:46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."pallet-babe:46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."pallet-grandpa:46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."pallet-session:46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."pallet-timestamp:45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy.patchable-async-sleep] +audit-as-crates-io = false +first-party = true + +[policy."prime-field:0.1.0"] +audit-as-crates-io = false +first-party = true + +[policy."primeorder:0.13.6@git:8293be3f9855eea0936523a9b100f1f69261ea4b"] +first-party = false + +[policy."primitive-types:0.12.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."rapidhash:4.99.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."rlp:0.5.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."ryu:1.99.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."sc-allocator:36.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-authority-discovery:0.56.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-basic-authorship:0.54.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-block-builder:0.49.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-chain-spec:49.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-client-api:45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-client-db:0.52.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-consensus-babe:0.56.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-consensus-epochs:0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-consensus-grandpa:0.41.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-consensus-slots:0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-consensus:0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-executor-common:0.44.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-executor-wasmtime:0.44.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-executor:0.48.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-informant:0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-keystore:40.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-network-common:0.53.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-network-gossip:0.56.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-network-sync:0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-network-transactions:0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-network-types:0.20.2@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-network:0.56.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-proposer-metrics:0.20.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-rpc-api:0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-rpc-server:28.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-rpc:51.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-service:0.57.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-state-db:0.42.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-sysinfo:47.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-telemetry:30.0.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-tracing-proc-macro:11.1.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-tracing:45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-transaction-pool-api:44.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-transaction-pool:45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sc-utils:20.1.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."schemars:0.9.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."schemars:1.99.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy.schnorr-signatures] +audit-as-crates-io = false +first-party = true + +[policy."secp256k1:0.30.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy.secq256k1] +audit-as-crates-io = false +first-party = true + +[policy.serai-abi] +audit-as-crates-io = false +first-party = true + +[policy.serai-bitcoin-processor] +audit-as-crates-io = false +first-party = true + +[policy.serai-client] +audit-as-crates-io = false +first-party = true + +[policy.serai-coordinator] +audit-as-crates-io = false +first-party = true + +[policy.serai-coordinator-substrate] +audit-as-crates-io = false +first-party = true + +[policy.serai-coordinator-tributary] +audit-as-crates-io = false +first-party = true + +[policy.serai-cosign] +audit-as-crates-io = false +first-party = true + +[policy.serai-db] +audit-as-crates-io = false +first-party = true + +[policy.serai-docker-tests] +audit-as-crates-io = false +first-party = true + +[policy.serai-env] +audit-as-crates-io = false +first-party = true + +[policy.serai-ethereum-processor] +audit-as-crates-io = false +first-party = true + +[policy.serai-ethereum-relayer] +audit-as-crates-io = false +first-party = true + +[policy.serai-message-queue-tests] +audit-as-crates-io = false +first-party = true + +[policy.serai-monero-processor] +audit-as-crates-io = false +first-party = true + +[policy.serai-no-std-tests] +audit-as-crates-io = false +first-party = true + +[policy.serai-node] +audit-as-crates-io = false +first-party = true + +[policy.serai-orchestrator] +audit-as-crates-io = false +first-party = true + +[policy.serai-primitives] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-bin] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-ethereum-deployer] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-ethereum-erc20] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-ethereum-primitives] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-ethereum-router] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-frost-attempt-manager] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-messages] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-primitives] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-signers] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-smart-contract-scheduler] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-utxo-scheduler-primitives] +audit-as-crates-io = false +first-party = true + +[policy.serai-processor-view-keys] +audit-as-crates-io = false +first-party = true + +[policy.serai-reproducible-runtime-tests] +audit-as-crates-io = false +first-party = true + +[policy.serai-runtime] +audit-as-crates-io = false +first-party = true + +[policy.serai-task] +audit-as-crates-io = false +first-party = true + +[policy.serai-validator-sets-pallet] +audit-as-crates-io = false +first-party = true + +[policy.short-weierstrass] +audit-as-crates-io = false +first-party = true + +[policy."simple-request:0.2.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy."simple-request:0.3.0"] +audit-as-crates-io = false +first-party = true + +[policy."sp-api-proc-macro:27.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-api:41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-application-crypto:45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-arithmetic:28.0.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-authority-discovery:41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-block-builder:41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-blockchain:44.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-consensus-babe:0.47.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-consensus-grandpa:28.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-consensus-slots:0.47.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-consensus:0.47.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-core:40.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-crypto-hashing-proc-macro:0.1.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-crypto-hashing:0.1.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-database:10.0.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-externalities:0.32.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-inherents:41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-io:45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-keyring:46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-keystore:0.46.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-rpc:38.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-runtime-interface-proc-macro:21.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-runtime-interface:34.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-runtime:46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-session:43.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-staking:43.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-state-machine:0.50.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-storage:23.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-timestamp:41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-tracing:19.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-transaction-pool:41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-trie:43.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-version-proc-macro:15.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-version:44.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-wasm-interface:24.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."sp-weights:34.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy."std-shims:0.1.5"] +audit-as-crates-io = false +first-party = true + +[policy."std-shims:0.1.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy.substrate-median] +audit-as-crates-io = false +first-party = true + +[policy."substrate-prometheus-endpoint:0.17.7@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988"] +first-party = false + +[policy.tendermint-machine] +audit-as-crates-io = false +first-party = true + +[policy."thiserror:1.99.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy.tributary-sdk] +audit-as-crates-io = false +first-party = true + +[policy."wit-bindgen-rust-macro:0.51.99"] +first-party = false +notes = "Member of `patches/`, but explicitly tracked as if third-party" + +[policy.zalloc] +audit-as-crates-io = false +first-party = true + +[[exemptions.Inflector]] +version = "0.11.4" +criteria = "safe-to-deploy" + +[[exemptions.addr2line]] +version = "0.26.1" +criteria = "safe-to-deploy" + +[[exemptions.aead]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.aes]] +version = "0.8.4" +criteria = "safe-to-deploy" + +[[exemptions.aes-gcm]] +version = "0.10.3" +criteria = "safe-to-deploy" + +[[exemptions.ahash]] +version = "0.8.12" +criteria = "safe-to-deploy" + +[[exemptions.aho-corasick]] +version = "1.1.4" +criteria = "safe-to-deploy" + +[[exemptions.allocator-api2]] +version = "0.2.21" +criteria = "safe-to-deploy" + +[[exemptions.alloy-chains]] +version = "0.2.33" +criteria = "safe-to-deploy" + +[[exemptions.alloy-consensus]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-consensus-any]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-core]] +version = "1.5.7" +criteria = "safe-to-deploy" + +[[exemptions.alloy-eip2124]] +version = "0.2.99" +criteria = "safe-to-deploy" +notes = "Member of `patches/`" + +[[exemptions.alloy-eip2930]] +version = "0.2.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-eip7702]] +version = "0.6.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-eip7928]] +version = "0.3.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-eips]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-genesis]] +version = "1.8.3" +criteria = "safe-to-run" + +[[exemptions.alloy-hardforks]] +version = "0.2.13" +criteria = "safe-to-run" + +[[exemptions.alloy-json-abi]] +version = "1.5.7" +criteria = "safe-to-deploy" + +[[exemptions.alloy-json-rpc]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-network]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-network-primitives]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-node-bindings]] +version = "1.8.3" +criteria = "safe-to-run" + +[[exemptions.alloy-primitives]] +version = "1.5.7" +criteria = "safe-to-deploy" + +[[exemptions.alloy-provider]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-rlp]] +version = "0.3.15" +criteria = "safe-to-deploy" + +[[exemptions.alloy-rlp-derive]] +version = "0.3.15" +criteria = "safe-to-deploy" + +[[exemptions.alloy-rpc-client]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-rpc-types-any]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-rpc-types-debug]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-rpc-types-eth]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-rpc-types-trace]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-serde]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-signer]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-signer-local]] +version = "1.8.3" +criteria = "safe-to-run" + +[[exemptions.alloy-sol-macro]] +version = "1.5.7" +criteria = "safe-to-deploy" + +[[exemptions.alloy-sol-macro-expander]] +version = "1.5.7" +criteria = "safe-to-deploy" + +[[exemptions.alloy-sol-macro-input]] +version = "1.5.7" +criteria = "safe-to-deploy" + +[[exemptions.alloy-sol-type-parser]] +version = "1.5.7" +criteria = "safe-to-deploy" + +[[exemptions.alloy-sol-types]] +version = "1.5.7" +criteria = "safe-to-deploy" + +[[exemptions.alloy-transport]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.alloy-trie]] +version = "0.9.5" +criteria = "safe-to-deploy" + +[[exemptions.alloy-tx-macros]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.android_system_properties]] +version = "0.1.5" +criteria = "safe-to-deploy" + +[[exemptions.anstyle]] +version = "1.0.14" +criteria = "safe-to-deploy" + +[[exemptions.anyhow]] +version = "1.0.102" +criteria = "safe-to-deploy" + +[[exemptions.arbitrary]] +version = "1.4.2" +criteria = "safe-to-deploy" + +[[exemptions.ark-bls12-381]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.ark-bn254]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.ark-ec]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.ark-ff]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.ark-ff-asm]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.ark-ff-macros]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.ark-poly]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.ark-serialize]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.ark-serialize-derive]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.ark-std]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.array-bytes]] +version = "7.1.0" +criteria = "safe-to-deploy" + +[[exemptions.arrayref]] +version = "0.3.9" +criteria = "safe-to-deploy" + +[[exemptions.arrayvec]] +version = "0.7.6" +criteria = "safe-to-deploy" + +[[exemptions.asn1-rs]] +version = "0.7.1" +criteria = "safe-to-deploy" + +[[exemptions.asn1-rs-derive]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.asn1-rs-impl]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.async-channel]] +version = "2.5.0" +criteria = "safe-to-deploy" + +[[exemptions.async-io]] +version = "2.6.0" +criteria = "safe-to-deploy" + +[[exemptions.async-stream]] +version = "0.3.6" +criteria = "safe-to-deploy" + +[[exemptions.async-stream-impl]] +version = "0.3.6" +criteria = "safe-to-deploy" + +[[exemptions.async-trait]] +version = "0.1.89" +criteria = "safe-to-deploy" + +[[exemptions.asynchronous-codec]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.atomic-waker]] +version = "1.1.2" +criteria = "safe-to-deploy" + +[[exemptions.attohttpc]] +version = "0.30.1" +criteria = "safe-to-deploy" + +[[exemptions.auto_impl]] +version = "1.3.0" +criteria = "safe-to-deploy" + +[[exemptions.autocfg]] +version = "1.5.0" +criteria = "safe-to-deploy" + +[[exemptions.base-x]] +version = "0.2.11" +criteria = "safe-to-deploy" + +[[exemptions.base16ct]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.base16ct]] +version = "1.0.0" +criteria = "safe-to-deploy" + +[[exemptions.base58ck]] +version = "0.1.0" +criteria = "safe-to-deploy" + +[[exemptions.base64]] +version = "0.22.1" +criteria = "safe-to-deploy" + +[[exemptions.base64ct]] +version = "1.8.3" +criteria = "safe-to-deploy" + +[[exemptions.bech32]] +version = "0.11.1" +criteria = "safe-to-deploy" + +[[exemptions.bindgen]] +version = "0.72.1" +criteria = "safe-to-deploy" + +[[exemptions.bip39]] +version = "2.2.2" +criteria = "safe-to-deploy" + +[[exemptions.bit-set]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.bit-vec]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.bitcoin]] +version = "0.32.8" +criteria = "safe-to-deploy" + +[[exemptions.bitcoin-internals]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.bitcoin-io]] +version = "0.1.4" +criteria = "safe-to-deploy" + +[[exemptions.bitcoin-units]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.bitcoin_hashes]] +version = "0.14.1" +criteria = "safe-to-deploy" + +[[exemptions.bitflags]] +version = "1.3.2" +criteria = "safe-to-deploy" + +[[exemptions.bitflags]] +version = "2.11.0" +criteria = "safe-to-deploy" + +[[exemptions.bitvec]] +version = "1.0.1" +criteria = "safe-to-deploy" + +[[exemptions.blake2]] +version = "0.10.6" +criteria = "safe-to-deploy" + +[[exemptions.blake2]] +version = "0.11.0-rc.5" +criteria = "safe-to-deploy" + +[[exemptions.block-buffer]] +version = "0.10.4" +criteria = "safe-to-deploy" + +[[exemptions.block-buffer]] +version = "0.12.0" +criteria = "safe-to-deploy" + +[[exemptions.bollard]] +version = "0.17.1" +criteria = "safe-to-deploy" + +[[exemptions.bollard-stubs]] +version = "1.45.0-rc.26.0.1" +criteria = "safe-to-deploy" + +[[exemptions.borsh]] +version = "1.6.1" +criteria = "safe-to-deploy" + +[[exemptions.borsh-derive]] +version = "1.6.1" +criteria = "safe-to-deploy" + +[[exemptions.bounded-collections]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.bs58]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.bumpalo]] +version = "3.20.2" +criteria = "safe-to-deploy" + +[[exemptions.byte-slice-cast]] +version = "1.2.3" +criteria = "safe-to-deploy" + +[[exemptions.byteorder]] +version = "1.5.0" +criteria = "safe-to-deploy" + +[[exemptions.bytes]] +version = "1.11.1" +criteria = "safe-to-deploy" + +[[exemptions.bzip2-sys]] +version = "0.1.13+1.0.8" +criteria = "safe-to-deploy" + +[[exemptions.cc]] +version = "1.2.59" +criteria = "safe-to-deploy" + +[[exemptions.cexpr]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.cfg-expr]] +version = "0.20.7" +criteria = "safe-to-deploy" + +[[exemptions.cfg-if]] +version = "1.0.4" +criteria = "safe-to-deploy" + +[[exemptions.cfg_aliases]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.chacha20]] +version = "0.9.1" +criteria = "safe-to-deploy" + +[[exemptions.chacha20poly1305]] +version = "0.10.1" +criteria = "safe-to-deploy" + +[[exemptions.chrono]] +version = "0.4.44" +criteria = "safe-to-deploy" + +[[exemptions.cipher]] +version = "0.4.4" +criteria = "safe-to-deploy" + +[[exemptions.ciphersuite]] +version = "0.4.99" +criteria = "safe-to-deploy" +notes = "Member of `patches/`" + +[[exemptions.clang-sys]] +version = "1.8.1" +criteria = "safe-to-deploy" + +[[exemptions.clap]] +version = "4.6.0" +criteria = "safe-to-deploy" + +[[exemptions.clap_builder]] +version = "4.6.0" +criteria = "safe-to-deploy" + +[[exemptions.clap_derive]] +version = "4.6.0" +criteria = "safe-to-deploy" + +[[exemptions.clap_lex]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.cmov]] +version = "0.5.3" +criteria = "safe-to-deploy" + +[[exemptions.cobs]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.concurrent-queue]] +version = "2.5.0" +criteria = "safe-to-deploy" + +[[exemptions.console]] +version = "0.16.3" +criteria = "safe-to-deploy" + +[[exemptions.const-hex]] +version = "1.18.1" +criteria = "safe-to-deploy" + +[[exemptions.const-oid]] +version = "0.9.6" +criteria = "safe-to-deploy" + +[[exemptions.const_format]] +version = "0.2.35" +criteria = "safe-to-deploy" + +[[exemptions.const_format_proc_macros]] +version = "0.2.34" +criteria = "safe-to-deploy" + +[[exemptions.core-foundation]] +version = "0.9.4" +criteria = "safe-to-deploy" + +[[exemptions.core-foundation]] +version = "0.10.1" +criteria = "safe-to-deploy" + +[[exemptions.core-foundation-sys]] +version = "0.8.7" +criteria = "safe-to-deploy" + +[[exemptions.core2]] +version = "0.4.99" +criteria = "safe-to-deploy" +notes = "Member of `patches/`" + +[[exemptions.cpubits]] +version = "0.1.0" +criteria = "safe-to-deploy" + +[[exemptions.cpufeatures]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-assembler-x64]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-assembler-x64-meta]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-bforest]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-bitset]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-codegen]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-codegen-meta]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-codegen-shared]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-control]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-entity]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-frontend]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-isle]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-native]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.cranelift-srcgen]] +version = "0.130.0" +criteria = "safe-to-deploy" + +[[exemptions.crc32fast]] +version = "1.5.0" +criteria = "safe-to-deploy" + +[[exemptions.critical-section]] +version = "1.2.0" +criteria = "safe-to-deploy" + +[[exemptions.crossbeam-deque]] +version = "0.8.6" +criteria = "safe-to-deploy" + +[[exemptions.crossbeam-epoch]] +version = "0.9.18" +criteria = "safe-to-deploy" + +[[exemptions.crossbeam-utils]] +version = "0.8.21" +criteria = "safe-to-deploy" + +[[exemptions.crunchy]] +version = "0.2.4" +criteria = "safe-to-deploy" + +[[exemptions.crypto-bigint]] +version = "0.5.5" +criteria = "safe-to-deploy" + +[[exemptions.crypto-bigint]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.crypto-common]] +version = "0.1.7" +criteria = "safe-to-deploy" + +[[exemptions.crypto-common]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.ctr]] +version = "0.9.2" +criteria = "safe-to-deploy" + +[[exemptions.ctutils]] +version = "0.4.2" +criteria = "safe-to-deploy" + +[[exemptions.curve25519-dalek]] +version = "4.1.3" +criteria = "safe-to-deploy" + +[[exemptions.curve25519-dalek-derive]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.darling]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.darling_core]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.darling_macro]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.dashmap]] +version = "6.1.0" +criteria = "safe-to-deploy" + +[[exemptions.data-encoding]] +version = "2.10.0" +criteria = "safe-to-deploy" + +[[exemptions.data-encoding-macro]] +version = "0.1.19" +criteria = "safe-to-deploy" + +[[exemptions.data-encoding-macro-internal]] +version = "0.1.17" +criteria = "safe-to-deploy" + +[[exemptions.der]] +version = "0.7.10" +criteria = "safe-to-deploy" + +[[exemptions.der-parser]] +version = "10.0.0" +criteria = "safe-to-deploy" + +[[exemptions.deranged]] +version = "0.5.8" +criteria = "safe-to-deploy" + +[[exemptions.derive-syn-parse]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.derive-where]] +version = "1.6.1" +criteria = "safe-to-deploy" + +[[exemptions.derive_more]] +version = "1.0.0" +criteria = "safe-to-deploy" + +[[exemptions.derive_more]] +version = "2.0.1" +criteria = "safe-to-deploy" + +[[exemptions.derive_more-impl]] +version = "1.0.0" +criteria = "safe-to-deploy" + +[[exemptions.derive_more-impl]] +version = "2.0.1" +criteria = "safe-to-deploy" + +[[exemptions.digest]] +version = "0.10.7" +criteria = "safe-to-deploy" + +[[exemptions.digest]] +version = "0.11.2" +criteria = "safe-to-deploy" + +[[exemptions.digest_auth]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.directories]] +version = "6.0.0" +criteria = "safe-to-deploy" + +[[exemptions.dirs]] +version = "6.0.0" +criteria = "safe-to-deploy" + +[[exemptions.dirs-sys]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.displaydoc]] +version = "0.2.5" +criteria = "safe-to-deploy" + +[[exemptions.dockertest]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.dunce]] +version = "1.0.5" +criteria = "safe-to-deploy" + +[[exemptions.dyn-clone]] +version = "1.0.20" +criteria = "safe-to-deploy" + +[[exemptions.ecdsa]] +version = "0.16.9" +criteria = "safe-to-deploy" + +[[exemptions.ed25519]] +version = "2.2.3" +criteria = "safe-to-deploy" + +[[exemptions.ed25519-dalek]] +version = "2.2.0" +criteria = "safe-to-deploy" + +[[exemptions.educe]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.either]] +version = "1.15.0" +criteria = "safe-to-deploy" + +[[exemptions.elliptic-curve]] +version = "0.13.8" +criteria = "safe-to-deploy" + +[[exemptions.embedded-io]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.embedded-io]] +version = "0.6.1" +criteria = "safe-to-deploy" + +[[exemptions.encode_unicode]] +version = "1.0.0" +criteria = "safe-to-deploy" + +[[exemptions.enum-as-inner]] +version = "0.6.1" +criteria = "safe-to-deploy" + +[[exemptions.enum-ordinalize]] +version = "4.3.2" +criteria = "safe-to-deploy" + +[[exemptions.enum-ordinalize-derive]] +version = "4.3.2" +criteria = "safe-to-deploy" + +[[exemptions.env_logger]] +version = "0.10.2" +criteria = "safe-to-deploy" + +[[exemptions.environmental]] +version = "1.1.4" +criteria = "safe-to-deploy" + +[[exemptions.equivalent]] +version = "1.0.2" +criteria = "safe-to-deploy" + +[[exemptions.errno]] +version = "0.3.14" +criteria = "safe-to-deploy" + +[[exemptions.event-listener]] +version = "5.4.1" +criteria = "safe-to-deploy" + +[[exemptions.event-listener-strategy]] +version = "0.5.4" +criteria = "safe-to-deploy" + +[[exemptions.expander]] +version = "2.0.0" +criteria = "safe-to-deploy" + +[[exemptions.fastrand]] +version = "2.4.0" +criteria = "safe-to-deploy" + +[[exemptions.ff]] +version = "0.13.1" +criteria = "safe-to-deploy" + +[[exemptions.fiat-crypto]] +version = "0.2.9" +criteria = "safe-to-deploy" + +[[exemptions.finality-grandpa]] +version = "0.16.3" +criteria = "safe-to-deploy" + +[[exemptions.find-msvc-tools]] +version = "0.1.9" +criteria = "safe-to-deploy" + +[[exemptions.fixed-hash]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.fixedbitset]] +version = "0.5.7" +criteria = "safe-to-deploy" + +[[exemptions.fnv]] +version = "1.0.7" +criteria = "safe-to-deploy" + +[[exemptions.foldhash]] +version = "0.1.5" +criteria = "safe-to-deploy" + +[[exemptions.foldhash]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.fork-tree]] +version = "13.0.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.form_urlencoded]] +version = "1.2.2" +criteria = "safe-to-deploy" + +[[exemptions.forwarded-header-value]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.frame-benchmarking]] +version = "46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.frame-executive]] +version = "46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.frame-support]] +version = "46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.frame-support-procedural]] +version = "37.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.frame-support-procedural-tools]] +version = "13.0.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.frame-support-procedural-tools-derive]] +version = "12.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.frame-system]] +version = "46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.frame-try-runtime]] +version = "0.52.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.fs-err]] +version = "2.11.0" +criteria = "safe-to-deploy" + +[[exemptions.fs2]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.funty]] +version = "2.0.0" +criteria = "safe-to-deploy" + +[[exemptions.futures]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.futures-bounded]] +version = "0.2.4" +criteria = "safe-to-deploy" + +[[exemptions.futures-channel]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.futures-core]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.futures-executor]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.futures-io]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.futures-lite]] +version = "2.6.1" +criteria = "safe-to-deploy" + +[[exemptions.futures-macro]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.futures-rustls]] +version = "0.26.0" +criteria = "safe-to-deploy" + +[[exemptions.futures-sink]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.futures-task]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.futures-timer]] +version = "3.0.3" +criteria = "safe-to-deploy" + +[[exemptions.futures-util]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.futures-utils-wasm]] +version = "0.1.0" +criteria = "safe-to-deploy" + +[[exemptions.generic-array]] +version = "0.14.7" +criteria = "safe-to-deploy" + +[[exemptions.generic-array]] +version = "1.3.5" +criteria = "safe-to-deploy" + +[[exemptions.getrandom]] +version = "0.2.99" +criteria = "safe-to-deploy" +notes = "Member of `patches/`" + +[[exemptions.getrandom]] +version = "0.3.99" +criteria = "safe-to-deploy" +notes = "Member of `patches/`" + +[[exemptions.getrandom]] +version = "0.4.2" +criteria = "safe-to-deploy" + +[[exemptions.getrandom_or_panic]] +version = "0.0.3" +criteria = "safe-to-deploy" + +[[exemptions.ghash]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.gimli]] +version = "0.33.1" +criteria = "safe-to-deploy" + +[[exemptions.glob]] +version = "0.3.3" +criteria = "safe-to-deploy" + +[[exemptions.governor]] +version = "0.10.4" +criteria = "safe-to-deploy" + +[[exemptions.group]] +version = "0.13.0" +criteria = "safe-to-deploy" + +[[exemptions.h2]] +version = "0.4.13" +criteria = "safe-to-deploy" + +[[exemptions.hash-db]] +version = "0.16.0" +criteria = "safe-to-deploy" + +[[exemptions.hash256-std-hasher]] +version = "0.15.2" +criteria = "safe-to-deploy" + +[[exemptions.hashbrown]] +version = "0.13.2" +criteria = "safe-to-deploy" + +[[exemptions.hashbrown]] +version = "0.14.5" +criteria = "safe-to-deploy" + +[[exemptions.hashbrown]] +version = "0.15.5" +criteria = "safe-to-deploy" + +[[exemptions.hashbrown]] +version = "0.16.1" +criteria = "safe-to-deploy" + +[[exemptions.hashlink]] +version = "0.9.1" +criteria = "safe-to-deploy" + +[[exemptions.hashlink]] +version = "0.10.0" +criteria = "safe-to-deploy" + +[[exemptions.heck]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.hermit-abi]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.hex]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.hex-conservative]] +version = "0.2.2" +criteria = "safe-to-deploy" + +[[exemptions.hex-literal]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.hex_fmt]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.hex_lit]] +version = "0.1.99" +criteria = "safe-to-deploy" +notes = "Member of `patches/`" + +[[exemptions.hickory-proto]] +version = "0.25.2@git:265df12bc084df83f67733ab99b9fb17ad48bf7a" +criteria = "safe-to-deploy" + +[[exemptions.hickory-resolver]] +version = "0.25.2@git:265df12bc084df83f67733ab99b9fb17ad48bf7a" +criteria = "safe-to-deploy" + +[[exemptions.hkdf]] +version = "0.12.4" +criteria = "safe-to-deploy" + +[[exemptions.hmac]] +version = "0.12.1" +criteria = "safe-to-deploy" + +[[exemptions.home]] +version = "0.5.99" +criteria = "safe-to-deploy" +notes = "Member of `patches/`" + +[[exemptions.http]] +version = "1.4.0" +criteria = "safe-to-deploy" + +[[exemptions.http-body]] +version = "1.0.1" +criteria = "safe-to-deploy" + +[[exemptions.http-body-util]] +version = "0.1.3" +criteria = "safe-to-deploy" + +[[exemptions.httparse]] +version = "1.10.1" +criteria = "safe-to-deploy" + +[[exemptions.httpdate]] +version = "1.0.3" +criteria = "safe-to-deploy" + +[[exemptions.humantime]] +version = "2.3.0" +criteria = "safe-to-deploy" + +[[exemptions.hybrid-array]] +version = "0.4.10" +criteria = "safe-to-deploy" + +[[exemptions.hyper]] +version = "1.9.0" +criteria = "safe-to-deploy" + +[[exemptions.hyper-named-pipe]] +version = "0.1.0" +criteria = "safe-to-deploy" + +[[exemptions.hyper-rustls]] +version = "0.27.7" +criteria = "safe-to-deploy" + +[[exemptions.hyper-util]] +version = "0.1.20" +criteria = "safe-to-deploy" + +[[exemptions.hyperlocal]] +version = "0.9.1" +criteria = "safe-to-deploy" + +[[exemptions.iana-time-zone]] +version = "0.1.65" +criteria = "safe-to-deploy" + +[[exemptions.iana-time-zone-haiku]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.ident_case]] +version = "1.0.1" +criteria = "safe-to-deploy" + +[[exemptions.idna]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.idna_adapter]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.idna_mapping]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.if-addrs]] +version = "0.15.0" +criteria = "safe-to-deploy" + +[[exemptions.if-watch]] +version = "3.2.2" +criteria = "safe-to-deploy" + +[[exemptions.igd-next]] +version = "0.16.2" +criteria = "safe-to-deploy" + +[[exemptions.impl-serde]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.impl-trait-for-tuples]] +version = "0.2.3" +criteria = "safe-to-deploy" + +[[exemptions.indexmap]] +version = "2.13.1" +criteria = "safe-to-deploy" + +[[exemptions.inout]] +version = "0.1.4" +criteria = "safe-to-deploy" + +[[exemptions.ip_network]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.ipconfig]] +version = "0.3.4" +criteria = "safe-to-deploy" + +[[exemptions.ipnet]] +version = "2.12.0" +criteria = "safe-to-deploy" + +[[exemptions.itertools]] +version = "0.13.0" +criteria = "safe-to-deploy" + +[[exemptions.itertools]] +version = "0.14.0" +criteria = "safe-to-deploy" + +[[exemptions.itoa]] +version = "1.0.18" +criteria = "safe-to-deploy" + +[[exemptions.jobserver]] +version = "0.1.34" +criteria = "safe-to-deploy" + +[[exemptions.js-sys]] +version = "0.3.94" +criteria = "safe-to-deploy" + +[[exemptions.jsonrpsee]] +version = "0.24.10" +criteria = "safe-to-deploy" + +[[exemptions.jsonrpsee-core]] +version = "0.24.10" +criteria = "safe-to-deploy" + +[[exemptions.jsonrpsee-proc-macros]] +version = "0.24.10" +criteria = "safe-to-deploy" + +[[exemptions.jsonrpsee-server]] +version = "0.24.10" +criteria = "safe-to-deploy" + +[[exemptions.jsonrpsee-types]] +version = "0.24.10" +criteria = "safe-to-deploy" + +[[exemptions.k256]] +version = "0.13.4@git:8293be3f9855eea0936523a9b100f1f69261ea4b" +criteria = "safe-to-deploy" + +[[exemptions.keccak]] +version = "0.1.6" +criteria = "safe-to-deploy" + +[[exemptions.keccak]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.keccak-asm]] +version = "0.1.99" +criteria = "safe-to-deploy" +notes = "Member of `patches/`" + +[[exemptions.kvdb]] +version = "0.13.0" +criteria = "safe-to-deploy" + +[[exemptions.kvdb-memorydb]] +version = "0.13.0" +criteria = "safe-to-deploy" + +[[exemptions.kvdb-rocksdb]] +version = "0.21.0" +criteria = "safe-to-deploy" + +[[exemptions.leb128fmt]] +version = "0.1.0" +criteria = "safe-to-deploy" + +[[exemptions.libc]] +version = "0.2.184" +criteria = "safe-to-deploy" + +[[exemptions.libm]] +version = "0.2.16" +criteria = "safe-to-deploy" + +[[exemptions.libp2p]] +version = "0.56.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-allow-block-list]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-connection-limits]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-core]] +version = "0.43.2" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-dns]] +version = "0.44.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-gossipsub]] +version = "0.49.4" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-identify]] +version = "0.47.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-identity]] +version = "0.2.13" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-kad]] +version = "0.48.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-mdns]] +version = "0.48.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-metrics]] +version = "0.17.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-noise]] +version = "0.46.1" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-ping]] +version = "0.47.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-quic]] +version = "0.13.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-request-response]] +version = "0.29.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-swarm]] +version = "0.47.1" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-swarm-derive]] +version = "0.35.1" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-tcp]] +version = "0.44.1" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-tls]] +version = "0.6.2" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-upnp]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-websocket]] +version = "0.45.1" +criteria = "safe-to-deploy" + +[[exemptions.libp2p-yamux]] +version = "0.47.0" +criteria = "safe-to-deploy" + +[[exemptions.libredox]] +version = "0.1.15" +criteria = "safe-to-deploy" + +[[exemptions.librocksdb-sys]] +version = "0.17.3+10.4.2" +criteria = "safe-to-deploy" + +[[exemptions.libz-sys]] +version = "1.1.28" +criteria = "safe-to-deploy" + +[[exemptions.linked-hash-map]] +version = "0.5.6" +criteria = "safe-to-deploy" + +[[exemptions.linked_hash_set]] +version = "0.1.6" +criteria = "safe-to-deploy" + +[[exemptions.linux-raw-sys]] +version = "0.12.1" +criteria = "safe-to-deploy" + +[[exemptions.lock_api]] +version = "0.4.14" +criteria = "safe-to-deploy" + +[[exemptions.log]] +version = "0.4.29" +criteria = "safe-to-deploy" + +[[exemptions.lru]] +version = "0.16.3" +criteria = "safe-to-deploy" + +[[exemptions.lru-slab]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.lz4]] +version = "1.28.1" +criteria = "safe-to-deploy" + +[[exemptions.lz4-sys]] +version = "1.11.1+lz4-1.10.0" +criteria = "safe-to-deploy" + +[[exemptions.mach2]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.macro-string]] +version = "0.1.4" +criteria = "safe-to-deploy" + +[[exemptions.macro_magic]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.macro_magic_core]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.macro_magic_core_macros]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.macro_magic_macros]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.matchers]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.md-5]] +version = "0.10.6" +criteria = "safe-to-deploy" + +[[exemptions.memchr]] +version = "2.8.0" +criteria = "safe-to-deploy" + +[[exemptions.memfd]] +version = "0.6.5" +criteria = "safe-to-deploy" + +[[exemptions.memmap2]] +version = "0.9.10" +criteria = "safe-to-deploy" + +[[exemptions.memory-db]] +version = "0.34.0" +criteria = "safe-to-deploy" + +[[exemptions.merlin]] +version = "3.0.0" +criteria = "safe-to-deploy" + +[[exemptions.minimal-lexical]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.mio]] +version = "1.2.0" +criteria = "safe-to-deploy" + +[[exemptions.multiaddr]] +version = "0.17.1" +criteria = "safe-to-deploy" + +[[exemptions.multiaddr]] +version = "0.18.2" +criteria = "safe-to-deploy" + +[[exemptions.multibase]] +version = "0.9.1" +criteria = "safe-to-deploy" + +[[exemptions.multihash]] +version = "0.17.0" +criteria = "safe-to-deploy" + +[[exemptions.multihash]] +version = "0.19.3" +criteria = "safe-to-deploy" + +[[exemptions.multihash-derive]] +version = "0.8.1" +criteria = "safe-to-deploy" + +[[exemptions.multimap]] +version = "0.10.1" +criteria = "safe-to-deploy" + +[[exemptions.multistream-select]] +version = "0.13.0" +criteria = "safe-to-deploy" + +[[exemptions.netlink-packet-core]] +version = "0.8.1" +criteria = "safe-to-deploy" + +[[exemptions.netlink-packet-route]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.netlink-proto]] +version = "0.12.0" +criteria = "safe-to-deploy" + +[[exemptions.netlink-sys]] +version = "0.8.8" +criteria = "safe-to-deploy" + +[[exemptions.nix]] +version = "0.30.1" +criteria = "safe-to-deploy" + +[[exemptions.nohash-hasher]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.nom]] +version = "7.1.3" +criteria = "safe-to-deploy" + +[[exemptions.nonempty]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.nu-ansi-term]] +version = "0.50.3" +criteria = "safe-to-deploy" + +[[exemptions.num-bigint]] +version = "0.4.6" +criteria = "safe-to-deploy" + +[[exemptions.num-integer]] +version = "0.1.46" +criteria = "safe-to-deploy" + +[[exemptions.num-rational]] +version = "0.4.2" +criteria = "safe-to-deploy" + +[[exemptions.num-traits]] +version = "0.2.19" +criteria = "safe-to-deploy" + +[[exemptions.num_cpus]] +version = "1.17.0" +criteria = "safe-to-deploy" + +[[exemptions.num_enum]] +version = "0.7.6" +criteria = "safe-to-deploy" + +[[exemptions.num_enum_derive]] +version = "0.7.6" +criteria = "safe-to-deploy" + +[[exemptions.nybbles]] +version = "0.4.8" +criteria = "safe-to-deploy" + +[[exemptions.object]] +version = "0.38.1" +criteria = "safe-to-deploy" + +[[exemptions.oid-registry]] +version = "0.8.1" +criteria = "safe-to-deploy" + +[[exemptions.once_cell]] +version = "1.21.4" +criteria = "safe-to-deploy" + +[[exemptions.openssl-probe]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.p256]] +version = "0.13.2@git:8293be3f9855eea0936523a9b100f1f69261ea4b" +criteria = "safe-to-deploy" + +[[exemptions.pallet-authorship]] +version = "46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.pallet-babe]] +version = "46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.pallet-grandpa]] +version = "46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.pallet-session]] +version = "46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.pallet-timestamp]] +version = "45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.parity-db]] +version = "0.5.4" +criteria = "safe-to-deploy" + +[[exemptions.parity-scale-codec]] +version = "3.7.5" +criteria = "safe-to-deploy" + +[[exemptions.parity-scale-codec-derive]] +version = "3.7.5" +criteria = "safe-to-deploy" + +[[exemptions.parking]] +version = "2.2.1" +criteria = "safe-to-deploy" + +[[exemptions.parking_lot]] +version = "0.12.5" +criteria = "safe-to-deploy" + +[[exemptions.parking_lot_core]] +version = "0.9.12" +criteria = "safe-to-deploy" + +[[exemptions.partial_sort]] +version = "1.0.0" +criteria = "safe-to-deploy" + +[[exemptions.paste]] +version = "1.0.15" +criteria = "safe-to-deploy" + +[[exemptions.pem]] +version = "3.0.6" +criteria = "safe-to-deploy" + +[[exemptions.percent-encoding]] +version = "2.3.2" +criteria = "safe-to-deploy" + +[[exemptions.petgraph]] +version = "0.8.3" +criteria = "safe-to-deploy" + +[[exemptions.pin-project]] +version = "1.1.11" +criteria = "safe-to-deploy" + +[[exemptions.pin-project-internal]] +version = "1.1.11" +criteria = "safe-to-deploy" + +[[exemptions.pin-project-lite]] +version = "0.2.17" +criteria = "safe-to-deploy" + +[[exemptions.pin-utils]] +version = "0.1.0" +criteria = "safe-to-deploy" + +[[exemptions.pkcs8]] +version = "0.10.2" +criteria = "safe-to-deploy" + +[[exemptions.pkg-config]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.polling]] +version = "3.11.0" +criteria = "safe-to-deploy" + +[[exemptions.poly1305]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.polyval]] +version = "0.6.2" +criteria = "safe-to-deploy" + +[[exemptions.portable-atomic]] +version = "1.13.1" +criteria = "safe-to-deploy" + +[[exemptions.postcard]] +version = "1.1.3" +criteria = "safe-to-deploy" + +[[exemptions.powerfmt]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.ppv-lite86]] +version = "0.2.21" +criteria = "safe-to-deploy" + +[[exemptions.prettyplease]] +version = "0.2.37" +criteria = "safe-to-deploy" + +[[exemptions.primeorder]] +version = "0.13.6@git:8293be3f9855eea0936523a9b100f1f69261ea4b" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-crate]] +version = "1.1.3" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-crate]] +version = "3.5.0" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-error]] +version = "1.0.4" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-error-attr]] +version = "1.0.4" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-error-attr2]] +version = "2.0.0" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-error2]] +version = "2.0.1" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-warning]] +version = "1.84.1" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro2]] +version = "1.0.106" +criteria = "safe-to-deploy" + +[[exemptions.prometheus]] +version = "0.14.0" +criteria = "safe-to-deploy" + +[[exemptions.prometheus-client]] +version = "0.23.1" +criteria = "safe-to-deploy" + +[[exemptions.prometheus-client-derive-encode]] +version = "0.4.2" +criteria = "safe-to-deploy" + +[[exemptions.proptest]] +version = "1.11.0" +criteria = "safe-to-deploy" + +[[exemptions.prost]] +version = "0.14.3" +criteria = "safe-to-deploy" + +[[exemptions.prost-build]] +version = "0.14.3" +criteria = "safe-to-deploy" + +[[exemptions.prost-derive]] +version = "0.14.3" +criteria = "safe-to-deploy" + +[[exemptions.prost-types]] +version = "0.14.3" +criteria = "safe-to-deploy" + +[[exemptions.pulley-interpreter]] +version = "43.0.0" +criteria = "safe-to-deploy" + +[[exemptions.pulley-macros]] +version = "43.0.0" +criteria = "safe-to-deploy" + +[[exemptions.quanta]] +version = "0.12.6" +criteria = "safe-to-deploy" + +[[exemptions.quick-error]] +version = "1.2.3" +criteria = "safe-to-deploy" + +[[exemptions.quick-protobuf]] +version = "0.8.1" +criteria = "safe-to-deploy" + +[[exemptions.quick-protobuf-codec]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.quinn]] +version = "0.11.9" +criteria = "safe-to-deploy" + +[[exemptions.quinn-proto]] +version = "0.11.14" +criteria = "safe-to-deploy" + +[[exemptions.quinn-udp]] +version = "0.5.14" +criteria = "safe-to-deploy" + +[[exemptions.quote]] +version = "1.0.45" +criteria = "safe-to-deploy" + +[[exemptions.r-efi]] +version = "6.0.0" +criteria = "safe-to-deploy" + +[[exemptions.radium]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.rand]] +version = "0.8.5" +criteria = "safe-to-deploy" + +[[exemptions.rand]] +version = "0.9.2" +criteria = "safe-to-deploy" + +[[exemptions.rand_chacha]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.rand_chacha]] +version = "0.9.0" +criteria = "safe-to-deploy" + +[[exemptions.rand_core]] +version = "0.6.4" +criteria = "safe-to-deploy" + +[[exemptions.rand_core]] +version = "0.9.5" +criteria = "safe-to-deploy" + +[[exemptions.rand_distr]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.rand_pcg]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.rand_xorshift]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.raw-cpuid]] +version = "11.6.0" +criteria = "safe-to-deploy" + +[[exemptions.rayon]] +version = "1.11.0" +criteria = "safe-to-deploy" + +[[exemptions.rayon-core]] +version = "1.13.0" +criteria = "safe-to-deploy" + +[[exemptions.rcgen]] +version = "0.13.2" +criteria = "safe-to-deploy" + +[[exemptions.redox_syscall]] +version = "0.5.18" +criteria = "safe-to-deploy" + +[[exemptions.redox_users]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.ref-cast]] +version = "1.0.25" +criteria = "safe-to-deploy" + +[[exemptions.ref-cast-impl]] +version = "1.0.25" +criteria = "safe-to-deploy" + +[[exemptions.regalloc2]] +version = "0.15.0" +criteria = "safe-to-deploy" + +[[exemptions.regex]] +version = "1.12.3" +criteria = "safe-to-deploy" + +[[exemptions.regex-automata]] +version = "0.4.14" +criteria = "safe-to-deploy" + +[[exemptions.regex-syntax]] +version = "0.8.10" +criteria = "safe-to-deploy" + +[[exemptions.resolv-conf]] +version = "0.7.6" +criteria = "safe-to-deploy" + +[[exemptions.revm]] +version = "36.0.0" +criteria = "safe-to-deploy" + +[[exemptions.revm-bytecode]] +version = "9.0.0" +criteria = "safe-to-deploy" + +[[exemptions.revm-context]] +version = "15.0.0" +criteria = "safe-to-deploy" + +[[exemptions.revm-context-interface]] +version = "16.0.0" +criteria = "safe-to-deploy" + +[[exemptions.revm-database]] +version = "12.0.0" +criteria = "safe-to-deploy" + +[[exemptions.revm-database-interface]] +version = "10.0.0" +criteria = "safe-to-deploy" + +[[exemptions.revm-handler]] +version = "17.0.0" +criteria = "safe-to-deploy" + +[[exemptions.revm-inspector]] +version = "17.0.0" +criteria = "safe-to-deploy" + +[[exemptions.revm-interpreter]] +version = "34.0.0" +criteria = "safe-to-deploy" + +[[exemptions.revm-precompile]] +version = "32.1.0" +criteria = "safe-to-deploy" + +[[exemptions.revm-primitives]] +version = "22.1.0" +criteria = "safe-to-deploy" + +[[exemptions.revm-state]] +version = "10.0.0" +criteria = "safe-to-deploy" + +[[exemptions.rfc6979]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.ring]] +version = "0.17.14" +criteria = "safe-to-deploy" + +[[exemptions.ripemd]] +version = "0.1.3" +criteria = "safe-to-deploy" + +[[exemptions.rocksdb]] +version = "0.24.0" +criteria = "safe-to-deploy" + +[[exemptions.route-recognizer]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.rtnetlink]] +version = "0.20.0" +criteria = "safe-to-deploy" + +[[exemptions.ruint]] +version = "1.17.2" +criteria = "safe-to-deploy" + +[[exemptions.ruint-macro]] +version = "1.2.1" +criteria = "safe-to-deploy" + +[[exemptions.rustc-hash]] +version = "2.1.2" +criteria = "safe-to-deploy" + +[[exemptions.rustc-hex]] +version = "2.1.0" +criteria = "safe-to-deploy" + +[[exemptions.rustc_version]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.rusticata-macros]] +version = "4.1.0" +criteria = "safe-to-deploy" + +[[exemptions.rustix]] +version = "1.1.4" +criteria = "safe-to-deploy" + +[[exemptions.rustls]] +version = "0.23.37" +criteria = "safe-to-deploy" + +[[exemptions.rustls-native-certs]] +version = "0.8.3" +criteria = "safe-to-deploy" + +[[exemptions.rustls-pki-types]] +version = "1.14.0" +criteria = "safe-to-deploy" + +[[exemptions.rustls-webpki]] +version = "0.103.10" +criteria = "safe-to-deploy" + +[[exemptions.rustversion]] +version = "1.0.22" +criteria = "safe-to-deploy" + +[[exemptions.rusty-fork]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.rw-stream-sink]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.sc-allocator]] +version = "36.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-authority-discovery]] +version = "0.56.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-basic-authorship]] +version = "0.54.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-block-builder]] +version = "0.49.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-chain-spec]] +version = "49.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-client-api]] +version = "45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-client-db]] +version = "0.52.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-consensus]] +version = "0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-consensus-babe]] +version = "0.56.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-consensus-epochs]] +version = "0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-consensus-grandpa]] +version = "0.41.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-consensus-slots]] +version = "0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-executor]] +version = "0.48.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-executor-common]] +version = "0.44.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-executor-wasmtime]] +version = "0.44.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-informant]] +version = "0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-keystore]] +version = "40.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-network]] +version = "0.56.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-network-common]] +version = "0.53.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-network-gossip]] +version = "0.56.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-network-sync]] +version = "0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-network-transactions]] +version = "0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-network-types]] +version = "0.20.2@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-proposer-metrics]] +version = "0.20.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-rpc]] +version = "51.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-rpc-api]] +version = "0.55.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-rpc-server]] +version = "28.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-service]] +version = "0.57.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-state-db]] +version = "0.42.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-sysinfo]] +version = "47.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-telemetry]] +version = "30.0.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-tracing]] +version = "45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-tracing-proc-macro]] +version = "11.1.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-transaction-pool]] +version = "45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-transaction-pool-api]] +version = "44.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sc-utils]] +version = "20.1.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.scale-info]] +version = "2.11.6" +criteria = "safe-to-deploy" + +[[exemptions.scale-info-derive]] +version = "2.11.6" +criteria = "safe-to-deploy" + +[[exemptions.schannel]] +version = "0.1.29" +criteria = "safe-to-deploy" + +[[exemptions.schnellru]] +version = "0.2.4" +criteria = "safe-to-deploy" + +[[exemptions.schnorrkel]] +version = "0.11.5" +criteria = "safe-to-deploy" + +[[exemptions.scopeguard]] +version = "1.2.0" +criteria = "safe-to-deploy" + +[[exemptions.sec1]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.secp256k1]] +version = "0.29.1" +criteria = "safe-to-deploy" + +[[exemptions.secp256k1-sys]] +version = "0.10.1" +criteria = "safe-to-deploy" + +[[exemptions.secrecy]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.security-framework]] +version = "3.7.0" +criteria = "safe-to-deploy" + +[[exemptions.security-framework-sys]] +version = "2.17.0" +criteria = "safe-to-deploy" + +[[exemptions.semver]] +version = "1.0.28" +criteria = "safe-to-deploy" + +[[exemptions.serde]] +version = "1.0.228" +criteria = "safe-to-deploy" + +[[exemptions.serde_bytes]] +version = "0.11.19" +criteria = "safe-to-deploy" + +[[exemptions.serde_core]] +version = "1.0.228" +criteria = "safe-to-deploy" + +[[exemptions.serde_derive]] +version = "1.0.228" +criteria = "safe-to-deploy" + +[[exemptions.serde_json]] +version = "1.0.149" +criteria = "safe-to-deploy" + +[[exemptions.serde_repr]] +version = "0.1.20" +criteria = "safe-to-deploy" + +[[exemptions.serde_spanned]] +version = "1.1.1" +criteria = "safe-to-deploy" + +[[exemptions.serde_urlencoded]] +version = "0.7.1" +criteria = "safe-to-deploy" + +[[exemptions.serde_with]] +version = "3.18.0" +criteria = "safe-to-deploy" + +[[exemptions.serde_with_macros]] +version = "3.18.0" +criteria = "safe-to-deploy" + +[[exemptions.serdect]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.sha1]] +version = "0.10.6" +criteria = "safe-to-deploy" + +[[exemptions.sha2]] +version = "0.10.9" +criteria = "safe-to-deploy" + +[[exemptions.sha2]] +version = "0.11.0" +criteria = "safe-to-deploy" + +[[exemptions.sha3]] +version = "0.10.8" +criteria = "safe-to-deploy" + +[[exemptions.sha3]] +version = "0.11.0" +criteria = "safe-to-deploy" + +[[exemptions.sharded-slab]] +version = "0.1.7" +criteria = "safe-to-deploy" + +[[exemptions.shlex]] +version = "1.3.0" +criteria = "safe-to-deploy" + +[[exemptions.signal-hook-registry]] +version = "1.4.8" +criteria = "safe-to-deploy" + +[[exemptions.signature]] +version = "2.2.0" +criteria = "safe-to-deploy" + +[[exemptions.simple-request]] +version = "0.2.99" +criteria = "safe-to-deploy" +notes = "Member of `patches/`" + +[[exemptions.siphasher]] +version = "1.0.2" +criteria = "safe-to-deploy" + +[[exemptions.slab]] +version = "0.4.12" +criteria = "safe-to-deploy" + +[[exemptions.smallvec]] +version = "1.15.1" +criteria = "safe-to-deploy" + +[[exemptions.snap]] +version = "1.1.1" +criteria = "safe-to-deploy" + +[[exemptions.snow]] +version = "0.9.6" +criteria = "safe-to-deploy" + +[[exemptions.socket2]] +version = "0.5.10" +criteria = "safe-to-deploy" + +[[exemptions.socket2]] +version = "0.6.3" +criteria = "safe-to-deploy" + +[[exemptions.soketto]] +version = "0.8.1" +criteria = "safe-to-deploy" + +[[exemptions.sp-api]] +version = "41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-api-proc-macro]] +version = "27.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-application-crypto]] +version = "45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-arithmetic]] +version = "28.0.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-authority-discovery]] +version = "41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-block-builder]] +version = "41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-blockchain]] +version = "44.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-consensus]] +version = "0.47.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-consensus-babe]] +version = "0.47.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-consensus-grandpa]] +version = "28.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-consensus-slots]] +version = "0.47.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-core]] +version = "40.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-crypto-hashing]] +version = "0.1.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-crypto-hashing-proc-macro]] +version = "0.1.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-database]] +version = "10.0.1@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-externalities]] +version = "0.32.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-inherents]] +version = "41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-io]] +version = "45.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-keyring]] +version = "46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-keystore]] +version = "0.46.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-rpc]] +version = "38.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-runtime]] +version = "46.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-runtime-interface]] +version = "34.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-runtime-interface-proc-macro]] +version = "21.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-session]] +version = "43.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-staking]] +version = "43.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-state-machine]] +version = "0.50.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-storage]] +version = "23.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-timestamp]] +version = "41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-tracing]] +version = "19.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-transaction-pool]] +version = "41.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-trie]] +version = "43.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-version]] +version = "44.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-version-proc-macro]] +version = "15.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-wasm-interface]] +version = "24.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.sp-weights]] +version = "34.0.0@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.spin]] +version = "0.10.0" +criteria = "safe-to-deploy" + +[[exemptions.spinning_top]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.spki]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.static_assertions]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.std-shims]] +version = "0.1.99" +criteria = "safe-to-deploy" +notes = "Member of `patches/`" + +[[exemptions.strsim]] +version = "0.11.1" +criteria = "safe-to-deploy" + +[[exemptions.strum]] +version = "0.26.3" +criteria = "safe-to-deploy" + +[[exemptions.strum]] +version = "0.27.2" +criteria = "safe-to-deploy" + +[[exemptions.strum]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.strum_macros]] +version = "0.26.4" +criteria = "safe-to-deploy" + +[[exemptions.strum_macros]] +version = "0.27.2" +criteria = "safe-to-deploy" + +[[exemptions.strum_macros]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.substrate-prometheus-endpoint]] +version = "0.17.7@git:bb58a9b8fc8ef8c85156dec576b83474e2eba988" +criteria = "safe-to-deploy" + +[[exemptions.subtle]] +version = "2.6.1" +criteria = "safe-to-deploy" + +[[exemptions.syn]] +version = "1.0.109" +criteria = "safe-to-deploy" + +[[exemptions.syn]] +version = "2.0.117" +criteria = "safe-to-deploy" + +[[exemptions.syn-solidity]] +version = "1.5.7" +criteria = "safe-to-deploy" + +[[exemptions.sync_wrapper]] +version = "1.0.2" +criteria = "safe-to-deploy" + +[[exemptions.synstructure]] +version = "0.12.6" +criteria = "safe-to-deploy" + +[[exemptions.synstructure]] +version = "0.13.2" +criteria = "safe-to-deploy" + +[[exemptions.system-configuration]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.system-configuration-sys]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.tap]] +version = "1.0.1" +criteria = "safe-to-deploy" + +[[exemptions.target-lexicon]] +version = "0.13.5" +criteria = "safe-to-deploy" + +[[exemptions.tempfile]] +version = "3.27.0" +criteria = "safe-to-deploy" + +[[exemptions.termcolor]] +version = "1.4.1" +criteria = "safe-to-deploy" + +[[exemptions.thiserror]] +version = "1.99.99" +criteria = "safe-to-deploy" +notes = "Member of `patches/`" + +[[exemptions.thiserror]] +version = "2.0.18" +criteria = "safe-to-deploy" + +[[exemptions.thiserror-impl]] +version = "2.0.18" +criteria = "safe-to-deploy" + +[[exemptions.thread_local]] +version = "1.1.9" +criteria = "safe-to-deploy" + +[[exemptions.time]] +version = "0.3.47" +criteria = "safe-to-deploy" + +[[exemptions.time-core]] +version = "0.1.8" +criteria = "safe-to-deploy" + +[[exemptions.time-macros]] +version = "0.2.27" +criteria = "safe-to-deploy" + +[[exemptions.tiny-keccak]] +version = "2.0.2" +criteria = "safe-to-deploy" + +[[exemptions.tinyvec]] +version = "1.11.0" +criteria = "safe-to-deploy" + +[[exemptions.tinyvec_macros]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.tokio]] +version = "1.51.0" +criteria = "safe-to-deploy" + +[[exemptions.tokio-macros]] +version = "2.7.0" +criteria = "safe-to-deploy" + +[[exemptions.tokio-rustls]] +version = "0.26.4" +criteria = "safe-to-deploy" + +[[exemptions.tokio-stream]] +version = "0.1.18" +criteria = "safe-to-deploy" + +[[exemptions.tokio-util]] +version = "0.7.18" +criteria = "safe-to-deploy" + +[[exemptions.toml]] +version = "0.5.11" +criteria = "safe-to-deploy" + +[[exemptions.toml]] +version = "1.1.2+spec-1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.toml_datetime]] +version = "1.1.1+spec-1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.toml_edit]] +version = "0.25.10+spec-1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.toml_parser]] +version = "1.1.2+spec-1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.tower]] +version = "0.4.13" +criteria = "safe-to-deploy" + +[[exemptions.tower]] +version = "0.5.3" +criteria = "safe-to-deploy" + +[[exemptions.tower-http]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.tower-layer]] +version = "0.3.3" +criteria = "safe-to-deploy" + +[[exemptions.tower-service]] +version = "0.3.3" +criteria = "safe-to-deploy" + +[[exemptions.tracing]] +version = "0.1.44" +criteria = "safe-to-deploy" + +[[exemptions.tracing-attributes]] +version = "0.1.31" +criteria = "safe-to-deploy" + +[[exemptions.tracing-core]] +version = "0.1.36" +criteria = "safe-to-deploy" + +[[exemptions.tracing-futures]] +version = "0.2.5" +criteria = "safe-to-deploy" + +[[exemptions.tracing-log]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.tracing-subscriber]] +version = "0.3.23" +criteria = "safe-to-deploy" + +[[exemptions.trie-db]] +version = "0.31.0" +criteria = "safe-to-deploy" + +[[exemptions.trie-root]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.try-lock]] +version = "0.2.5" +criteria = "safe-to-deploy" + +[[exemptions.tt-call]] +version = "1.0.9" +criteria = "safe-to-deploy" + +[[exemptions.tuplex]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.twox-hash]] +version = "2.1.2" +criteria = "safe-to-deploy" + +[[exemptions.typenum]] +version = "1.19.0" +criteria = "safe-to-deploy" + +[[exemptions.uint]] +version = "0.10.0" +criteria = "safe-to-deploy" + +[[exemptions.unarray]] +version = "0.1.4" +criteria = "safe-to-deploy" + +[[exemptions.unicode-bidi]] +version = "0.3.18" +criteria = "safe-to-deploy" + +[[exemptions.unicode-ident]] +version = "1.0.24" +criteria = "safe-to-deploy" + +[[exemptions.unicode-joining-type]] +version = "1.0.0" +criteria = "safe-to-deploy" + +[[exemptions.unicode-normalization]] +version = "0.1.25" +criteria = "safe-to-deploy" + +[[exemptions.unicode-width]] +version = "0.2.2" +criteria = "safe-to-deploy" + +[[exemptions.unicode-xid]] +version = "0.2.6" +criteria = "safe-to-deploy" + +[[exemptions.universal-hash]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.unsigned-varint]] +version = "0.7.2" +criteria = "safe-to-deploy" + +[[exemptions.unsigned-varint]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.untrusted]] +version = "0.9.0" +criteria = "safe-to-deploy" + +[[exemptions.url]] +version = "2.5.8" +criteria = "safe-to-deploy" + +[[exemptions.utf8_iter]] +version = "1.0.4" +criteria = "safe-to-deploy" + +[[exemptions.valuable]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.vcpkg]] +version = "0.2.15" +criteria = "safe-to-deploy" + +[[exemptions.version_check]] +version = "0.9.5" +criteria = "safe-to-deploy" + +[[exemptions.wait-timeout]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.want]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.wasi]] +version = "0.11.1+wasi-snapshot-preview1" +criteria = "safe-to-deploy" + +[[exemptions.wasip2]] +version = "1.0.2+wasi-0.2.9" +criteria = "safe-to-deploy" + +[[exemptions.wasip3]] +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen]] +version = "0.2.117" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen-macro]] +version = "0.2.117" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen-macro-support]] +version = "0.2.117" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen-shared]] +version = "0.2.117" +criteria = "safe-to-deploy" + +[[exemptions.wasm-encoder]] +version = "0.245.1" +criteria = "safe-to-deploy" + +[[exemptions.wasmparser]] +version = "0.245.1" +criteria = "safe-to-deploy" + +[[exemptions.wasmprinter]] +version = "0.245.1" +criteria = "safe-to-deploy" + +[[exemptions.wasmtime]] +version = "43.0.0" +criteria = "safe-to-deploy" + +[[exemptions.wasmtime-environ]] +version = "43.0.0" +criteria = "safe-to-deploy" + +[[exemptions.wasmtime-internal-core]] +version = "43.0.0" +criteria = "safe-to-deploy" + +[[exemptions.wasmtime-internal-cranelift]] +version = "43.0.0" +criteria = "safe-to-deploy" + +[[exemptions.wasmtime-internal-fiber]] +version = "43.0.0" +criteria = "safe-to-deploy" + +[[exemptions.wasmtime-internal-jit-debug]] +version = "43.0.0" +criteria = "safe-to-deploy" + +[[exemptions.wasmtime-internal-jit-icache-coherence]] +version = "43.0.0" +criteria = "safe-to-deploy" + +[[exemptions.wasmtime-internal-unwinder]] +version = "43.0.0" +criteria = "safe-to-deploy" + +[[exemptions.wasmtime-internal-versioned-export-macros]] +version = "43.0.0" +criteria = "safe-to-deploy" + +[[exemptions.wasmtimer]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.web-sys]] +version = "0.3.94" +criteria = "safe-to-deploy" + +[[exemptions.web-time]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.webpki-roots]] +version = "0.26.11" +criteria = "safe-to-deploy" + +[[exemptions.webpki-roots]] +version = "1.0.6" +criteria = "safe-to-deploy" + +[[exemptions.widestring]] +version = "1.2.1" +criteria = "safe-to-deploy" + +[[exemptions.winapi]] +version = "0.3.9" +criteria = "safe-to-deploy" + +[[exemptions.winapi-i686-pc-windows-gnu]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.winapi-util]] +version = "0.1.11" +criteria = "safe-to-deploy" + +[[exemptions.winapi-x86_64-pc-windows-gnu]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.winnow]] +version = "0.7.15" +criteria = "safe-to-deploy" + +[[exemptions.winnow]] +version = "1.0.1" +criteria = "safe-to-deploy" + +[[exemptions.wit-bindgen]] +version = "0.51.0" +criteria = "safe-to-deploy" + +[[exemptions.wyz]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.x25519-dalek]] +version = "2.0.1" +criteria = "safe-to-deploy" + +[[exemptions.x509-parser]] +version = "0.17.0" +criteria = "safe-to-deploy" + +[[exemptions.xml-rs]] +version = "0.8.28" +criteria = "safe-to-deploy" + +[[exemptions.xmltree]] +version = "0.10.3" +criteria = "safe-to-deploy" + +[[exemptions.yamux]] +version = "0.12.1" +criteria = "safe-to-deploy" + +[[exemptions.yamux]] +version = "0.13.10" +criteria = "safe-to-deploy" + +[[exemptions.yasna]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.zerocopy]] +version = "0.8.48" +criteria = "safe-to-deploy" + +[[exemptions.zerocopy-derive]] +version = "0.8.48" +criteria = "safe-to-deploy" + +[[exemptions.zeroize]] +version = "1.8.2" +criteria = "safe-to-deploy" + +[[exemptions.zeroize_derive]] +version = "1.4.3" +criteria = "safe-to-deploy" + +[[exemptions.zmij]] +version = "1.0.21" +criteria = "safe-to-deploy" + +[[exemptions.zstd-sys]] +version = "2.0.16+zstd.1.5.7" +criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock new file mode 100644 index 000000000..1ea6635d5 --- /dev/null +++ b/supply-chain/imports.lock @@ -0,0 +1,163 @@ + +# cargo-vet imports lock + +[[publisher.windows]] +version = "0.62.2" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-collections]] +version = "0.3.2" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-core]] +version = "0.62.2" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-future]] +version = "0.3.2" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-implement]] +version = "0.60.2" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-interface]] +version = "0.59.3" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-link]] +version = "0.2.1" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-numerics]] +version = "0.3.1" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-registry]] +version = "0.6.1" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-result]] +version = "0.4.1" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-strings]] +version = "0.5.1" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-sys]] +version = "0.52.0" +when = "2023-11-15" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-sys]] +version = "0.61.2" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-targets]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-threading]] +version = "0.2.1" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_gnullvm]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_msvc]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_gnu]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_gnullvm]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_msvc]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnu]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnullvm]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_msvc]] +version = "0.52.6" +when = "2024-07-03" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" diff --git a/supply-chain/no-first-party.sh b/supply-chain/no-first-party.sh new file mode 100755 index 000000000..76844d13d --- /dev/null +++ b/supply-chain/no-first-party.sh @@ -0,0 +1,20 @@ +# `cargo vet` assumes all non-`crates.io` dependencies are first-party and +# therefore trusted. Due to our extensive usage of non-`crates.io` +# dependencies, we disagree with this policy and require no crates are +# identified are first-party. +# +# Unfortunately, `cargo vet` does not provide this functionality and the only +# way to mitigate it is to explicitly define all 'first-party' crates as +# third-party. Because there's no builtin way to enforce this, we use the +# following `sh` script to do so. + +# Fetch the graph +GRAPH=$(cargo vet dump-graph --depth first-party) +# Jump to its start +SUBGRAPH_START=$(echo "$GRAPH" | grep -n "^[[:space:]]*subgraph first-party$" | cut -d':' -f1) +GRAPH=$(echo "$GRAPH" | tail -n+$SUBGRAPH_START) +# Filter to its end +SUBGRAPH_END=$(echo "$GRAPH" | grep -n "^[[:space:]]*end$" | head -n1 | cut -d':' -f1) +GRAPH=$(echo "$GRAPH" | head -n$SUBGRAPH_END) +# Assert this is empty (just the start and end lines) +[ $(echo "$GRAPH" | wc -l) -eq 2 ] diff --git a/tests/shim-rpc/Cargo.toml b/tests/shim-rpc/Cargo.toml new file mode 100644 index 000000000..f40e6b795 --- /dev/null +++ b/tests/shim-rpc/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "serai-shim-rpc" +version = "0.1.0" +description = "A bespoke shim RPC node for testing Serai RPC clients without a real chain" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/tests/shim-rpc" +authors = ["Luke Parker ", "rafael_xmr "] +edition = "2021" +publish = false + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[package.metadata.cargo-machete] +ignored = ["serai-primitives"] + +[lints] +workspace = true + +[dependencies] +hex = { version = "0.4", default-features = false, features = ["std"] } +borsh = { version = "1", default-features = false, features = ["std", "derive"] } + +rand_core = { version = "0.6", default-features = false } +blake2 = { version = "0.11.0-rc.0", default-features = false } +schnorrkel = { version = "0.11", default-features = false, features = ["std"] } + +serai-primitives = { path = "../../substrate/primitives", features = ["std", "test-helpers"] } +serai-abi = { path = "../../substrate/abi", default-features = false, features = ["std"] } + +serde = { version = "1", default-features = false, features = ["std"] } +jsonrpsee = { version = "0.24", default-features = false, features = ["server"] } +tokio = { version = "1", default-features = false } + +[dev-dependencies] +serai-client-serai = { path = "../../substrate/client/serai" } diff --git a/tests/shim-rpc/LICENSE b/tests/shim-rpc/LICENSE new file mode 100644 index 000000000..2334e883e --- /dev/null +++ b/tests/shim-rpc/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2026 Serai Contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License Version 3 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/tests/shim-rpc/README.md b/tests/shim-rpc/README.md new file mode 100644 index 000000000..6c8a555c4 --- /dev/null +++ b/tests/shim-rpc/README.md @@ -0,0 +1,25 @@ +# serai-shim-rpc + +In-process **shim RPC node**, wire-compatible with the production `Serai` RPC serai (`serai-client-serai`). + +- Binds to an ephemeral `127.0.0.1` port and implements some RPC methods the real node exposes (`blockchain/*`, `validator-sets/*`, ...); +- Lets tests **pre-populate blocks with arbitrary events** (allocations, SetDecided, SetKeys, etc.) so the exact scenario is fully controlled; +- Supports **dynamic block addition** (`add_block_with_events`) and **error injection** (`set_error` / `clear_error`) during a test; +- Builds blocks using the same `IncrementalUnbalancedMerkleTree` + `BLOCK_BRANCH_TAG`/`BLOCK_LEAF_TAG` logic as the real chain, so `builds_upon` hashes are valid. + +## Example + +```rust +use serai_shim_rpc::{SeraiShimRpc, ShimState}; +use serai_client_serai::Serai; + +let mut state = ShimState::default(); +state.make_block(0, vec![vec![]]); +state.make_block(1, vec![vec![allocation_event(...), set_keys_event(...)]]); + +let shim_serai = SeraiShimRpc::start(state).await; +let serai = Serai::new(shim_serai.url()).unwrap(); + +let latest = serai.latest_finalized_block_number().await.unwrap(); +assert_eq!(latest, 1); +``` diff --git a/tests/shim-rpc/src/builder.rs b/tests/shim-rpc/src/builder.rs new file mode 100644 index 000000000..d08c95bf5 --- /dev/null +++ b/tests/shim-rpc/src/builder.rs @@ -0,0 +1,40 @@ +use serai_abi::Event; + +use crate::{state::ShimState, SeraiShimRpc}; + +/// Builder for constructing a [`SeraiShimRpc`] with pre-populated blocks. +#[must_use] +pub struct SeraiShimRpcBuilder { + blocks: Vec>>, +} + +impl SeraiShimRpcBuilder { + /// Create a new builder. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { blocks: Vec::new() } + } + + /// Add a single block with the given events (one `Vec` per transaction). + pub fn with_block(mut self, events: Vec>) -> Self { + self.blocks.push(events); + self + } + + /// Add multiple blocks, each with their own events. + pub fn with_blocks(mut self, blocks: Vec>>) -> Self { + self.blocks.extend(blocks); + self + } + + /// Build and start the shim RPC node. + pub async fn build(self) -> SeraiShimRpc { + let mut shim_state = ShimState::default(); + for (i, events) in self.blocks.into_iter().enumerate() { + let number = u64::try_from(i).unwrap() + 1; + shim_state.make_block(number, events); + } + + SeraiShimRpc::start(shim_state).await + } +} diff --git a/tests/shim-rpc/src/event_fuzzer.rs b/tests/shim-rpc/src/event_fuzzer.rs new file mode 100644 index 000000000..a757540ff --- /dev/null +++ b/tests/shim-rpc/src/event_fuzzer.rs @@ -0,0 +1,300 @@ +//! Random event, state, and block generator for fuzz testing. + +use std::collections::HashMap; + +use rand_core::{RngCore as _, OsRng}; + +use serai_abi::{ + primitives::{ + address::SeraiAddress, + crypto::KeyPair, + network_id::{ExternalNetworkId, NetworkId}, + validator_sets::{ExternalValidatorSet, KeyShares, Session, ValidatorSet}, + test_helpers::{ + random_external_address, random_external_key, random_keypair, random_serai_address, + }, + }, + validator_sets, Event, +}; + +use crate::test_helpers::*; + +/// Random event, state, and block generator. +pub struct EventFuzzer { + /// Available validator addresses. + pub validators: Vec, + /// All networks. + networks: Vec, + /// Running stake ledger: `(network, validator) -> accumulated_stake`. + // TODO: Track for `NetworkId`, not `ExternalNetworkId` + stakes: HashMap<(ExternalNetworkId, SeraiAddress), u64>, + /// Sets that have been decided but have not yet set their keys. + pending_keys: HashMap>, + /// Next session number per network. + pub next_session: HashMap, + /// Keypairs indexed by public key bytes, for signing cosigns. + pub keypairs: HashMap<[u8; 32], schnorrkel::Keypair>, +} + +impl EventFuzzer { + #[expect(clippy::new_without_default)] + pub fn new() -> Self { + // OsRng.next_u64() % 17 = 0..16, + 4 means from 4..20 validators per test + let num_validators = usize::try_from((OsRng.next_u64() % 17) + 4).unwrap(); + + let validators: Vec = + (0 .. num_validators).map(|_| random_serai_address(&mut OsRng)).collect(); + + let networks: Vec = NetworkId::all().collect(); + + Self { + validators, + networks, + stakes: HashMap::new(), + pending_keys: HashMap::new(), + next_session: HashMap::new(), + keypairs: HashMap::new(), + } + } + + /// Pick a random element from a slice. + fn pick(slice: &[T]) -> &T { + let i = OsRng.next_u64() % u64::try_from(slice.len()).unwrap(); + &slice[usize::try_from(i).unwrap()] + } + + /// Generate a random amount using a weighted distribution. + fn random_amount() -> u64 { + match OsRng.next_u64() % 100 { + 0 ..= 24 => (OsRng.next_u64() % 10) + 1, + 25 ..= 59 => (OsRng.next_u64() % 990) + 11, + 60 ..= 84 => (OsRng.next_u64() % 99_000) + 1_001, + _ => (OsRng.next_u64() % 9_900_000) + 100_001, + } + } + + /// Generate a random allocation event. + fn random_allocation(&mut self) -> Event { + let validator = *Self::pick(&self.validators.clone()); + let network = *Self::pick(&self.networks.clone()); + let amount = Self::random_amount(); + if let Ok(ext) = ExternalNetworkId::try_from(network) { + *self.stakes.entry((ext, validator)).or_default() += amount; + } + allocation_event(validator, network, amount) + } + + /// Generate a random deallocation event. Returns `None` if no validator has stake. + fn random_deallocation(&mut self) -> Option { + // ~25% chance of generating a Serai deallocation + if OsRng.next_u64() % 4 == 0 { + let validator = *Self::pick(&self.validators.clone()); + let amount = Self::random_amount(); + return Some(deallocation_event(validator, NetworkId::Serai, amount)); + } + + let candidates: Vec<((ExternalNetworkId, SeraiAddress), u64)> = self + .stakes + .iter() + .filter(|(_v, &stake)| stake > 0) + .map(|(&validator, &stake)| (validator, stake)) + .collect(); + if candidates.is_empty() { + return None; + } + let &((network, validator), current_stake) = Self::pick(&candidates); + // Use weighted amount, clamped to current_stake so we don't underflow + let amount = Self::random_amount().min(current_stake); + *self.stakes.entry((network, validator)).or_default() -= amount; + Some(deallocation_event(validator, NetworkId::External(network), amount)) + } + + /// Generate a random SetDecided event. + fn random_set_decided(&mut self) -> Option { + let external_networks: Vec = + self.networks.iter().copied().filter_map(|n| ExternalNetworkId::try_from(n).ok()).collect(); + let network = *Self::pick(&external_networks); + let session_num = *self.next_session.entry(network).or_insert(0); + let set = ExternalValidatorSet { network, session: Session(session_num) }; + + // Don't double-decide a set that's already pending keys + if self.pending_keys.contains_key(&set) { + return None; + } + + // Pick 1..=min(3, validators.len()) random validators for this set + let max_count = self.validators.len().min(3); + let count = + usize::try_from((OsRng.next_u64() % u64::try_from(max_count).unwrap()) + 1).unwrap(); + + // Shuffle-pick by swapping from a clone + let mut pool = self.validators.clone(); + let mut chosen = Vec::with_capacity(count); + for _ in 0 .. count { + let i = usize::try_from(OsRng.next_u64() % u64::try_from(pool.len()).unwrap()).unwrap(); + chosen.push(pool.swap_remove(i)); + } + + self.pending_keys.insert(set, chosen.clone()); + + let validators_with_shares: Vec<(SeraiAddress, KeyShares)> = + chosen.into_iter().map(|v| (v, KeyShares::ONE)).collect(); + + Some(set_decided_event( + ValidatorSet { network: NetworkId::External(network), session: Session(session_num) }, + validators_with_shares, + )) + } + + /// Generate a random SetKeys event for a pending (decided but not yet keyed) set. + fn random_set_keys(&mut self) -> Option { + if self.pending_keys.is_empty() { + return None; + } + + let keys: Vec = self.pending_keys.keys().copied().collect(); + let i = usize::try_from(OsRng.next_u64() % u64::try_from(keys.len()).unwrap()).unwrap(); + let set = keys[i]; + // Remove from pending + self.pending_keys.remove(&set); + + // Advance session for this network so the next SetDecided gets session+1 + *self.next_session.entry(set.network).or_insert(0) += 1; + + let (keypair, public) = random_keypair(&mut OsRng); + self.keypairs.insert(public.0, keypair); + let external_key = random_external_key(&mut OsRng); + let key_pair = KeyPair(public, external_key); + + Some(Event::ValidatorSets(validator_sets::Event::SetKeys { set, key_pair })) + } + + /// Generate a random BurnWithInstruction event. + pub fn random_burn(&mut self) -> Event { + burn_with_instruction_event( + random_serai_address(&mut OsRng), + random_external_address(&mut OsRng), + Self::random_amount(), + ) + } + + /// Generate random events for a single block. + fn generate_block_events(&mut self) -> Vec> { + let num_events = OsRng.next_u64() % 8; // 0..=7 events per block + if num_events == 0 { + return vec![]; + } + + let mut alloc_count = 0u64; + let mut dealloc_count = 0u64; + let mut set_decided_count = 0u64; + let mut set_keys_count = 0u64; + let mut burn_count = 0u64; + + for _ in 0 .. num_events { + match OsRng.next_u64() % 100 { + 0 ..= 35 => alloc_count += 1, + 36 ..= 55 => dealloc_count += 1, + 56 ..= 70 => set_decided_count += 1, + 71 ..= 85 => set_keys_count += 1, + 86 ..= 99 => burn_count += 1, + _ => unreachable!(), + } + } + + let mut events = Vec::new(); + + for _ in 0 .. alloc_count { + events.push(self.random_allocation()); + } + for _ in 0 .. dealloc_count { + if let Some(e) = self.random_deallocation() { + events.push(e); + } + } + for _ in 0 .. set_decided_count { + if let Some(e) = self.random_set_decided() { + events.push(e); + } + } + for _ in 0 .. set_keys_count { + if let Some(event) = self.random_set_keys() { + events.push(event); + } + } + for _ in 0 .. burn_count { + events.push(self.random_burn()); + } + + // Shuffle the events to test order-independence + for i in (1 .. events.len()).rev() { + let j = usize::try_from(OsRng.next_u64() % u64::try_from(i + 1).unwrap()).unwrap(); + events.swap(i, j); + } + + if events.is_empty() { + vec![] + } else { + vec![events] + } + } + + /// Force a complete allocation of SetDecided -> SetKeys sequence for every external network, + /// guaranteeing a global session with multiple validator sets will form. + fn force_keygen(&mut self) -> [Vec>; 3] { + let external_networks: Vec = + self.networks.iter().copied().filter_map(|n| ExternalNetworkId::try_from(n).ok()).collect(); + + let mut alloc_events = Vec::new(); + let mut decided_events = Vec::new(); + let mut keys_events = Vec::new(); + + for &network in &external_networks { + let validator = *Self::pick(&self.validators.clone()); + let amount = Self::random_amount(); + + *self.stakes.entry((network, validator)).or_default() += amount; + alloc_events.push(allocation_event(validator, NetworkId::External(network), amount)); + + let session_num = *self.next_session.entry(network).or_insert(0); + let set = ExternalValidatorSet { network, session: Session(session_num) }; + self.pending_keys.insert(set, vec![validator]); + decided_events.push(set_decided_event( + ValidatorSet { network: NetworkId::External(network), session: Session(session_num) }, + vec![(validator, KeyShares::ONE)], + )); + + self.pending_keys.remove(&set); + *self.next_session.entry(network).or_insert(0) += 1; + let (keypair, public) = random_keypair(&mut OsRng); + self.keypairs.insert(public.0, keypair); + let external_key = random_external_key(&mut OsRng); + keys_events.push(Event::ValidatorSets(validator_sets::Event::SetKeys { + set, + key_pair: KeyPair(public, external_key), + })); + } + + [vec![alloc_events], vec![decided_events], vec![keys_events]] + } + + /// Generate `count` blocks of random events. + pub fn generate_blocks(&mut self, count: usize) -> Vec>> { + let mut blocks = Vec::with_capacity(count); + for _ in 0 .. count { + blocks.push(self.generate_block_events()); + } + blocks + } + + /// Generate `count` blocks, starting with a forced keygen sequence (3 blocks) + /// to guarantee at least one global session forms, followed by random blocks. + pub fn generate_blocks_with_keygen(&mut self, count: usize) -> Vec>> { + assert!(count >= 4, "need at least 4 blocks for forced keygen + one random block"); + + let [alloc, decided, keys] = self.force_keygen(); + let mut blocks = vec![alloc, decided, keys]; + blocks.extend(self.generate_blocks(count - 3)); + blocks + } +} diff --git a/tests/shim-rpc/src/lib.rs b/tests/shim-rpc/src/lib.rs new file mode 100644 index 000000000..f2b9a88cb --- /dev/null +++ b/tests/shim-rpc/src/lib.rs @@ -0,0 +1,159 @@ +#![allow(clippy::std_instead_of_alloc, clippy::std_instead_of_core)] + +use std::{sync::Arc, net::SocketAddr, env}; + +use serai_abi::{ + primitives::{BlockHash, merkle::IncrementalUnbalancedMerkleTree}, + Event, +}; + +use jsonrpsee::server::{ServerBuilder, ServerHandle}; +use tokio::sync::RwLock; + +pub mod state; +pub mod rpc; +pub mod builder; +pub mod test_helpers; + +pub mod event_fuzzer; + +pub use state::*; +pub use builder::SeraiShimRpcBuilder; + +/// A bespoke shim RPC node that speaks JSON-RPC 2.0 over HTTP, +/// wire-compatible with the production `Serai` client. +pub struct SeraiShimRpc { + url: String, + state: SharedState, + _handle: ServerHandle, +} + +impl SeraiShimRpc { + /// Create a builder for configuring and starting a shim RPC node. + pub fn builder() -> SeraiShimRpcBuilder { + SeraiShimRpcBuilder::new() + } + + /// Start a shim RPC node with the given initial state, binding to an ephemeral port. + pub async fn start(initial_state: ShimState) -> Self { + let state = Arc::new(RwLock::new(initial_state)); + let rpc_module = rpc::build_rpc_module(state.clone()).expect("failed to build RPC module"); + + let server = ServerBuilder::default() + .build(SocketAddr::from(([127, 0, 0, 1], 0))) + .await + .expect("failed to bind shim RPC node server"); + + let addr = server.local_addr().expect("server should have a local address"); + Self { url: format!("http://{addr}"), state, _handle: server.start(rpc_module) } + } + + /// The HTTP URL this shim is listening on. + pub fn url(&self) -> String { + self.url.clone() + } + + /// Create a block at the given number with events. + /// Returns the hash of the newly created block. + pub async fn make_block(&self, number: u64, events: Vec>) -> BlockHash { + self.state.write().await.make_block(number, events) + } + + /// Add a block with events dynamically (during a test). + /// The block number is automatically determined as the next sequential block. + /// Returns the hash of the newly created block. + pub async fn add_block_with_events(&self, events: Vec>) -> BlockHash { + let mut state = self.state.write().await; + let number = + state.latest_finalized_block_number().map(|latest_block| latest_block + 1).unwrap_or(0); + state.make_block(number, events) + } + + /// Get the shim's current `builds_upon` merkle tree state. + pub async fn builds_upon(&self) -> IncrementalUnbalancedMerkleTree { + self.state.read().await.builds_upon.clone() + } + + /// Inject an error for a specific RPC method. Any call to this method will fail. + pub async fn set_error(&self, method: &str, message: &str) { + let mut state = self.state.write().await; + state.errors.method_errors.insert(method.to_owned(), message.to_owned()); + } + + /// Clear an injected error for a specific RPC method. + pub async fn clear_error(&self, method: &str) { + let mut state = self.state.write().await; + state.errors.method_errors.remove(method); + } + + /// Clear all injected errors. + pub async fn clear_all_errors(&self) { + let mut state = self.state.write().await; + state.errors = ErrorInjection::default(); + } + + /// Inject an error for a specific RPC method + block number combination. + pub async fn set_block_number_error(&self, method: &str, number: u64, message: &str) { + let mut state = self.state.write().await; + state.errors.block_number_errors.insert((method.to_owned(), number), message.to_owned()); + } + + /// Clear an injected error for a specific RPC method + block number. + pub async fn clear_block_number_error(&self, method: &str, number: u64) { + let mut state = self.state.write().await; + state.errors.block_number_errors.remove(&(method.to_owned(), number)); + } + + /// Inject an error for a specific RPC method + block hash combination. + pub async fn set_block_hash_error(&self, method: &str, hash: BlockHash, message: &str) { + let mut state = self.state.write().await; + state.errors.block_hash_errors.insert((method.to_owned(), hash), message.to_owned()); + } + + /// Clear an injected error for a specific RPC method + block hash. + pub async fn clear_block_hash_error(&self, method: &str, hash: BlockHash) { + let mut state = self.state.write().await; + state.errors.block_hash_errors.remove(&(method.to_owned(), hash)); + } + + /// Hide a block so that `blockchain/block` returns `None` for it. + pub async fn set_block_missing(&self, number: u64) { + self.state.write().await.missing_blocks.insert(number); + } + + /// Un-hide a previously hidden block. + pub async fn clear_block_missing(&self, number: u64) { + self.state.write().await.missing_blocks.remove(&number); + } + + /// Remove a block (and its associated events) from the shim state. + pub async fn remove_block(&self, number: u64) { + let mut state = self.state.write().await; + state.remove_block(number); + } + + /// Create a non-linear block (wrong `builds_upon`) without advancing the chain state. + pub async fn make_non_linear_block(&self, number: u64, events: Vec>) -> BlockHash { + let mut state = self.state.write().await; + state.make_non_linear_block(number, events) + } + + /// Access the underlying shared state directly. + pub fn state(&self) -> &SharedState { + &self.state + } + + /// Set the probability (0–100) that any RPC request randomly fails. + /// + /// 0 disables fuzzing (the default), 100 fails every request. + /// If the `SERAI_SHIM_RPC_NO_ERROR` env var is set, the rate is forced to 0. + pub async fn set_failure_rate(&self, percent: u8) { + let effective = if env::var("SERAI_SHIM_RPC_NO_ERROR").is_ok() { 0 } else { percent }; + self.state.write().await.errors.failure_rate = effective; + } + + /// Disable random request failures. + pub async fn clear_failure_rate(&self) { + self.state.write().await.errors.failure_rate = 0; + } +} diff --git a/tests/shim-rpc/src/rpc.rs b/tests/shim-rpc/src/rpc.rs new file mode 100644 index 000000000..d97aa8608 --- /dev/null +++ b/tests/shim-rpc/src/rpc.rs @@ -0,0 +1,290 @@ +use serai_abi::{ + primitives::{ + BlockHash, + network_id::{ExternalNetworkId, NetworkId}, + validator_sets::{ExternalValidatorSet, Session, ValidatorSet}, + }, + Event, +}; + +use serde::Deserialize; +use jsonrpsee::{ + RpcModule, + types::{error::ErrorObjectOwned, params::Params}, +}; + +use crate::state::{SharedState, ShimState}; + +/// Typed RPC errors mirroring `substrate/node/src/rpc/utils.rs`. +enum Error { + Internal(String), + InvalidRequest(String), + InvalidStateReference, + #[allow(dead_code)] + InvalidTransaction(String), +} + +impl From for ErrorObjectOwned { + fn from(error: Error) -> Self { + match error { + Error::Internal(msg) => ErrorObjectOwned::owned(-1, msg, Option::<()>::None), + Error::InvalidRequest(msg) => ErrorObjectOwned::owned(-2, msg, Option::<()>::None), + Error::InvalidStateReference => ErrorObjectOwned::owned( + -3, + "the block used as the reference was not locally held", + Option::<()>::None, + ), + Error::InvalidTransaction(msg) => ErrorObjectOwned::owned( + -4, + format!("transaction was not accepted to the mempool: {msg}"), + Option::<()>::None, + ), + } + } +} + +/// Resolve a block hash from JSON-RPC params. +/// +/// Mirrors `substrate/node/src/rpc/utils.rs`: +/// - `{ "block": "hex_hash" }` = lookup by hash +/// - `{ "block": 123 }` = lookup by number +fn resolve_block_hash(params: &Params, state: &ShimState) -> Result, Error> { + #[derive(Deserialize)] + struct BlockByHash { + block: String, + } + #[derive(Deserialize)] + struct BlockByNumber { + block: u64, + } + + if let Ok(by_hash) = params.parse::() { + let Some(hash_bytes) = + hex::decode(&by_hash.block).ok().and_then(|bytes| <[u8; 32]>::try_from(bytes).ok()) + else { + return Err(Error::InvalidRequest("requested block hash wasn't a valid hash".to_owned())); + }; + let block_hash = BlockHash(hash_bytes); + if state.block_number_by_hash.contains_key(&block_hash) { + Ok(Some(block_hash)) + } else { + Ok(None) + } + } else if let Ok(by_number) = params.parse::() { + Ok(state.block_hash_by_number(by_number.block)) + } else { + Err(Error::InvalidRequest("requested block wasn't a valid hash nor number".to_owned())) + } +} + +fn network_from_str(network: &str) -> Result { + Ok(match network.to_lowercase().as_str() { + "serai" => NetworkId::Serai, + "bitcoin" => NetworkId::External(ExternalNetworkId::Bitcoin), + "ethereum" => NetworkId::External(ExternalNetworkId::Ethereum), + "monero" => NetworkId::External(ExternalNetworkId::Monero), + _ => return Err(Error::InvalidRequest("unrecognized network requested".to_owned())), + }) +} + +fn parse_network(params: &Params) -> Result { + #[derive(Deserialize)] + struct Network { + network: String, + } + let network: Network = + params.parse().map_err(|_| Error::InvalidRequest(r#"missing "network" field"#.to_owned()))?; + network_from_str(&network.network) +} + +fn parse_set(params: &Params) -> Result { + #[derive(Deserialize)] + struct Set { + network: String, + session: u32, + } + let set: Set = params + .parse() + .map_err(|_| Error::InvalidRequest(r#"missing "network"/"session" fields"#.to_owned()))?; + let network = network_from_str(&set.network)?; + ExternalValidatorSet::try_from(ValidatorSet { network, session: Session(set.session) }).map_err( + |()| Error::InvalidRequest("requested keys for a non-external validator set".to_owned()), + ) +} + +/// Build the RPC module with all method handlers matching the real Serai node. +pub fn build_rpc_module(state: SharedState) -> Result, ErrorObjectOwned> { + let mut module = RpcModule::new(state); + + module + .register_async_method( + "blockchain/latest_finalized_block_number", + async |_params, state, _ext| { + let state = state.read().await; + if let Some(err) = + state.errors.check_random_failure("blockchain/latest_finalized_block_number") + { + return Err(Error::Internal(err)); + } + if let Some(err) = state.errors.check_method("blockchain/latest_finalized_block_number") { + return Err(Error::Internal(err.to_owned())); + } + Ok(state.latest_finalized_block_number().unwrap_or(0)) + }, + ) + .map_err(|e| Error::Internal(e.to_string()))?; + + module + .register_async_method("blockchain/is_finalized", async |params, state, _ext| { + let state = state.read().await; + if let Some(err) = state.errors.check_random_failure("blockchain/is_finalized") { + return Err(Error::Internal(err)); + } + if let Some(err) = state.errors.check_method("blockchain/is_finalized") { + return Err(Error::Internal(err.to_owned())); + } + let Some(block_hash) = resolve_block_hash(¶ms, &state)? else { + return Ok(false); + }; + if let Some(err) = state.errors.check_block_hash("blockchain/is_finalized", &block_hash) { + return Err(Error::Internal(err.to_owned())); + } + Ok(state.block_number_by_hash.contains_key(&block_hash)) + }) + .map_err(|e| Error::Internal(e.to_string()))?; + + module + .register_async_method("blockchain/block", async |params, state, _ext| { + let state = state.read().await; + if let Some(err) = state.errors.check_random_failure("blockchain/block") { + return Err(Error::Internal(err)); + } + if let Some(err) = state.errors.check_method("blockchain/block") { + return Err(Error::Internal(err.to_owned())); + } + let Some(block_hash) = resolve_block_hash(¶ms, &state)? else { + return Ok(None); + }; + if let Some(err) = state.errors.check_block_hash("blockchain/block", &block_hash) { + return Err(Error::Internal(err.to_owned())); + } + let Some(&number) = state.block_number_by_hash.get(&block_hash) else { + return Ok(None); + }; + if let Some(err) = state.errors.check_block_number("blockchain/block", number) { + return Err(Error::Internal(err.to_owned())); + } + if state.missing_blocks.contains(&number) { + return Ok(None); + } + let Some(block) = state.blocks_by_number.get(&number) else { + return Ok(None); + }; + Ok(Some(hex::encode(borsh::to_vec(block).unwrap()))) + }) + .map_err(|e| Error::Internal(e.to_string()))?; + + module + .register_async_method("blockchain/events", async |params, state, _ext| { + let state = state.read().await; + if let Some(err) = state.errors.check_random_failure("blockchain/events") { + return Err(Error::Internal(err)); + } + if let Some(err) = state.errors.check_method("blockchain/events") { + return Err(Error::Internal(err.to_owned())); + } + let Some(block_hash) = resolve_block_hash(¶ms, &state)? else { + return Err(Error::InvalidStateReference); + }; + if let Some(err) = state.errors.check_block_hash("blockchain/events", &block_hash) { + return Err(Error::Internal(err.to_owned())); + } + let events = state.events_by_hash.get(&block_hash).cloned().unwrap_or_else(|| vec![vec![]]); + Ok( + events + .into_iter() + .map(|events_per_tx: Vec| { + events_per_tx + .into_iter() + .map(|event| hex::encode(borsh::to_vec(&event).unwrap())) + .collect::>() + }) + .collect::>>(), + ) + }) + .map_err(|e| Error::Internal(e.to_string()))?; + + module + .register_async_method("validator-sets/current_session", async |params, state, _ext| { + let state = state.read().await; + if let Some(err) = state.errors.check_random_failure("validator-sets/current_session") { + return Err(Error::Internal(err)); + } + if let Some(err) = state.errors.check_method("validator-sets/current_session") { + return Err(Error::Internal(err.to_owned())); + } + let Some(block_hash) = resolve_block_hash(¶ms, &state)? else { + return Err(Error::InvalidStateReference); + }; + let network = parse_network(¶ms)?; + let vs = state.validator_sets_for_block(&block_hash); + Ok(vs.sessions.get(&network).map(|s| s.0)) + }) + .map_err(|e| Error::Internal(e.to_string()))?; + + module + .register_async_method("validator-sets/current_stake", async |params, state, _ext| { + let state = state.read().await; + if let Some(err) = state.errors.check_random_failure("validator-sets/current_stake") { + return Err(Error::Internal(err)); + } + if let Some(err) = state.errors.check_method("validator-sets/current_stake") { + return Err(Error::Internal(err.to_owned())); + } + let Some(block_hash) = resolve_block_hash(¶ms, &state)? else { + return Err(Error::InvalidStateReference); + }; + let network = parse_network(¶ms)?; + let vs = state.validator_sets_for_block(&block_hash); + Ok(vs.stakes.get(&network).map(|a| a.0)) + }) + .map_err(|e| Error::Internal(e.to_string()))?; + + module + .register_async_method("validator-sets/keys", async |params, state, _ext| { + let state = state.read().await; + if let Some(err) = state.errors.check_random_failure("validator-sets/keys") { + return Err(Error::Internal(err)); + } + if let Some(err) = state.errors.check_method("validator-sets/keys") { + return Err(Error::Internal(err.to_owned())); + } + let Some(block_hash) = resolve_block_hash(¶ms, &state)? else { + return Err(Error::InvalidStateReference); + }; + let set = parse_set(¶ms)?; + let vs = state.validator_sets_for_block(&block_hash); + Ok(vs.keys.get(&set).map(|kp| hex::encode(borsh::to_vec(kp).unwrap()))) + }) + .map_err(|e| Error::Internal(e.to_string()))?; + + module + .register_async_method("validator-sets/current_validators", async |params, state, _ext| { + let state = state.read().await; + if let Some(err) = state.errors.check_random_failure("validator-sets/current_validators") { + return Err(Error::Internal(err)); + } + if let Some(err) = state.errors.check_method("validator-sets/current_validators") { + return Err(Error::Internal(err.to_owned())); + } + let Some(block_hash) = resolve_block_hash(¶ms, &state)? else { + return Err(Error::InvalidStateReference); + }; + let network = parse_network(¶ms)?; + let vs = state.validator_sets_for_block(&block_hash); + Ok(vs.validators.get(&network).map(|v| v.iter().map(ToString::to_string).collect::>())) + }) + .map_err(|e| Error::Internal(e.to_string()))?; + + Ok(module) +} diff --git a/tests/shim-rpc/src/state.rs b/tests/shim-rpc/src/state.rs new file mode 100644 index 000000000..6da8d2245 --- /dev/null +++ b/tests/shim-rpc/src/state.rs @@ -0,0 +1,194 @@ +use std::{ + sync::Arc, + collections::{HashSet, HashMap}, + time::SystemTime, +}; + +use rand_core::{RngCore as _, OsRng}; + +use blake2::{Digest as _, Blake2b256}; + +use serai_abi::{ + primitives::{ + BlockHash, + crypto::KeyPair, + merkle::{UnbalancedMerkleTree, IncrementalUnbalancedMerkleTree}, + address::SeraiAddress, + network_id::{ExternalNetworkId, NetworkId}, + validator_sets::{Session, ExternalValidatorSet}, + balance::Amount, + }, + BLOCK_LEAF_TAG, BLOCK_BRANCH_TAG, HeaderV1, Header, Block, Event, +}; + +use tokio::sync::RwLock; + +/// Per-block validator-sets state. +#[derive(Clone, Debug, Default)] +pub struct ValidatorSetsState { + pub sessions: HashMap, + pub stakes: HashMap, + pub keys: HashMap, + pub validators: HashMap>, + pub pending_slash_reports: HashMap, +} + +/// Injectable failures at three levels. +#[derive(Clone, Debug, Default)] +pub struct ErrorInjection { + /// Any call to the method fails with this message. + pub method_errors: HashMap, + /// Fails for a specific block number. + pub block_number_errors: HashMap<(String, u64), String>, + /// Fails for a specific block hash. + pub block_hash_errors: HashMap<(String, BlockHash), String>, + /// Probability (0–100) that any request randomly fails. 0 = never, 100 = always. + pub failure_rate: u8, +} + +impl ErrorInjection { + /// Check if this request should randomly fail based on the configured `failure_rate`. + pub fn check_random_failure(&self, method: &str) -> Option { + ((OsRng.next_u32() % 100) < u32::from(self.failure_rate)) + .then(|| format!("fuzz: random failure on `{method}` (rate={}%)", self.failure_rate)) + } + + /// Check if an error should be injected for this method call. + pub fn check_method(&self, method: &str) -> Option<&String> { + self.method_errors.get(method) + } + + /// Check if an error should be injected for this method + block number. + pub fn check_block_number(&self, method: &str, number: u64) -> Option<&String> { + self.block_number_errors.get(&(method.to_owned(), number)) + } + + /// Check if an error should be injected for this method + block hash. + pub fn check_block_hash(&self, method: &str, hash: &BlockHash) -> Option<&String> { + self.block_hash_errors.get(&(method.to_owned(), *hash)) + } +} + +/// The shared mutable state backing the shim RPC node. +pub struct ShimState { + pub blocks_by_number: HashMap, + pub block_number_by_hash: HashMap, + pub events_by_hash: HashMap>>, + pub builds_upon: IncrementalUnbalancedMerkleTree, + pub published_transactions: Vec>, + pub default_validator_sets: ValidatorSetsState, + pub validator_sets_by_block: HashMap, + pub errors: ErrorInjection, + /// Block numbers that `blockchain/block` should return `None` for (simulates "not found"). + pub missing_blocks: HashSet, +} + +impl Default for ShimState { + fn default() -> Self { + Self { + blocks_by_number: HashMap::new(), + block_number_by_hash: HashMap::new(), + events_by_hash: HashMap::new(), + builds_upon: IncrementalUnbalancedMerkleTree::new(), + published_transactions: Vec::new(), + default_validator_sets: ValidatorSetsState::default(), + validator_sets_by_block: HashMap::new(), + errors: ErrorInjection::default(), + missing_blocks: HashSet::new(), + } + } +} + +impl ShimState { + /// Construct a block and register it. + pub fn make_block(&mut self, number: u64, events: Vec>) -> BlockHash { + let block = Block { + header: Header::V1(HeaderV1 { + number, + builds_upon: self.builds_upon.clone().calculate(BLOCK_BRANCH_TAG), + proposer: SeraiAddress([0; 32]), + unix_time_in_millis: u64::try_from( + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis(), + ) + .unwrap(), + transactions_commitment: UnbalancedMerkleTree::EMPTY, + // TODO: Properly populate `events_commitment` + events_commitment: UnbalancedMerkleTree::EMPTY, + consensus_commitment: [0; 32], + }), + transactions: vec![], + }; + + let block_hash = block.header.hash(); + + self.builds_upon.append( + BLOCK_BRANCH_TAG, + Blake2b256::new_with_prefix([BLOCK_LEAF_TAG]).chain_update(block_hash.0).finalize().into(), + ); + + self.block_number_by_hash.insert(block_hash, number); + self.blocks_by_number.insert(number, block); + self.events_by_hash.insert(block_hash, events); + + block_hash + } + + /// The latest finalized block number. + pub fn latest_finalized_block_number(&self) -> Option { + self.blocks_by_number.keys().copied().max() + } + + /// Create a block whose `builds_upon` header value comes from an empty tree, + /// making it invalid with respect to the actual chain. + /// + /// Unlike [`Self::make_block`], this does **not** advance the internal + /// `builds_upon` state, so subsequent calls to `make_block` remain valid. + pub fn make_non_linear_block(&mut self, number: u64, events: Vec>) -> BlockHash { + let block = Block { + header: Header::V1(HeaderV1 { + number, + builds_upon: IncrementalUnbalancedMerkleTree::new().calculate(BLOCK_BRANCH_TAG), + proposer: SeraiAddress([0; 32]), + unix_time_in_millis: u64::try_from( + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis(), + ) + .unwrap(), + transactions_commitment: UnbalancedMerkleTree::EMPTY, + events_commitment: UnbalancedMerkleTree::EMPTY, + consensus_commitment: [0; 32], + }), + transactions: vec![], + }; + + let block_hash = block.header.hash(); + + // Register the block but do not update builds_upon + self.block_number_by_hash.insert(block_hash, number); + self.blocks_by_number.insert(number, block); + self.events_by_hash.insert(block_hash, events); + + block_hash + } + + /// Remove a block from all maps. + pub fn remove_block(&mut self, number: u64) { + if let Some(block) = self.blocks_by_number.remove(&number) { + let hash = block.header.hash(); + self.block_number_by_hash.remove(&hash); + self.events_by_hash.remove(&hash); + } + } + + /// Look up a block hash by block number. + pub fn block_hash_by_number(&self, number: u64) -> Option { + self.blocks_by_number.get(&number).map(|block| block.header.hash()) + } + + /// Get the validator-sets state for a specific block, falling back to the default. + pub fn validator_sets_for_block(&self, hash: &BlockHash) -> &ValidatorSetsState { + self.validator_sets_by_block.get(hash).unwrap_or(&self.default_validator_sets) + } +} + +/// Thread-safe shared state handle. +pub type SharedState = Arc>; diff --git a/tests/shim-rpc/src/test_helpers.rs b/tests/shim-rpc/src/test_helpers.rs new file mode 100644 index 000000000..90ea247c4 --- /dev/null +++ b/tests/shim-rpc/src/test_helpers.rs @@ -0,0 +1,39 @@ +//! Test helper functions for constructing common Serai ABI events. + +use serai_abi::{ + primitives::{ + address::*, network_id::*, coin::*, balance::*, validator_sets::*, instructions::*, + }, + *, +}; + +pub fn set_decided_event(set: ValidatorSet, validators: Vec<(SeraiAddress, KeyShares)>) -> Event { + Event::ValidatorSets(validator_sets::Event::SetDecided { set, validators }) +} + +pub fn allocation_event(validator: SeraiAddress, network: NetworkId, amount: u64) -> Event { + Event::ValidatorSets(validator_sets::Event::Allocation { + validator, + network, + amount: Amount(amount), + }) +} + +pub fn deallocation_event(validator: SeraiAddress, network: NetworkId, amount: u64) -> Event { + Event::ValidatorSets(validator_sets::Event::Deallocation { + validator, + network, + amount: Amount(amount), + timeline: DeallocationTimeline::Immediate, + }) +} + +pub fn burn_with_instruction_event(from: SeraiAddress, to: ExternalAddress, amount: u64) -> Event { + Event::Coins(coins::Event::BurnWithInstruction { + from, + instruction: OutInstructionWithBalance { + instruction: OutInstruction::Transfer(to), + balance: ExternalBalance { coin: ExternalCoin::Bitcoin, amount: Amount(amount) }, + }, + }) +} diff --git a/tests/shim-rpc/tests/integration.rs b/tests/shim-rpc/tests/integration.rs new file mode 100644 index 000000000..a1bbd740e --- /dev/null +++ b/tests/shim-rpc/tests/integration.rs @@ -0,0 +1,244 @@ +use rand_core::{RngCore as _, OsRng}; + +use serai_client_serai::{ + *, + abi::primitives::{ + network_id::*, validator_sets::*, balance::*, test_helpers::random_serai_address, + }, +}; + +use serai_shim_rpc::{*, test_helpers::*}; + +#[tokio::test] +async fn test_basic_block_and_number() { + let shim = SeraiShimRpcBuilder::new() + .with_block(vec![vec![]]) + .with_block(vec![vec![]]) + .with_block(vec![vec![]]) + .build() + .await; + + let serai = Serai::new(shim.url()).unwrap(); + + let latest = serai.latest_finalized_block_number().await.unwrap(); + assert_eq!(latest, 3, "latest finalized block number should be 3"); + + let block = serai.block_by_number(1).await.unwrap(); + assert!(block.is_some(), "block 1 should exist"); + assert_eq!(block.unwrap().header.number(), 1, "block 1 should have number 1"); + + let block2 = serai.block_by_number(2).await.unwrap().unwrap(); + assert_eq!(block2.header.number(), 2, "block 2 should have number 2"); + + let block3 = serai.block_by_number(3).await.unwrap().unwrap(); + assert_eq!(block3.header.number(), 3, "block 3 should have number 3"); + + let none = serai.block_by_number(5 + (OsRng.next_u64() % 1000)).await.unwrap(); + assert!(none.is_none(), "non-existent block should return None"); +} + +#[tokio::test] +async fn test_block_by_hash() { + let shim = SeraiShimRpcBuilder::new().with_block(vec![vec![]]).build().await; + let serai = Serai::new(shim.url()).unwrap(); + + let block = serai.block_by_number(1).await.unwrap().unwrap(); + let hash = block.header.hash(); + + let block_by_hash = serai.block(hash).await.unwrap(); + assert!(block_by_hash.is_some(), "block lookup by hash should return Some"); + assert_eq!( + block_by_hash.unwrap().header.number(), + 1, + "block looked up by hash should be block 1" + ); + + let finalized = serai.finalized(hash).await.unwrap(); + assert!(finalized, "block should be finalized"); +} + +#[tokio::test] +async fn test_events_round_trip() { + let validator = random_serai_address(&mut OsRng); + let events = vec![vec![ + allocation_event(validator, NetworkId::External(ExternalNetworkId::Bitcoin), OsRng.next_u64()), + set_decided_event( + ValidatorSet { + network: NetworkId::External(ExternalNetworkId::Bitcoin), + session: Session(0), + }, + vec![(validator, KeyShares::ONE)], + ), + ]]; + + let shim = SeraiShimRpcBuilder::new().with_block(events.clone()).build().await; + + let serai = Serai::new(shim.url()).unwrap(); + + let block = serai.block_by_number(1).await.unwrap().unwrap(); + let hash = block.header.hash(); + + let rpc_events = serai.events(hash).await.unwrap(); + assert_eq!( + rpc_events.validator_sets().events().cloned().map(serai_abi::Event::from).collect::>(), + events.into_iter().flat_map(Vec::into_iter).collect::>() + ); +} + +#[tokio::test] +async fn test_dynamic_block_addition() { + let shim = SeraiShimRpc::builder().build().await; + + let serai = Serai::new(shim.url()).unwrap(); + + let latest = serai.latest_finalized_block_number().await.unwrap(); + assert_eq!(latest, 0, "initially no blocks should exist"); + + let hash = shim.make_block(1, vec![vec![]]).await; + + let latest = serai.latest_finalized_block_number().await.unwrap(); + assert_eq!(latest, 1, "should have 1 block after adding one"); + + let block = serai.block(hash).await.unwrap(); + assert!(block.is_some(), "block should be retrievable by hash"); + assert_eq!(block.unwrap().header.number(), 1, "dynamically added block should be block 1"); + + shim.add_block_with_events(vec![vec![]]).await; + let latest = serai.latest_finalized_block_number().await.unwrap(); + assert_eq!(latest, 2, "should have 2 blocks after adding another"); +} + +#[tokio::test] +async fn test_error_injection() { + let shim = SeraiShimRpcBuilder::new().with_block(vec![vec![]]).build().await; + + let serai = Serai::new(shim.url()).unwrap(); + + let latest = serai.latest_finalized_block_number().await.unwrap(); + assert_eq!(latest, 1, "should work normally before error injection"); + + shim.set_error("blockchain/latest_finalized_block_number", "simulated failure").await; + + let result = serai.latest_finalized_block_number().await; + assert!(result.is_err(), "should fail after error injection"); + let err_msg = format!("{}", result.unwrap_err()); + assert!(err_msg.contains("simulated failure"), "error was: {err_msg}"); + + shim.clear_error("blockchain/latest_finalized_block_number").await; + + let latest = serai.latest_finalized_block_number().await.unwrap(); + assert_eq!(latest, 1, "should work again after clearing error"); +} + +#[tokio::test] +async fn test_clear_all_errors() { + let shim = SeraiShimRpcBuilder::new().with_block(vec![vec![]]).build().await; + + let serai = Serai::new(shim.url()).unwrap(); + + shim.set_error("blockchain/latest_finalized_block_number", "err1").await; + shim.set_error("blockchain/block", "err2").await; + + assert!( + serai.latest_finalized_block_number().await.is_err(), + "latest_finalized should fail with injected error" + ); + assert!( + serai.block_by_number(1).await.is_err(), + "block_by_number should fail with injected error" + ); + + shim.clear_all_errors().await; + + assert_eq!( + serai.latest_finalized_block_number().await.unwrap(), + 1, + "latest_finalized should work after clearing all errors" + ); + assert!( + serai.block_by_number(1).await.unwrap().is_some(), + "block_by_number should work after clearing all errors" + ); +} + +#[tokio::test] +async fn test_builds_upon_chain() { + let shim = SeraiShimRpcBuilder::new() + .with_block(vec![vec![]]) + .with_block(vec![vec![]]) + .with_block(vec![vec![]]) + .build() + .await; + + let serai = Serai::new(shim.url()).unwrap(); + + let block1 = serai.block_by_number(1).await.unwrap().unwrap(); + let block2 = serai.block_by_number(2).await.unwrap().unwrap(); + let block3 = serai.block_by_number(3).await.unwrap().unwrap(); + + assert_ne!( + block1.header.builds_upon(), + block2.header.builds_upon(), + "block 1 and 2 should have distinct builds_upon" + ); + assert_ne!( + block2.header.builds_upon(), + block3.header.builds_upon(), + "block 2 and 3 should have distinct builds_upon" + ); + + assert_ne!( + block1.header.hash(), + block2.header.hash(), + "block 1 and 2 should have distinct hashes" + ); + assert_ne!( + block2.header.hash(), + block3.header.hash(), + "block 2 and 3 should have distinct hashes" + ); +} + +#[tokio::test] +async fn test_publish_transaction() { + let shim = SeraiShimRpc::builder().with_block(vec![vec![]]).build().await; + + { + let state = shim.state().read().await; + assert!(state.published_transactions.is_empty(), "no transactions should exist initially"); + } + + { + let mut state = shim.state().write().await; + state.published_transactions.push(vec![0xDE, 0xAD]); + } + { + let state = shim.state().read().await; + assert_eq!(state.published_transactions.len(), 1, "should have 1 published transaction"); + assert_eq!(state.published_transactions[0], vec![0xDE, 0xAD], "transaction bytes should match"); + } +} + +#[tokio::test] +async fn test_validator_sets_state() { + let shim = SeraiShimRpc::builder().with_block(vec![vec![]]).build().await; + + let serai = Serai::new(shim.url()).unwrap(); + + { + let mut state = shim.state().write().await; + let network = NetworkId::External(ExternalNetworkId::Bitcoin); + state.default_validator_sets.sessions.insert(network, Session(5)); + state.default_validator_sets.stakes.insert(network, Amount(1_000_000)); + } + + let serai_state = serai.state().await.unwrap(); + + let session = + serai_state.current_session(NetworkId::External(ExternalNetworkId::Bitcoin)).await.unwrap(); + assert_eq!(session, Some(Session(5)), "current session should be 5"); + + let stake = + serai_state.current_stake(NetworkId::External(ExternalNetworkId::Bitcoin)).await.unwrap(); + assert_eq!(stake, Some(Amount(1_000_000)), "current stake should be 1_000_000"); +}