diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index cd10f15..080fc4d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,20 +32,20 @@ jobs: - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 with: - path: reftests/vectors - key: ${{ runner.os }}-reftest-vectors-coverage-${{ hashFiles('reftests/src/lib.rs', 'reftests/src/vectors.rs', 'reftests/src/vectors/*.rs') }} + path: tests/vectors + key: ${{ runner.os }}-reftest-vectors-coverage-${{ hashFiles('tests/src/lib.rs', 'tests/src/vectors.rs', 'tests/src/vectors/*.rs') }} restore-keys: | ${{ runner.os }}-reftest-vectors-coverage- - name: coverage run: | cargo llvm-cov clean --workspace --locked - cargo llvm-cov --locked --no-report run --release -p reftests --bin reftests-minimal --no-default-features --features minimal - cargo llvm-cov --locked --no-report run --release -p reftests --bin reftests + cargo llvm-cov --locked --no-report run --release -p tests --bin reftests-minimal --no-default-features --features minimal + cargo llvm-cov --locked --no-report run --release -p tests --bin reftests cargo llvm-cov report --release --locked --codecov --output-path codecov.json \ - --ignore-filename-regex '(reftests/src|/registry/|\.cargo)' + --ignore-filename-regex '(tests/src|/registry/|\.cargo)' cargo llvm-cov report --release --locked --lcov --output-path lcov.info \ - --ignore-filename-regex '(reftests/src|/registry/|\.cargo)' + --ignore-filename-regex '(tests/src|/registry/|\.cargo)' - uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac with: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c161454..2dd9ac9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -42,3 +42,6 @@ jobs: - name: rustdoc run: cargo doc --workspace --no-deps --document-private-items --locked + + - name: rustdoc-node + run: cargo doc -p moonglass-node --no-deps --document-private-items --no-default-features --features minimal,node --locked diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7f0b145..d5b2f41 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -64,6 +64,9 @@ jobs: - name: clippy run: cargo clippy --workspace --all-targets --locked -- -D warnings + - name: clippy-node + run: cargo clippy -p moonglass-node --no-default-features --features minimal,node --all-targets --locked -- -D warnings + actionlint: name: actionlint-shellcheck runs-on: ubuntu-24.04 diff --git a/.github/workflows/mainnet.yml b/.github/workflows/mainnet.yml index 48c0bf2..fb1fb71 100644 --- a/.github/workflows/mainnet.yml +++ b/.github/workflows/mainnet.yml @@ -50,13 +50,13 @@ jobs: - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 with: - path: reftests/vectors - key: ${{ runner.os }}-reftest-vectors-mainnet-${{ hashFiles('reftests/src/lib.rs', 'reftests/src/vectors.rs', 'reftests/src/vectors/*.rs') }} + path: tests/vectors + key: ${{ runner.os }}-reftest-vectors-mainnet-${{ hashFiles('tests/src/lib.rs', 'tests/src/vectors.rs', 'tests/src/vectors/*.rs') }} restore-keys: | ${{ runner.os }}-reftest-vectors-mainnet- - name: build-reftests-runner - run: cargo build --release -p reftests --locked + run: cargo build --release -p tests --locked - name: run-reftests shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e64ce0..3ddc8ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,6 +40,9 @@ jobs: - name: unit-tests run: cargo test --workspace --locked + - name: node-tests + run: cargo test -p moonglass-node --no-default-features --features minimal,node --locked + reftests: name: consensus-reftests-minimal runs-on: ubuntu-24.04 @@ -55,16 +58,16 @@ jobs: - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 with: - path: reftests/vectors - key: ${{ runner.os }}-reftest-vectors-minimal-${{ hashFiles('reftests/src/lib.rs', 'reftests/src/vectors.rs', 'reftests/src/vectors/*.rs') }} + path: tests/vectors + key: ${{ runner.os }}-reftest-vectors-minimal-${{ hashFiles('tests/src/lib.rs', 'tests/src/vectors.rs', 'tests/src/vectors/*.rs') }} restore-keys: | ${{ runner.os }}-reftest-vectors-minimal- - name: test-reftests-minimal - run: cargo test -p reftests --no-default-features --features minimal --locked + run: cargo test -p tests --no-default-features --features minimal --locked - name: build-reftests-runner - run: cargo build --release -p reftests --no-default-features --features minimal --locked + run: cargo build --release -p tests --no-default-features --features minimal --locked - name: run-reftests shell: bash diff --git a/.gitignore b/.gitignore index a778906..ae65ae0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/tests/vectors/ /reftests/vectors/ # Coverage artifacts, regenerated by the Coverage workflow and cargo-llvm-cov. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33d9ffa..8fc8938 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,7 +47,7 @@ fix(state_transition): reject attestation with future target epoch docs(constants): document bit-40 builder index flag refactor(epoch): split process_epoch into phase-named helpers test(primitives): add round-trip for BuilderIndex encoding -chore: bump ssz_rs to 0.10 +chore: bump thiserror to 2 ci: split required checks by lane ``` @@ -81,8 +81,8 @@ Crates and tools outside the main library may document extra checks in their own README files. Run those when your change touches that code. Consensus changes also need fixture validation. If upstream consensus-spec -fixtures cover the behavior, add the matching `reftests` adapter and checks, -then run that lane. See [`reftests/README.md`](reftests/README.md). +fixtures cover the behavior, add the matching `tests` adapter and checks, +then run that lane. See [`tests/README.md`](tests/README.md). ## Review @@ -100,8 +100,6 @@ Useful areas for contributors: implemented Moonglass behavior. - Expand transition and fork-choice coverage as Moonglass exposes more public consensus APIs. -- Evaluate replacing the current `ssz_rs` dependency when the project is ready - to own that surface. - Explore Rust-to-Lean generation and formal verification. Discuss larger scope changes before implementation, especially networking, @@ -130,7 +128,8 @@ ownership, mutations, invariants, and implementation boundaries. Avoid comments that only restate obvious control flow. Error descriptions are centralized in the domain error modules under -`moonglass/src/error*.rs`. Do not add repetitive per-function `# Errors` +`moonglass-core/src/error.rs` and `moonglass-core/src/error/`. Do not add +repetitive per-function `# Errors` sections only to satisfy Clippy. Function docs should explain protocol flow and local invariants, and only mention a rejection inline when it is essential to understanding that function. diff --git a/Cargo.lock b/Cargo.lock index 63fd5d0..17345d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -20,12 +55,37 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alloy-rlp" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc90b1e703d3c03f4ff7f48e82dd0bc1c8211ab7d079cd836a06fcfeb06651cb" +dependencies = [ + "arrayvec", + "bytes", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + [[package]] name = "ark-bls12-381" version = "0.6.0" @@ -51,7 +111,7 @@ dependencies = [ "ark-std", "educe", "fnv", - "hashbrown", + "hashbrown 0.17.1", "itertools", "num-bigint", "num-integer", @@ -69,7 +129,7 @@ dependencies = [ "ark-ff-macros", "ark-serialize", "ark-std", - "digest 0.10.7", + "digest", "educe", "num-bigint", "num-traits", @@ -83,7 +143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" dependencies = [ "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -96,7 +156,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -111,7 +171,7 @@ dependencies = [ "ark-std", "educe", "fnv", - "hashbrown", + "hashbrown 0.17.1", ] [[package]] @@ -122,7 +182,7 @@ checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" dependencies = [ "ark-serialize-derive", "ark-std", - "digest 0.10.7", + "digest", "num-bigint", ] @@ -134,7 +194,7 @@ checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -144,869 +204,3518 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" dependencies = [ "num-traits", - "rand", + "rand 0.8.6", ] [[package]] -name = "autocfg" -version = "1.5.1" +name = "arrayref" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] -name = "base64" -version = "0.22.1" +name = "arrayvec" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] -name = "bitflags" -version = "2.12.1" +name = "asn1-rs" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] [[package]] -name = "bitvec" -version = "1.0.1" +name = "asn1-rs-derive" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ - "funty", - "radium", - "tap", - "wyz", + "proc-macro2", + "quote", + "syn", + "synstructure", ] [[package]] -name = "block-buffer" -version = "0.9.0" +name = "asn1-rs-impl" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ - "generic-array", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "block-buffer" -version = "0.10.4" +name = "asn1_der" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "4858a9d740c5007a9069007c3b4e91152d0506f13c1b31dd49051fd537656156" + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "generic-array", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", ] [[package]] -name = "cc" -version = "1.2.63" +name = "async-trait" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ - "find-msvc-tools", - "shlex", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "cfg-if" -version = "1.0.4" +name = "asynchronous-codec" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2" dependencies = [ - "libc", + "http 0.2.12", + "log", + "url", ] [[package]] -name = "crc32fast" -version = "1.5.0" +name = "autocfg" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ - "cfg-if", + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", + "hyper 1.10.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", ] [[package]] -name = "crypto-common" -version = "0.1.7" +name = "axum-core" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ - "generic-array", - "typenum", + "async-trait", + "bytes", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", ] [[package]] -name = "digest" -version = "0.9.0" +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base256emoji" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" dependencies = [ - "generic-array", + "const-str", + "match-lookup", ] [[package]] -name = "digest" -version = "0.10.7" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "block-buffer 0.10.4", - "crypto-common", + "digest", ] [[package]] -name = "displaydoc" -version = "0.2.6" +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "generic-array", ] [[package]] -name = "educe" -version = "0.6.0" +name = "bs58" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.117", + "tinyvec", ] [[package]] -name = "either" -version = "1.16.0" +name = "bumpalo" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] -name = "enum-ordinalize" -version = "4.3.2" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" -dependencies = [ - "enum-ordinalize-derive", -] +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] -name = "enum-ordinalize-derive" -version = "4.3.2" +name = "bytes" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "find-msvc-tools", + "shlex", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "cfg-if" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "errno" -version = "0.3.14" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "filetime" -version = "0.2.29" +name = "chacha20" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "libc", + "cipher", + "cpufeatures", ] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "chacha20poly1305" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] [[package]] -name = "flate2" -version = "1.1.9" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crc32fast", - "miniz_oxide", + "crypto-common", + "inout", + "zeroize", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] -name = "funty" -version = "2.0.0" +name = "const-str" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" [[package]] -name = "generic-array" -version = "0.14.7" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "typenum", - "version_check", + "core-foundation-sys", + "libc", ] [[package]] -name = "getrandom" +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "cfg-if", "libc", - "wasi", ] [[package]] -name = "hashbrown" -version = "0.17.1" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "allocator-api2", + "cfg-if", ] [[package]] -name = "hex" -version = "0.4.3" +name = "crossbeam-utils" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "icu_collections" -version = "2.2.0" +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", ] [[package]] -name = "icu_locale_core" -version = "2.2.0" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "data-encoding-macro" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3259c913752a86488b501ed8680446a5ed2d5aeac6e596cb23ba3800768ea32c" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" +dependencies = [ + "data-encoding", + "syn", +] + +[[package]] +name = "delay_map" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e365f083a5cb5972d50ce8b1b2c9f125dc5ec0f50c0248cfb568ae59efcf0b" +dependencies = [ + "futures", + "tokio", + "tokio-util", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", ] [[package]] -name = "icu_normalizer" -version = "2.2.0" +name = "deranged" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "discv5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4b4e7798d2ff74e29cee344dc490af947ae657d6ab5273dde35d58ce06a4d71" +dependencies = [ + "aes", + "aes-gcm", + "alloy-rlp", + "arrayvec", + "ctr", + "delay_map", + "enr", + "fnv", + "futures", + "hashlink", + "hex", + "hkdf", + "lazy_static", + "lru", + "more-asserts", + "parking_lot", + "rand 0.8.6", "smallvec", - "zerovec", + "socket2 0.5.10", + "tokio", + "tracing", + "uint", + "zeroize", ] [[package]] -name = "icu_normalizer_data" -version = "2.2.0" +name = "displaydoc" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "icu_properties" -version = "2.2.0" +name = "dtoa" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", ] [[package]] -name = "icu_properties_data" -version = "2.2.0" +name = "ed25519" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] [[package]] -name = "icu_provider" +name = "ed25519-dalek" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", ] [[package]] -name = "idna" -version = "1.1.0" +name = "educe" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", + "enum-ordinalize", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "idna_adapter" -version = "1.2.2" +name = "either" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "icu_normalizer", - "icu_properties", + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", ] [[package]] -name = "indexmap" -version = "2.14.0" +name = "enr" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "851bd664a3d3a3c175cff92b2f0df02df3c541b4895d0ae307611827aae46152" dependencies = [ - "equivalent", - "hashbrown", + "alloy-rlp", + "base64", + "bytes", + "ed25519-dalek", + "hex", + "k256", + "log", + "rand 0.8.6", + "serde", + "sha3", + "zeroize", ] [[package]] -name = "itertools" -version = "0.14.0" +name = "enum-as-inner" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "either", + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "enum-ordinalize" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] [[package]] -name = "libc" -version = "0.2.186" +name = "enum-ordinalize-derive" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "litemap" -version = "0.8.2" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] [[package]] -name = "log" -version = "0.4.32" +name = "ff" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] [[package]] -name = "memchr" -version = "2.8.1" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-bounded" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f328e7fb845fc832912fb6a34f40cf6d1888c92f974d1893a54e97b5ff542e" +dependencies = [ + "futures-timer", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls", + "rustls-pki-types", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-ticker" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9763058047f713632a52e916cc7f6a4b3fc6e9fc1ff8c5b1dc49e5a89041682e" +dependencies = [ + "futures", + "futures-timer", + "instant", +] + +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + +[[package]] +name = "hickory-proto" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.8.6", + "socket2 0.5.10", + "thiserror 1.0.69", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand 0.8.6", + "resolv-conf", + "smallvec", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.2", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.2", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.4.2", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.2", + "hyper 1.10.1", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.7", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "hyper 1.10.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.4", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "if-addrs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "if-watch" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c02a5161c313f0cbdbadc511611893584a10a7b6153cb554bdf83ddce99ec2" +dependencies = [ + "async-io", + "core-foundation", + "fnv", + "futures", + "if-addrs", + "ipnet", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "rtnetlink", + "system-configuration", + "tokio", + "windows", +] + +[[package]] +name = "igd-next" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064d90fec10d541084e7b39ead8875a5a80d9114a2b18791565253bae25f49e4" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rand 0.8.6", + "tokio", + "url", + "xmltree", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2 0.6.4", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libp2p" +version = "0.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbe80f9c7e00526cd6b838075b9c171919404a4732cb2fa8ece0a093223bfc4" +dependencies = [ + "bytes", + "either", + "futures", + "futures-timer", + "getrandom 0.2.17", + "libp2p-allow-block-list", + "libp2p-connection-limits", + "libp2p-core", + "libp2p-dns", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-identity", + "libp2p-mdns", + "libp2p-metrics", + "libp2p-noise", + "libp2p-ping", + "libp2p-quic", + "libp2p-request-response", + "libp2p-swarm", + "libp2p-tcp", + "libp2p-upnp", + "libp2p-yamux", + "multiaddr", + "pin-project", + "rw-stream-sink", + "thiserror 1.0.69", +] + +[[package]] +name = "libp2p-allow-block-list" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1027ccf8d70320ed77e984f273bc8ce952f623762cb9bf2d126df73caef8041" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "void", +] + +[[package]] +name = "libp2p-connection-limits" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d003540ee8baef0d254f7b6bfd79bac3ddf774662ca0abf69186d517ef82ad8" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "void", +] + +[[package]] +name = "libp2p-core" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a61f26c83ed111104cd820fe9bc3aaabbac5f1652a1d213ed6e900b7918a1298" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "libp2p-identity", + "multiaddr", + "multihash", + "multistream-select", + "once_cell", + "parking_lot", + "pin-project", + "quick-protobuf", + "rand 0.8.6", + "rw-stream-sink", + "smallvec", + "thiserror 1.0.69", + "tracing", + "unsigned-varint 0.8.0", + "void", + "web-time", +] + +[[package]] +name = "libp2p-dns" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97f37f30d5c7275db282ecd86e54f29dd2176bd3ac656f06abf43bedb21eb8bd" +dependencies = [ + "async-trait", + "futures", + "hickory-resolver", + "libp2p-core", + "libp2p-identity", + "parking_lot", + "smallvec", + "tracing", +] + +[[package]] +name = "libp2p-gossipsub" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4e830fdf24ac8c444c12415903174d506e1e077fbe3875c404a78c5935a8543" +dependencies = [ + "asynchronous-codec", + "base64", + "byteorder", + "bytes", + "either", + "fnv", + "futures", + "futures-ticker", + "getrandom 0.2.17", + "hex_fmt", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "prometheus-client", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.6", + "regex", + "sha2", + "smallvec", + "tracing", + "void", + "web-time", +] + +[[package]] +name = "libp2p-identify" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1711b004a273be4f30202778856368683bd9a83c4c7dcc8f848847606831a4e3" +dependencies = [ + "asynchronous-codec", + "either", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "lru", + "quick-protobuf", + "quick-protobuf-codec", + "smallvec", + "thiserror 1.0.69", + "tracing", + "void", +] + +[[package]] +name = "libp2p-identity" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9525f3831544f7ae497bde79adf114ef127b0fbbb97edbbf692a80408636421c" +dependencies = [ + "asn1_der", + "bs58", + "ed25519-dalek", + "hkdf", + "k256", + "multihash", + "prost", + "rand 0.8.6", + "sha2", + "thiserror 2.0.18", + "tracing", + "zeroize", +] + +[[package]] +name = "libp2p-mdns" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b8546b6644032565eb29046b42744aee1e9f261ed99671b2c93fb140dba417" +dependencies = [ + "data-encoding", + "futures", + "hickory-proto", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.6", + "smallvec", + "socket2 0.5.10", + "tokio", + "tracing", + "void", +] + +[[package]] +name = "libp2p-metrics" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ebafa94a717c8442d8db8d3ae5d1c6a15e30f2d347e0cd31d057ca72e42566" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-identity", + "libp2p-ping", + "libp2p-swarm", + "pin-project", + "prometheus-client", + "web-time", +] + +[[package]] +name = "libp2p-noise" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36b137cb1ae86ee39f8e5d6245a296518912014eaa87427d24e6ff58cfc1b28c" +dependencies = [ + "asynchronous-codec", + "bytes", + "curve25519-dalek", + "futures", + "libp2p-core", + "libp2p-identity", + "multiaddr", + "multihash", + "once_cell", + "quick-protobuf", + "rand 0.8.6", + "sha2", + "snow", + "static_assertions", + "thiserror 1.0.69", + "tracing", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "libp2p-ping" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005a34420359223b974ee344457095f027e51346e992d1e0dcd35173f4cdd422" +dependencies = [ + "either", + "futures", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.6", + "tracing", + "void", + "web-time", +] + +[[package]] +name = "libp2p-quic" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46352ac5cd040c70e88e7ff8257a2ae2f891a4076abad2c439584a31c15fd24e" +dependencies = [ + "bytes", + "futures", + "futures-timer", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-tls", + "parking_lot", + "quinn", + "rand 0.8.6", + "ring 0.17.14", + "rustls", + "socket2 0.5.10", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-request-response" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1356c9e376a94a75ae830c42cdaea3d4fe1290ba409a22c809033d1b7dcab0a6" +dependencies = [ + "async-trait", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.6", + "smallvec", + "tracing", + "void", + "web-time", +] + +[[package]] +name = "libp2p-swarm" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dd6741793d2c1fb2088f67f82cf07261f25272ebe3c0b0c311e0c6b50e851a" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm-derive", + "lru", + "multistream-select", + "once_cell", + "rand 0.8.6", + "smallvec", + "tokio", + "tracing", + "void", + "web-time", +] + +[[package]] +name = "libp2p-swarm-derive" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206e0aa0ebe004d778d79fb0966aa0de996c19894e2c0605ba2f8524dd4443d8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libp2p-tcp" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad964f312c59dcfcac840acd8c555de8403e295d39edf96f5240048b5fcaa314" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libc", + "libp2p-core", + "libp2p-identity", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b23dddc2b9c355f73c1e36eb0c3ae86f7dc964a3715f0731cfad352db4d847" +dependencies = [ + "futures", + "futures-rustls", + "libp2p-core", + "libp2p-identity", + "rcgen", + "ring 0.17.14", + "rustls", + "rustls-webpki 0.101.7", + "thiserror 1.0.69", + "x509-parser", + "yasna", +] + +[[package]] +name = "libp2p-upnp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01bf2d1b772bd3abca049214a3304615e6a36fa6ffc742bdd1ba774486200b8f" +dependencies = [ + "futures", + "futures-timer", + "igd-next", + "libp2p-core", + "libp2p-swarm", + "tokio", + "tracing", + "void", +] + +[[package]] +name = "libp2p-yamux" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788b61c80789dba9760d8c669a5bedb642c8267555c803fabd8396e4ca5c5882" +dependencies = [ + "either", + "futures", + "libp2p-core", + "thiserror 1.0.69", + "tracing", + "yamux 0.12.1", + "yamux 0.13.10", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "match-lookup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moonglass-core" +version = "0.1.0" +dependencies = [ + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-serialize", + "sha2", + "thiserror 2.0.18", +] + +[[package]] +name = "moonglass-node" +version = "0.1.0" +dependencies = [ + "async-trait", + "axum", + "discv5", + "futures", + "libp2p", + "moonglass-core", + "reqwest", + "serde_json", + "serde_yaml", + "sha2", + "snap", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + +[[package]] +name = "multiaddr" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "libp2p-identity", + "multibase", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint 0.8.0", + "url", +] + +[[package]] +name = "multibase" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +dependencies = [ + "base-x", + "base256emoji", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c63b00ad74d57e8c9aa870b5fccebf2fd64a308a5aee9f1bb88e4aea19447" +dependencies = [ + "unsigned-varint 0.8.0", +] + +[[package]] +name = "multistream-select" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project", + "smallvec", + "unsigned-varint 0.7.2", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus-client" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quick-protobuf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +dependencies = [ + "byteorder", +] + +[[package]] +name = "quick-protobuf-codec" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" +dependencies = [ + "asynchronous-codec", + "bytes", + "quick-protobuf", + "thiserror 1.0.69", + "unsigned-varint 0.8.0", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "futures-io", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.4", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring 0.17.14", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.4", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rcgen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c4f3084aa3bc7dfbba4eff4fab2a54db4324965d8872ab933565e6fbd83bc6" +dependencies = [ + "pem", + "ring 0.16.20", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", + "hyper 1.10.1", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.7", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rtnetlink" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b960d5d873a75b5be9761b1e73b146f52dddcd27bac75263f40fba686d4d7b5" +dependencies = [ + "futures-channel", + "futures-util", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "nix", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rw-stream-sink" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" +dependencies = [ + "futures", + "pin-project", + "static_assertions", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" [[package]] -name = "miniz_oxide" -version = "0.8.9" +name = "snow" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" dependencies = [ - "adler2", - "simd-adler32", + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "rand_core 0.6.4", + "ring 0.17.14", + "rustc_version", + "sha2", + "subtle", ] [[package]] -name = "moonglass" -version = "0.1.0" +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ - "ark-bls12-381", - "ark-ec", - "ark-ff", - "ark-poly", - "ark-serialize", - "sha2 0.10.9", - "ssz_rs", - "thiserror", + "libc", + "windows-sys 0.52.0", ] [[package]] -name = "num-bigint" -version = "0.4.6" +name = "socket2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ - "num-integer", - "num-traits", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "num-integer" -version = "0.1.46" +name = "spin" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] -name = "num-traits" -version = "0.2.19" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ - "autocfg", + "base64ct", + "der", ] [[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "opaque-debug" -version = "0.3.1" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "potential_utf" -version = "0.1.5" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "syn" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ - "zerocopy", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "unicode-ident", + "futures-core", ] [[package]] -name = "quote" -version = "1.0.45" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", + "quote", + "syn", ] [[package]] -name = "radium" +name = "system-configuration" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "rand_chacha", - "rand_core", + "bitflags", + "core-foundation", + "system-configuration-sys", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "system-configuration-sys" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ - "ppv-lite86", - "rand_core", + "core-foundation-sys", + "libc", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "tar" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] [[package]] -name = "reftests" +name = "tests" version = "0.1.0" dependencies = [ "flate2", - "moonglass", + "moonglass-core", "serde", "serde_json", "serde_yaml", - "sha2 0.10.9", + "sha2", "snap", - "ssz_rs", "tar", - "thiserror", + "thiserror 2.0.18", "ureq", ] [[package]] -name = "ring" -version = "0.17.14" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "untrusted", - "windows-sys 0.52.0", + "thiserror-impl 1.0.69", ] [[package]] -name = "rustix" -version = "1.1.4" +name = "thiserror" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", + "thiserror-impl 2.0.18", ] [[package]] -name = "rustls" -version = "0.23.40" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "rustls-pki-types" -version = "1.14.1" +name = "thiserror-impl" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ - "zeroize", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "rustls-webpki" -version = "0.103.13" +name = "thread_local" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", + "cfg-if", ] [[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "serde" -version = "1.0.228" +name = "time" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" dependencies = [ + "deranged", + "num-conv", + "powerfmt", "serde_core", - "serde_derive", + "time-core", + "time-macros", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "time-core" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] -name = "serde_derive" -version = "1.0.228" +name = "time-macros" +version = "0.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "num-conv", + "time-core", ] [[package]] -name = "serde_json" -version = "1.0.150" +name = "tinystr" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "displaydoc", + "zerovec", ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "tinyvec" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", + "tinyvec_macros", ] [[package]] -name = "sha2" -version = "0.9.9" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "sha2" -version = "0.10.9" +name = "tokio" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.4", + "tokio-macros", + "windows-sys 0.61.2", ] [[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "smallvec" -version = "1.15.1" +name = "tokio-macros" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "snap" -version = "1.1.1" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] [[package]] -name = "ssz_rs" -version = "0.9.0" +name = "tokio-util" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057291e5631f280978fa9c8009390663ca4613359fc1318e36a8c24c392f6d1f" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ - "bitvec", - "hex", - "num-bigint", - "serde", - "sha2 0.9.9", - "ssz_rs_derive", + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "slab", + "tokio", ] [[package]] -name = "ssz_rs_derive" -version = "0.9.0" +name = "tower" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07d54c4d01a1713eb363b55ba51595da15f6f1211435b71466460da022aa140" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", ] [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "tower-http" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] [[package]] -name = "subtle" -version = "2.6.1" +name = "tower-layer" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] -name = "syn" -version = "1.0.109" +name = "tower-service" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" -[[package]] -name = "syn" -version = "2.0.117" +[[package]] +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "synstructure" -version = "0.13.2" +name = "tracing-attributes" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tar" -version = "0.4.46" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ - "filetime", - "libc", - "xattr", + "once_cell", + "valuable", ] [[package]] -name = "thiserror" -version = "2.0.18" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "thiserror-impl", + "log", + "once_cell", + "tracing-core", ] [[package]] -name = "thiserror-impl" -version = "2.0.18" +name = "tracing-subscriber" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] -name = "tinystr" -version = "0.8.3" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" @@ -1014,18 +3723,58 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + +[[package]] +name = "unsigned-varint" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -1068,18 +3817,123 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -1098,12 +3952,146 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1138,6 +4126,15 @@ dependencies = [ "windows_x86_64_msvc", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1186,6 +4183,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.3" @@ -1193,12 +4196,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] -name = "wyz" -version = "0.5.1" +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" dependencies = [ - "tap", + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", ] [[package]] @@ -1211,6 +4234,61 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "yamux" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed0164ae619f2dc144909a9f082187ebb5893693d8c0196e8085283ccd4b776" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot", + "pin-project", + "rand 0.8.6", + "static_assertions", +] + +[[package]] +name = "yamux" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1991f6690292030e31b0144d73f5e8368936c58e45e7068254f7138b23b00672" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot", + "pin-project", + "rand 0.9.4", + "static_assertions", + "web-time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.3" @@ -1230,7 +4308,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -1251,7 +4329,7 @@ checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1271,7 +4349,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -1280,6 +4358,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" @@ -1311,7 +4403,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c59794b..61e29e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,18 @@ [workspace] -members = ["moonglass", "reftests"] +members = [ + "moonglass-core", + "moonglass-node", + "tests", +] resolver = "2" +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.96" +license = "AGPL-3.0-only" +repository = "https://github.com/brech1/moonglass" + [workspace.lints.rust] dead_code = "deny" missing_docs = "deny" diff --git a/README.md b/README.md index f6fb7d1..dc7b223 100644 --- a/README.md +++ b/README.md @@ -14,33 +14,52 @@ > [!WARNING] > Moonglass is experimental. -Moonglass is a Rust implementation of selected Ethereum consensus paths. It is -built for traceable reading: each path should make clear which object enters, -which rule owns it, what state changes, and where local validation stops. +Moonglass is a Rust implementation of the Ethereum consensus specifications, +built for traceable reading: each consensus path makes clear which object +enters, which rule owns it, what state changes, and where local validation +stops. + +The implementation is the engine crate [`moonglass-core`](moonglass-core/): +state transition, fork choice, the in-house SSZ layer (`moonglass_core::ssz`), +crypto (BLS, KZG), typed primitives, constants, and errors. It is pure +consensus, with no I/O. The compile-time preset (`mainnet` / `minimal`) is +selected here. This README is about that engine. + +## Workspace + +Two accessory crates build on the engine and document themselves: + +| Crate | Role | +| --- | --- | +| [`moonglass-node`](moonglass-node/) | Devnet `config.yaml` / genesis loading plus a read-only follower that tracks a live chain's head through the engine. See [`moonglass-node/README.md`](moonglass-node/README.md). | +| [`tests`](tests/) | Consensus-spec reference-test harness, run over `mainnet` and `minimal` lanes. See [`tests/README.md`](tests/README.md). | ## Scope -Moonglass focuses on: +`moonglass-core` implements: + +- State transition and fork choice for the currently live fork. +- Typed consensus objects, constants, and errors that stay close to spec shape. +- The SSZ encoding and the crypto (BLS, KZG) the specs depend on. -- State-transition and fork-choice behavior. -- Typed consensus objects, constants, and errors that stay close to the spec - shape. -- Rustdoc as the primary explanation layer. +It is an engine, not a node. Validator duties, execution-engine driving, +request/response serving, sync, and persistence live outside `moonglass-core`; +[`moonglass-node`](moonglass-node/) adds the network-facing pieces on top. -Moonglass does not try to be a full beacon node. Networking, sync, validator -duties, execution-engine driving, persistence, and production operations are -outside the current library boundary. +## Build and test -## Validation +```bash +cargo build -p moonglass-core # the engine +cargo build --workspace # the whole workspace +cargo clippy -p moonglass-core --all-targets +``` -Consensus validation runs through the accessory `reftests` crate, which checks -Moonglass against generated [consensus-specs] fixtures. If upstream fixtures -cover new behavior, add the matching reftest adapter and checks, then run the -relevant lane. Passing a lane only proves its wired fixture inventory. -Unsupported fixture families are not coverage. See -[`reftests/README.md`](reftests/README.md) for runner details. +Correctness is checked against the consensus-spec reference vectors; the runner, +its lanes, and their setup live in [`tests/README.md`](tests/README.md). The +follower and its feature flags live in +[`moonglass-node/README.md`](moonglass-node/README.md). -## Reading Model +## Reading model Start with the state boundary, not the directory tree. `BeaconState` is durable consensus state, advanced by state transition. `Store` is one node's local @@ -61,33 +80,37 @@ Useful entry points: - Blocks: `BeaconState::apply_signed_block`, then `Store::on_block`. - Payload commitments: `process_execution_payload_bid`, `Store::on_execution_payload_envelope`, and - `accept_parent_payload_commitment`. -- Votes and head choice: `process_attestation`, `Store::on_attestation`, - and `Store::get_head`. + `process_parent_execution_payload`. +- Votes and head choice: `process_attestation`, `Store::on_attestation`, and + `Store::get_head`. -## Repository Layout +## Repository layout | Path | Role | | --- | --- | -| `moonglass/src/containers/` | SSZ containers carried by consensus paths. | -| `moonglass/src/primitives/` | Typed roots, slots, epochs, indices, and checked arithmetic. | -| `moonglass/src/constants/` | Consumed protocol constants for the active preset. | -| `moonglass/src/state_transition/` | Slot, epoch, block, operation, and builder processing. | -| `moonglass/src/fork_choice/` | Local store updates, filtering, weights, and head selection. | -| `moonglass/src/crypto/` | Hashing, BLS, and KZG wrappers. | -| `moonglass/src/error/` | Centralized rejection reasons. | -| `reftests/` | Accessory consensus-spec fixture runner. | - -## Documentation Standard - -Rustdoc is the project documentation surface. Published docs are available at +| `moonglass-core/src/containers/` | SSZ containers carried by consensus paths. | +| `moonglass-core/src/primitives/` | Typed roots, slots, epochs, indices, and checked arithmetic. | +| `moonglass-core/src/constants/` | Consumed protocol constants for the active preset. | +| `moonglass-core/src/ssz/` | In-house SSZ encoding, decoding, and hash-tree-root. | +| `moonglass-core/src/state_transition/` | Slot, epoch, block, operation, and builder processing. | +| `moonglass-core/src/fork_choice/` | Local store updates, filtering, weights, and head selection. | +| `moonglass-core/src/networking.rs` | Fork digests, gossip topics, and request/response protocol ids. | +| `moonglass-core/src/gossip.rs` | Pure gossip and validator-duty predicates. | +| `moonglass-core/src/glossary.rs` | Reading vocabulary, the recommended starting point. | +| `moonglass-core/src/crypto/` | Hashing, BLS, and KZG wrappers. | +| `moonglass-core/src/error/` | Centralized rejection reasons. | + +## Documentation standard + +Rustdoc is the project documentation surface. Published docs are at [brech1.github.io/moonglass](https://brech1.github.io/moonglass/). -Library docs should explain protocol ownership, mutations, invariants, and +Library docs should explain protocol ownership, mutations, invariants, and the boundaries where the code makes those decisions. -The workspace denies missing docs, unused code, dead code, unreachable public -items, unsafe code, broken intra-doc links, and Clippy `all` and `pedantic`. +The workspace denies missing docs, missing docs in private items, unused code, +dead code, unreachable public items, unsafe code, unescaped rustdoc backticks, +broken intra-doc links, missing cargo metadata, and Clippy `all` and `pedantic`. The `missing_errors_doc` lint is allowed because error descriptions are centralized in the error modules. @@ -98,7 +121,7 @@ policy. Contribution policy, local checks, and current contribution areas live in [CONTRIBUTING.md](CONTRIBUTING.md). Consensus changes should include the -matching `reftests` adapter and checks when upstream fixtures exist. +matching `tests` adapter and checks when upstream fixtures exist. ## License diff --git a/codecov.yml b/codecov.yml index 19eef42..18e5dce 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,6 +10,6 @@ coverage: comment: false ignore: - - "moonglass/src/crypto/kzg.rs" - - "moonglass/src/crypto/kzg/**" - - "reftests/**" + - "moonglass-core/src/crypto/kzg.rs" + - "moonglass-core/src/crypto/kzg/**" + - "tests/**" diff --git a/moonglass/Cargo.toml b/moonglass-core/Cargo.toml similarity index 90% rename from moonglass/Cargo.toml rename to moonglass-core/Cargo.toml index cf134fc..e34b2b1 100644 --- a/moonglass/Cargo.toml +++ b/moonglass-core/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "moonglass" +name = "moonglass-core" version = "0.1.0" edition = "2024" rust-version = "1.96" description = "Readable Rust implementation of selected Ethereum consensus paths." license = "AGPL-3.0-only" repository = "https://github.com/brech1/moonglass" -documentation = "https://brech1.github.io/moonglass/moonglass/" +documentation = "https://brech1.github.io/moonglass/moonglass_core/" readme = "../README.md" keywords = ["ethereum", "consensus"] categories = ["cryptography::cryptocurrencies"] @@ -18,7 +18,6 @@ minimal = [] [dependencies] sha2 = "0.10" -ssz_rs = "0.9" thiserror = "2" # Arkworks diff --git a/moonglass/assets/trusted_setup.txt b/moonglass-core/assets/trusted_setup.txt similarity index 100% rename from moonglass/assets/trusted_setup.txt rename to moonglass-core/assets/trusted_setup.txt diff --git a/moonglass/src/constants.rs b/moonglass-core/src/constants.rs similarity index 60% rename from moonglass/src/constants.rs rename to moonglass-core/src/constants.rs index 8450848..81788fe 100644 --- a/moonglass/src/constants.rs +++ b/moonglass-core/src/constants.rs @@ -2,18 +2,17 @@ //! //! These parameters define timing, rewards, limits, domains, chain identity, //! fork-choice tuning, and SSZ/container bounds used by the implemented -//! transition and fork-choice paths. Networking and validator-duty constants are -//! omitted unless implemented paths consume them today. +//! transition, fork-choice, and networking paths. -mod block; -mod builder; -mod chain; -mod domains; -mod fork_choice; -mod rewards; -mod state; -mod time; -mod validator; +pub mod block; +pub mod builder; +pub mod chain; +pub mod domains; +pub mod fork_choice; +pub mod rewards; +pub mod state; +pub mod time; +pub mod validator; pub use block::*; pub use builder::*; diff --git a/moonglass/src/constants/block.rs b/moonglass-core/src/constants/block.rs similarity index 70% rename from moonglass/src/constants/block.rs rename to moonglass-core/src/constants/block.rs index 143b3eb..6f6223b 100644 --- a/moonglass/src/constants/block.rs +++ b/moonglass-core/src/constants/block.rs @@ -34,6 +34,9 @@ pub const SHUFFLE_ROUND_COUNT: u64 = 90; #[cfg(feature = "mainnet")] pub const SYNC_COMMITTEE_SIZE: usize = 512; +/// Sync committee subnets used for message aggregation. +pub const SYNC_COMMITTEE_SUBNET_COUNT: usize = 4; + /// Validators in the payload-timeliness committee. #[cfg(feature = "mainnet")] pub const PTC_SIZE: usize = 512; @@ -89,6 +92,57 @@ pub const MAX_EXTRA_DATA_BYTES: usize = 32; /// Maximum KZG blob commitments per block. pub const MAX_BLOB_COMMITMENTS_PER_BLOCK: usize = 4_096; +/// Serialized scalar-field element length. +pub const BYTES_PER_FIELD_ELEMENT: usize = 32; + +/// Number of field elements in one blob. +pub const FIELD_ELEMENTS_PER_BLOB: usize = 4_096; + +/// Serialized blob length. +pub const BYTES_PER_BLOB: usize = FIELD_ELEMENTS_PER_BLOB * BYTES_PER_FIELD_ELEMENT; + +/// Number of field elements in the extended blob. +pub const FIELD_ELEMENTS_PER_EXT_BLOB: usize = 2 * FIELD_ELEMENTS_PER_BLOB; + +/// Number of field elements carried by one cell. +pub const FIELD_ELEMENTS_PER_CELL: usize = 64; + +/// Serialized cell length. +pub const BYTES_PER_CELL: usize = FIELD_ELEMENTS_PER_CELL * BYTES_PER_FIELD_ELEMENT; + +/// Number of cells in the extended blob. +pub const CELLS_PER_EXT_BLOB: usize = FIELD_ELEMENTS_PER_EXT_BLOB / FIELD_ELEMENTS_PER_CELL; + +/// Number of data columns. +pub const NUMBER_OF_COLUMNS: usize = CELLS_PER_EXT_BLOB; + +/// Minimum number of samples for an honest node. +pub const SAMPLES_PER_SLOT: u64 = 8; + +/// Number of custody groups available for sampling. +pub const NUMBER_OF_CUSTODY_GROUPS: usize = 128; + +/// Minimum custody groups served by an honest node. +pub const CUSTODY_REQUIREMENT: u64 = 4; + +/// Number of data-column sidecar subnets. +pub const DATA_COLUMN_SIDECAR_SUBNET_COUNT: u64 = 128; + +/// Minimum epoch range over which nodes serve data-column sidecars. +pub const MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: u64 = 4_096; + +/// Maximum blocks addressable in a sidecar request. +pub const MAX_REQUEST_BLOCKS_DENEB: u64 = 128; + +/// Maximum execution payload envelopes addressable in one request. +pub const MAX_REQUEST_PAYLOADS: usize = 128; + +/// Domain prefix used for KZG cell-proof batch challenges. +pub const RANDOM_CHALLENGE_KZG_CELL_BATCH_DOMAIN: &[u8; 16] = b"RCKZGCBATCH__V1_"; + +/// Primitive root used by the scalar-field roots-of-unity helper. +pub const PRIMITIVE_ROOT_OF_UNITY: u64 = 7; + /// Blob limit applied before the first [`BLOB_SCHEDULE`] entry activates. pub const MAX_BLOBS_PER_BLOCK: u64 = 9; diff --git a/moonglass/src/constants/builder.rs b/moonglass-core/src/constants/builder.rs similarity index 100% rename from moonglass/src/constants/builder.rs rename to moonglass-core/src/constants/builder.rs diff --git a/moonglass-core/src/constants/chain.rs b/moonglass-core/src/constants/chain.rs new file mode 100644 index 0000000..08f1fa6 --- /dev/null +++ b/moonglass-core/src/constants/chain.rs @@ -0,0 +1,204 @@ +//! Chain identity: genesis trigger parameters, fork versions, and the +//! deposit-contract pointer. +//! +//! Fork versions are mixed into signing domains before a validator signs. This +//! module keeps the Ethereum consensus-spec names for constants consumed by the +//! implemented paths, without exporting every unused historical config entry. + +use crate::primitives::{Epoch, ExecutionAddress, Slot, Version}; + +/// Minimum active validator count required to trigger genesis. +#[cfg(feature = "mainnet")] +pub const MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: u64 = 16_384; + +/// Earliest Unix timestamp at which genesis may occur. +#[cfg(feature = "mainnet")] +pub const MIN_GENESIS_TIME: u64 = 1_606_824_000; + +/// Delay, in seconds, between the genesis trigger and the genesis slot. +#[cfg(feature = "mainnet")] +pub const GENESIS_DELAY: u64 = 604_800; + +/// First epoch number. +pub const GENESIS_EPOCH: Epoch = Epoch(0); + +/// First slot number. +pub const GENESIS_SLOT: Slot = Slot(0); + +/// Fork version stamped on the genesis state. +#[cfg(feature = "mainnet")] +pub const GENESIS_FORK_VERSION: Version = Version([0x00, 0x00, 0x00, 0x00]); + +/// Fork version for the first scheduled upgrade. +#[cfg(feature = "mainnet")] +pub const ALTAIR_FORK_VERSION: Version = Version([0x01, 0x00, 0x00, 0x00]); + +/// Activation epoch for the first scheduled upgrade. +#[cfg(feature = "mainnet")] +pub const ALTAIR_FORK_EPOCH: Epoch = Epoch(74_240); + +/// Fork version for the execution-payload upgrade. +#[cfg(feature = "mainnet")] +pub const BELLATRIX_FORK_VERSION: Version = Version([0x02, 0x00, 0x00, 0x00]); + +/// Activation epoch for the execution-payload upgrade. +#[cfg(feature = "mainnet")] +pub const BELLATRIX_FORK_EPOCH: Epoch = Epoch(144_896); + +/// Fork version used by voluntary-exit signatures. +/// +/// The consensus rules intentionally pin this domain to the same version used +/// when BLS-to-execution credential changes were introduced, so old voluntary +/// exits remain verifiable after later network upgrades. +#[cfg(feature = "mainnet")] +pub const CAPELLA_FORK_VERSION: Version = Version([0x03, 0x00, 0x00, 0x00]); + +/// Activation epoch for the withdrawal-credential upgrade. +#[cfg(feature = "mainnet")] +pub const CAPELLA_FORK_EPOCH: Epoch = Epoch(194_048); + +/// Fork version for blob-carrying blocks. +#[cfg(feature = "mainnet")] +pub const DENEB_FORK_VERSION: Version = Version([0x04, 0x00, 0x00, 0x00]); + +/// Activation epoch for blob-carrying blocks. +#[cfg(feature = "mainnet")] +pub const DENEB_FORK_EPOCH: Epoch = Epoch(269_568); + +/// Fork version for request-carrying payloads. +#[cfg(feature = "mainnet")] +pub const ELECTRA_FORK_VERSION: Version = Version([0x05, 0x00, 0x00, 0x00]); + +/// Activation epoch for request-carrying payloads. +#[cfg(feature = "mainnet")] +pub const ELECTRA_FORK_EPOCH: Epoch = Epoch(364_032); + +/// Fork version for data-column sampling. +#[cfg(feature = "mainnet")] +pub const FULU_FORK_VERSION: Version = Version([0x06, 0x00, 0x00, 0x00]); + +/// Activation epoch for data-column sampling. +#[cfg(feature = "mainnet")] +pub const FULU_FORK_EPOCH: Epoch = Epoch(411_392); + +/// Fork version for external payload commitments. +#[cfg(feature = "mainnet")] +pub const GLOAS_FORK_VERSION: Version = Version([0x07, 0x00, 0x00, 0x00]); + +/// Activation epoch for external payload commitments. +#[cfg(feature = "mainnet")] +pub const GLOAS_FORK_EPOCH: Epoch = Epoch(u64::MAX); + +/// Depth of the deposit-contract Merkle tree. +pub const DEPOSIT_CONTRACT_TREE_DEPTH: usize = 32; + +/// Length of a deposit proof: Merkle path plus root chunk. +pub const DEPOSIT_PROOF_LEN: usize = DEPOSIT_CONTRACT_TREE_DEPTH + 1; + +/// Sentinel for `deposit_requests_start_index` meaning no start index has been +/// assigned yet, because no execution-layer deposit request has been processed. +pub const UNSET_DEPOSIT_REQUESTS_START_INDEX: u64 = u64::MAX; + +/// Chain ID of the network the deposit contract lives on. +#[cfg(feature = "mainnet")] +pub const DEPOSIT_CHAIN_ID: u64 = 1; + +/// Network ID of the network the deposit contract lives on. +#[cfg(feature = "mainnet")] +pub const DEPOSIT_NETWORK_ID: u64 = 1; + +/// Execution-layer address of the canonical deposit contract. +#[cfg(feature = "mainnet")] +pub const DEPOSIT_CONTRACT_ADDRESS: ExecutionAddress = ExecutionAddress([ + 0x00, 0x00, 0x00, 0x00, 0x21, 0x9a, 0xb5, 0x40, 0x35, 0x6c, 0xbb, 0x83, 0x9c, 0xbe, 0x05, 0x30, + 0x3d, 0x77, 0x05, 0xfa, +]); + +// Minimal-preset values from the consensus-spec minimal configuration. + +/// Minimal-preset active validator count required to trigger genesis. +#[cfg(feature = "minimal")] +pub const MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: u64 = 64; + +/// Minimal-preset earliest Unix timestamp at which genesis may occur. +#[cfg(feature = "minimal")] +pub const MIN_GENESIS_TIME: u64 = 1_578_009_600; + +/// Minimal-preset delay, in seconds, between genesis trigger and genesis slot. +#[cfg(feature = "minimal")] +pub const GENESIS_DELAY: u64 = 300; + +/// Minimal-preset fork version stamped on the genesis state. +#[cfg(feature = "minimal")] +pub const GENESIS_FORK_VERSION: Version = Version([0x00, 0x00, 0x00, 0x01]); + +/// Minimal-preset fork version for the first scheduled upgrade. +#[cfg(feature = "minimal")] +pub const ALTAIR_FORK_VERSION: Version = Version([0x01, 0x00, 0x00, 0x01]); + +/// Minimal-preset activation epoch for the first scheduled upgrade. +#[cfg(feature = "minimal")] +pub const ALTAIR_FORK_EPOCH: Epoch = Epoch(u64::MAX); + +/// Minimal-preset fork version for the execution-payload upgrade. +#[cfg(feature = "minimal")] +pub const BELLATRIX_FORK_VERSION: Version = Version([0x02, 0x00, 0x00, 0x01]); + +/// Minimal-preset activation epoch for the execution-payload upgrade. +#[cfg(feature = "minimal")] +pub const BELLATRIX_FORK_EPOCH: Epoch = Epoch(u64::MAX); + +/// Minimal-preset fork version used by voluntary-exit signatures. +#[cfg(feature = "minimal")] +pub const CAPELLA_FORK_VERSION: Version = Version([0x03, 0x00, 0x00, 0x01]); + +/// Minimal-preset activation epoch for the withdrawal-credential upgrade. +#[cfg(feature = "minimal")] +pub const CAPELLA_FORK_EPOCH: Epoch = Epoch(u64::MAX); + +/// Minimal-preset fork version for blob-carrying blocks. +#[cfg(feature = "minimal")] +pub const DENEB_FORK_VERSION: Version = Version([0x04, 0x00, 0x00, 0x01]); + +/// Minimal-preset activation epoch for blob-carrying blocks. +#[cfg(feature = "minimal")] +pub const DENEB_FORK_EPOCH: Epoch = Epoch(u64::MAX); + +/// Minimal-preset fork version for request-carrying payloads. +#[cfg(feature = "minimal")] +pub const ELECTRA_FORK_VERSION: Version = Version([0x05, 0x00, 0x00, 0x01]); + +/// Minimal-preset activation epoch for request-carrying payloads. +#[cfg(feature = "minimal")] +pub const ELECTRA_FORK_EPOCH: Epoch = Epoch(u64::MAX); + +/// Minimal-preset fork version for data-column sampling. +#[cfg(feature = "minimal")] +pub const FULU_FORK_VERSION: Version = Version([0x06, 0x00, 0x00, 0x01]); + +/// Minimal-preset activation epoch for data-column sampling. +#[cfg(feature = "minimal")] +pub const FULU_FORK_EPOCH: Epoch = Epoch(u64::MAX); + +/// Minimal-preset fork version for external payload commitments. +#[cfg(feature = "minimal")] +pub const GLOAS_FORK_VERSION: Version = Version([0x07, 0x00, 0x00, 0x01]); + +/// Minimal-preset activation epoch for external payload commitments. +#[cfg(feature = "minimal")] +pub const GLOAS_FORK_EPOCH: Epoch = Epoch(u64::MAX); + +/// Minimal-preset deposit-contract chain ID. +#[cfg(feature = "minimal")] +pub const DEPOSIT_CHAIN_ID: u64 = 5; + +/// Minimal-preset deposit-contract network ID. +#[cfg(feature = "minimal")] +pub const DEPOSIT_NETWORK_ID: u64 = 5; + +/// Minimal-preset execution-layer address of the deposit contract. +#[cfg(feature = "minimal")] +pub const DEPOSIT_CONTRACT_ADDRESS: ExecutionAddress = ExecutionAddress([ + 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, + 0x34, 0x56, 0x78, 0x90, +]); diff --git a/moonglass/src/constants/domains.rs b/moonglass-core/src/constants/domains.rs similarity index 93% rename from moonglass/src/constants/domains.rs rename to moonglass-core/src/constants/domains.rs index 89c419f..3f66b61 100644 --- a/moonglass/src/constants/domains.rs +++ b/moonglass-core/src/constants/domains.rs @@ -40,6 +40,9 @@ pub const DOMAIN_BEACON_BUILDER: DomainType = DomainType([0x0B, 0x00, 0x00, 0x00 /// Domain for payload-timeliness committee attestations. pub const DOMAIN_PTC_ATTESTER: DomainType = DomainType([0x0C, 0x00, 0x00, 0x00]); +/// Domain for proposer preference signatures. +pub const DOMAIN_PROPOSER_PREFERENCES: DomainType = DomainType([0x0D, 0x00, 0x00, 0x00]); + /// Domain for builder-deposit signatures. /// /// A dedicated domain keeps validator deposit signatures and builder deposit diff --git a/moonglass/src/constants/fork_choice.rs b/moonglass-core/src/constants/fork_choice.rs similarity index 83% rename from moonglass/src/constants/fork_choice.rs rename to moonglass-core/src/constants/fork_choice.rs index f7a7846..e2ec971 100644 --- a/moonglass/src/constants/fork_choice.rs +++ b/moonglass-core/src/constants/fork_choice.rs @@ -21,6 +21,15 @@ pub const PROPOSER_REORG_CUTOFF_BPS: u64 = 1_667; /// Attestation deadline in basis points of `SLOT_DURATION_MS`. pub const ATTESTATION_DUE_BPS_GLOAS: u64 = 2_500; +/// Aggregate deadline in basis points of `SLOT_DURATION_MS`. +pub const AGGREGATE_DUE_BPS_GLOAS: u64 = 5_000; + +/// Sync-message deadline in basis points of `SLOT_DURATION_MS`. +pub const SYNC_MESSAGE_DUE_BPS_GLOAS: u64 = 2_500; + +/// Contribution deadline in basis points of `SLOT_DURATION_MS`. +pub const CONTRIBUTION_DUE_BPS_GLOAS: u64 = 5_000; + /// Payload attestation deadline in basis points of `SLOT_DURATION_MS`. pub const PAYLOAD_ATTESTATION_DUE_BPS: u64 = 7_500; diff --git a/moonglass/src/constants/rewards.rs b/moonglass-core/src/constants/rewards.rs similarity index 100% rename from moonglass/src/constants/rewards.rs rename to moonglass-core/src/constants/rewards.rs diff --git a/moonglass/src/constants/state.rs b/moonglass-core/src/constants/state.rs similarity index 100% rename from moonglass/src/constants/state.rs rename to moonglass-core/src/constants/state.rs diff --git a/moonglass/src/constants/time.rs b/moonglass-core/src/constants/time.rs similarity index 96% rename from moonglass/src/constants/time.rs rename to moonglass-core/src/constants/time.rs index 83a23f4..80f2d6e 100644 --- a/moonglass/src/constants/time.rs +++ b/moonglass-core/src/constants/time.rs @@ -2,15 +2,15 @@ //! //! Preset-specific entries control the clock geometry used by slot processing, //! epoch processing, historical-root rings, and deposit/sync lookahead. Callers -//! should read these constants from the active preset instead of assuming -//! mainnet's 32-slot epoch. +//! should read these constants from the active preset instead of assuming one +//! epoch shape. use crate::primitives::Epoch; /// Wall-clock duration of a slot, in milliseconds. /// /// Consumed by execution-payload timestamp validation in -/// `BeaconState::expected_execution_payload_timestamp`, so its value is +/// `BeaconState::compute_time_at_slot`, so its value is /// load-bearing for block acceptance, not purely cosmetic. #[cfg(feature = "mainnet")] pub const SLOT_DURATION_MS: u64 = 12_000; diff --git a/moonglass/src/constants/validator.rs b/moonglass-core/src/constants/validator.rs similarity index 100% rename from moonglass/src/constants/validator.rs rename to moonglass-core/src/constants/validator.rs diff --git a/moonglass/src/containers.rs b/moonglass-core/src/containers.rs similarity index 57% rename from moonglass/src/containers.rs rename to moonglass-core/src/containers.rs index 704c7d3..db9c8c7 100644 --- a/moonglass/src/containers.rs +++ b/moonglass-core/src/containers.rs @@ -6,27 +6,29 @@ //! to that snapshot. Supporting containers carry votes, signatures, deposits, //! withdrawals, execution payload commitments, and builder-market data. //! -//! **Scope.** Only containers the state transition consumes are modeled. -//! Out of scope: -//! - networking (`DataColumnSidecar`, `BlobSidecar`, p2p envelopes), -//! - sync-update helpers, -//! - validator duties and local keystore/signing flows. +//! **Scope.** Containers are added as implemented paths consume, verify, or +//! deserialize them. Validator duty orchestration and local signing flows remain +//! outside the modeled surface for now. -mod attestation; -mod block; -mod builder; -mod chain; -mod execution; -mod state; -mod sync; -mod validator; -mod withdrawal; +pub mod attestation; +pub mod block; +pub mod builder; +pub mod chain; +pub mod data_availability; +pub mod execution; +pub mod gossip; +pub mod state; +pub mod sync; +pub mod validator; +pub mod withdrawal; pub use attestation::*; pub use block::*; pub use builder::*; pub use chain::*; +pub use data_availability::*; pub use execution::*; +pub use gossip::*; pub use state::*; pub use sync::*; pub use validator::*; diff --git a/moonglass-core/src/containers/attestation.rs b/moonglass-core/src/containers/attestation.rs new file mode 100644 index 0000000..5a09530 --- /dev/null +++ b/moonglass-core/src/containers/attestation.rs @@ -0,0 +1,663 @@ +//! Validator vote containers and slashing evidence. +//! +//! An attestation is a validator vote for the current head block and for the +//! source/target checkpoints used by Casper finality. Conflicting votes can +//! become slashing evidence. + +use crate::constants::{MAX_ATTESTING_INDICES, MAX_COMMITTEES_PER_SLOT}; +use crate::containers::{Checkpoint, SignedBeaconBlockHeader}; +use crate::primitives::{BLSSignature, CommitteeIndex, Root, Slot, ValidatorIndex}; +use crate::ssz::prelude::*; + +/// The vote payload an attester signs. +/// +/// It names the slot, spec index field, head block, and finality checkpoints. +/// Aggregate attestations use `committee_bits` to identify participating +/// committees. The meaning of `index` depends on the attestation form being +/// evaluated. In payload-branch checks, a vote for a block whose slot equals +/// `data.slot` must use `index == 0` and remains pending for fork-choice +/// scoring. A vote naming an older head for `data.slot` uses `index == 0` for +/// the empty/no-payload branch and `index == 1` for the full/payload branch. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct AttestationData { + /// Slot the attestation is for. + pub slot: Slot, + /// Spec index field. + /// + /// For aggregate attestations this is not the sole committee selector, so use + /// `committee_bits` to identify participating committees. For payload + /// branch voting, a vote for a block at `data.slot` must use `0`. A vote for + /// an older head uses `0` for the empty branch and `1` for the full branch. + pub index: CommitteeIndex, + /// Head block root being voted for by chain-head selection. + pub beacon_block_root: Root, + /// Source checkpoint used by finality. + pub source: Checkpoint, + /// Target checkpoint used by finality. + pub target: Checkpoint, +} + +/// Attestation expanded into sorted attester indices for signature verification. +/// +/// `attesting_indices` is sorted and deduplicated, the form [`BeaconState::validate_indexed_attestation`](crate::containers::BeaconState::validate_indexed_attestation) +/// requires before checking the aggregate `signature` over `data`. Unlike +/// [`crate::containers::IndexedPayloadAttestation`], duplicate indices are invalid here because +/// each validator votes at most once. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct IndexedAttestation { + /// Sorted, deduplicated indices of validators that attested. + pub attesting_indices: List, + /// The vote shared by all listed attesters. + pub data: AttestationData, + /// Aggregate BLS signature of the listed attesters. + pub signature: BLSSignature, +} + +/// Wire-form attestation: per-committee participation bitfield + aggregate signature. +/// +/// The state transition consumes block-body attestations through +/// [`BeaconState::process_attestation`](crate::containers::BeaconState::process_attestation). Fork choice consumes both block and +/// gossip attestations through [`crate::fork_choice::Store::on_attestation()`] to update +/// latest messages for LMD-GHOST. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct Attestation { + /// Per-attester participation, packed across all committees of the slot. + pub aggregation_bits: Bitlist, + /// The vote shared by all participants. + pub data: AttestationData, + /// Aggregate signature over `data` from the participating attesters. + pub signature: BLSSignature, + /// Bitfield selecting which committees within the slot participated. + pub committee_bits: Bitvector, +} + +/// Single-attester attestation form used on gossip before aggregation. +/// +/// It names one `attester_index` and its `committee_index` directly rather than packing +/// participation into bitfields, so an individual vote can travel the network before it is +/// folded into an aggregate [`Attestation`]. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct SingleAttestation { + /// Committee the attester belongs to. + pub committee_index: CommitteeIndex, + /// Validator that produced the attestation. + pub attester_index: ValidatorIndex, + /// The vote. + pub data: AttestationData, + /// Attester's signature over `data`. + pub signature: BLSSignature, +} + +/// Attestation aggregate plus the aggregator's selection proof. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct AggregateAndProof { + /// Validator that aggregated the attestation. + pub aggregator_index: ValidatorIndex, + /// Aggregated attestation being gossiped. + pub aggregate: Attestation, + /// Signature proving the validator was selected to aggregate. + pub selection_proof: BLSSignature, +} + +/// Signed attestation aggregate gossip object. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct SignedAggregateAndProof { + /// Aggregate and selection proof being signed. + pub message: AggregateAndProof, + /// Aggregator signature over `message`. + pub signature: BLSSignature, +} + +/// Evidence of two contradictory attestations by overlapping validator sets. +/// +/// Block processing validates and applies this through +/// [`BeaconState::process_attester_slashing`](crate::containers::BeaconState::process_attester_slashing). Fork choice records the +/// equivocating validators through [`crate::fork_choice::Store::on_attester_slashing()`]. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct AttesterSlashing { + /// First conflicting attestation. + pub attestation_1: IndexedAttestation, + /// Second conflicting attestation. + /// + /// Must share at least one attester with `attestation_1`. + pub attestation_2: IndexedAttestation, +} + +/// Evidence that a proposer signed two distinct block headers for the same slot. +/// +/// Block processing validates and applies this through +/// [`BeaconState::process_proposer_slashing`](crate::containers::BeaconState::process_proposer_slashing). +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProposerSlashing { + /// First signed header by the proposer. + pub signed_header_1: SignedBeaconBlockHeader, + /// Conflicting signed header by the same proposer for the same slot. + pub signed_header_2: SignedBeaconBlockHeader, +} + +impl SszSized for AttestationData { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for AttestationData { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.slot)?; + encoder.write_field(&self.index)?; + encoder.write_field(&self.beacon_block_root)?; + encoder.write_field(&self.source)?; + encoder.write_field(&self.target)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for AttestationData { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + slot: decoder.deserialize_next::()?, + index: decoder.deserialize_next::()?, + beacon_block_root: decoder.deserialize_next::()?, + source: decoder.deserialize_next::()?, + target: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for AttestationData { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.slot)?, + Merkleized::hash_tree_root(&self.index)?, + Merkleized::hash_tree_root(&self.beacon_block_root)?, + Merkleized::hash_tree_root(&self.source)?, + Merkleized::hash_tree_root(&self.target)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for AttestationData { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for IndexedAttestation { + fn is_variable_size() -> bool { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for IndexedAttestation { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.attesting_indices)?; + encoder.write_field(&self.data)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for IndexedAttestation { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + attesting_indices: decoder + .deserialize_next::>()?, + data: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for IndexedAttestation { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.attesting_indices)?, + Merkleized::hash_tree_root(&self.data)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for IndexedAttestation { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for Attestation { + fn is_variable_size() -> bool { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::>(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::>(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for Attestation { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.aggregation_bits)?; + encoder.write_field(&self.data)?; + encoder.write_field(&self.signature)?; + encoder.write_field(&self.committee_bits)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for Attestation { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::>(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + aggregation_bits: decoder.deserialize_next::>()?, + data: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + committee_bits: decoder.deserialize_next::>()?, + }) + } +} + +impl Merkleized for Attestation { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.aggregation_bits)?, + Merkleized::hash_tree_root(&self.data)?, + Merkleized::hash_tree_root(&self.signature)?, + Merkleized::hash_tree_root(&self.committee_bits)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for Attestation { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SingleAttestation { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SingleAttestation { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.committee_index)?; + encoder.write_field(&self.attester_index)?; + encoder.write_field(&self.data)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SingleAttestation { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + committee_index: decoder.deserialize_next::()?, + attester_index: decoder.deserialize_next::()?, + data: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SingleAttestation { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.committee_index)?, + Merkleized::hash_tree_root(&self.attester_index)?, + Merkleized::hash_tree_root(&self.data)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SingleAttestation { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for AggregateAndProof { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for AggregateAndProof { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.aggregator_index)?; + encoder.write_field(&self.aggregate)?; + encoder.write_field(&self.selection_proof)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for AggregateAndProof { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + aggregator_index: decoder.deserialize_next::()?, + aggregate: decoder.deserialize_next::()?, + selection_proof: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for AggregateAndProof { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.aggregator_index)?, + Merkleized::hash_tree_root(&self.aggregate)?, + Merkleized::hash_tree_root(&self.selection_proof)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for AggregateAndProof { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SignedAggregateAndProof { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SignedAggregateAndProof { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.message)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SignedAggregateAndProof { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + message: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SignedAggregateAndProof { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.message)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SignedAggregateAndProof { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for AttesterSlashing { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for AttesterSlashing { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.attestation_1)?; + encoder.write_field(&self.attestation_2)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for AttesterSlashing { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + attestation_1: decoder.deserialize_next::()?, + attestation_2: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for AttesterSlashing { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.attestation_1)?, + Merkleized::hash_tree_root(&self.attestation_2)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for AttesterSlashing { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for ProposerSlashing { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for ProposerSlashing { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.signed_header_1)?; + encoder.write_field(&self.signed_header_2)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for ProposerSlashing { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + signed_header_1: decoder.deserialize_next::()?, + signed_header_2: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for ProposerSlashing { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.signed_header_1)?, + Merkleized::hash_tree_root(&self.signed_header_2)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for ProposerSlashing { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/containers/block.rs b/moonglass-core/src/containers/block.rs new file mode 100644 index 0000000..505d082 --- /dev/null +++ b/moonglass-core/src/containers/block.rs @@ -0,0 +1,555 @@ +//! Block-shaped containers: header, body, block, and their signed envelopes. + +use crate::ssz::prelude::*; + +use crate::constants::{ + MAX_ATTESTATIONS, MAX_ATTESTER_SLASHINGS, MAX_BLS_TO_EXECUTION_CHANGES, MAX_DEPOSITS, + MAX_PAYLOAD_ATTESTATIONS, MAX_PROPOSER_SLASHINGS, MAX_VOLUNTARY_EXITS, +}; +use crate::containers::{ + Attestation, AttesterSlashing, Deposit, Eth1Data, ExecutionRequests, PayloadAttestation, + ProposerSlashing, SignedBLSToExecutionChange, SignedExecutionPayloadBid, SignedVoluntaryExit, + SyncAggregate, +}; +use crate::primitives::{BLSSignature, Bytes32, Root, Slot, ValidatorIndex}; + +/// Compact block summary stored in state and signed by proposers. +/// +/// It carries the roots needed to identify a block without storing the full +/// body, and it is reused as proposer-slashing evidence. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct BeaconBlockHeader { + /// Slot the block is proposed for. + pub slot: Slot, + /// Validator that proposed the block. + pub proposer_index: ValidatorIndex, + /// Root of the parent block. + pub parent_root: Root, + /// Root of the post-state after applying the block. + pub state_root: Root, + /// Root of [`BeaconBlockBody`]. + pub body_root: Root, +} + +impl BeaconBlockHeader { + /// Return this header with `state_root` set. + pub fn with_state_root(mut self, state_root: Root) -> Self { + self.state_root = state_root; + self + } +} + +/// Header plus the proposer's signature. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct SignedBeaconBlockHeader { + /// The header being signed. + pub message: BeaconBlockHeader, + /// Proposer's signature over the domain-separated signing root of `message`. + pub signature: BLSSignature, +} + +/// All operations the proposer chose to include in this block. +/// +/// Parent-payload requests and withdrawals are processed around this body. The +/// body itself carries randomness, votes, slashings, lifecycle operations, +/// payload-timeliness votes, and sync-committee participation. +/// Consumed by [`BeaconState::process_block`](crate::containers::BeaconState::process_block): parent payload commitment is +/// handled before the current-slot bid, then operations are handled by +/// [`BeaconState::process_operations`](crate::containers::BeaconState::process_operations). +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct BeaconBlockBody { + /// Proposer's RANDAO reveal, mixed into committee-shuffling randomness. + pub randao_reveal: BLSSignature, + /// Proposer's deposit-chain vote used to track new deposits. + pub eth1_data: Eth1Data, + /// Proposer-supplied freeform 32-byte tag, ignored by consensus. + pub graffiti: Bytes32, + /// Evidence of duplicate block proposals. + pub proposer_slashings: List, + /// Evidence of double-vote or surround-vote misbehavior. + pub attester_slashings: List, + /// Validator votes for the head block and finality checkpoints. + pub attestations: List, + /// Legacy block-body deposits from the spec shape. + /// + /// Non-empty lists are rejected here. Active deposit application arrives + /// through parent-payload [`ExecutionRequests`]. + pub deposits: List, + /// Validator-signed requests to leave the active set. + pub voluntary_exits: List, + /// Aggregate sync-committee signature over the previous-slot block root. + pub sync_aggregate: SyncAggregate, + /// Requests to swap BLS withdrawal credentials for execution addresses. + pub bls_to_execution_changes: List, + /// Builder bid the proposer committed to for this slot. + /// + /// Accepted by [`BeaconState::process_execution_payload_bid`](crate::containers::BeaconState::process_execution_payload_bid), then matched + /// by [`BeaconState::verify_execution_payload_envelope`](crate::containers::BeaconState::verify_execution_payload_envelope) when the matching + /// payload envelope is delivered for this block. + pub signed_execution_payload_bid: SignedExecutionPayloadBid, + /// Payload-timeliness committee votes for the parent slot's payload. + /// + /// The state transition validates these with + /// [`BeaconState::process_payload_attestation`](crate::containers::BeaconState::process_payload_attestation). Fork choice replays the + /// aggregate through [`crate::fork_choice::Store::on_block()`], expanding participants + /// before writing local PTC vote evidence for the attested parent root. + pub payload_attestations: List, + /// Execution-to-consensus requests from the parent slot's payload. + /// + /// The block proves these requests by matching the accepted parent bid's + /// `execution_requests_root` before applying them in + /// [`BeaconState::process_parent_execution_payload`](crate::containers::BeaconState::process_parent_execution_payload). + pub parent_execution_requests: ExecutionRequests, +} + +/// Proposed beacon block with its slot identity, claimed post-state root, and +/// operations. +/// In state transition this is applied by +/// [`crate::containers::BeaconState::apply_signed_block`]. In fork choice it is +/// accepted by [`crate::fork_choice::Store::on_block()`] and stored in +/// [`crate::fork_choice::Store::blocks`]. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct BeaconBlock { + /// Slot the block is for. + pub slot: Slot, + /// Validator that proposed the block. + pub proposer_index: ValidatorIndex, + /// Root of the parent block. + pub parent_root: Root, + /// Root of the post-state produced by applying this block. + pub state_root: Root, + /// Block operations. + pub body: BeaconBlockBody, +} + +impl BeaconBlock { + /// Header corresponding to this block and the supplied body/state roots. + pub fn header(&self, body_root: Root, state_root: Root) -> BeaconBlockHeader { + BeaconBlockHeader { + slot: self.slot, + proposer_index: self.proposer_index, + parent_root: self.parent_root, + state_root, + body_root, + } + } +} + +/// Beacon block plus the proposer's signature. +/// +/// This is the entry object for the block transition: +/// [`crate::containers::BeaconState::apply_signed_block`] advances slots, +/// checks the proposer signature, processes the block, and verifies the claimed +/// post-state root. Fork choice passes the same object to +/// [`crate::fork_choice::Store::on_block()`] before caching the resulting post-state. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct SignedBeaconBlock { + /// The block being signed. + pub message: BeaconBlock, + /// Proposer's signature over the domain-separated signing root of `message`. + pub signature: BLSSignature, +} + +impl SszSized for BeaconBlockHeader { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for BeaconBlockHeader { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.slot)?; + encoder.write_field(&self.proposer_index)?; + encoder.write_field(&self.parent_root)?; + encoder.write_field(&self.state_root)?; + encoder.write_field(&self.body_root)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for BeaconBlockHeader { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + slot: decoder.deserialize_next::()?, + proposer_index: decoder.deserialize_next::()?, + parent_root: decoder.deserialize_next::()?, + state_root: decoder.deserialize_next::()?, + body_root: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for BeaconBlockHeader { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.slot)?, + Merkleized::hash_tree_root(&self.proposer_index)?, + Merkleized::hash_tree_root(&self.parent_root)?, + Merkleized::hash_tree_root(&self.state_root)?, + Merkleized::hash_tree_root(&self.body_root)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for BeaconBlockHeader { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SignedBeaconBlockHeader { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SignedBeaconBlockHeader { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.message)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SignedBeaconBlockHeader { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + message: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SignedBeaconBlockHeader { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.message)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SignedBeaconBlockHeader { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for BeaconBlockBody { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for BeaconBlockBody { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.randao_reveal)?; + encoder.write_field(&self.eth1_data)?; + encoder.write_field(&self.graffiti)?; + encoder.write_field(&self.proposer_slashings)?; + encoder.write_field(&self.attester_slashings)?; + encoder.write_field(&self.attestations)?; + encoder.write_field(&self.deposits)?; + encoder.write_field(&self.voluntary_exits)?; + encoder.write_field(&self.sync_aggregate)?; + encoder.write_field(&self.bls_to_execution_changes)?; + encoder.write_field(&self.signed_execution_payload_bid)?; + encoder.write_field(&self.payload_attestations)?; + encoder.write_field(&self.parent_execution_requests)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for BeaconBlockBody { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + randao_reveal: decoder.deserialize_next::()?, + eth1_data: decoder.deserialize_next::()?, + graffiti: decoder.deserialize_next::()?, + proposer_slashings: decoder + .deserialize_next::>()?, + attester_slashings: decoder + .deserialize_next::>()?, + attestations: decoder.deserialize_next::>()?, + deposits: decoder.deserialize_next::>()?, + voluntary_exits: decoder + .deserialize_next::>()?, + sync_aggregate: decoder.deserialize_next::()?, + bls_to_execution_changes: decoder + .deserialize_next::>( + )?, + signed_execution_payload_bid: decoder + .deserialize_next::()?, + payload_attestations: decoder + .deserialize_next::>()?, + parent_execution_requests: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for BeaconBlockBody { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.randao_reveal)?, + Merkleized::hash_tree_root(&self.eth1_data)?, + Merkleized::hash_tree_root(&self.graffiti)?, + Merkleized::hash_tree_root(&self.proposer_slashings)?, + Merkleized::hash_tree_root(&self.attester_slashings)?, + Merkleized::hash_tree_root(&self.attestations)?, + Merkleized::hash_tree_root(&self.deposits)?, + Merkleized::hash_tree_root(&self.voluntary_exits)?, + Merkleized::hash_tree_root(&self.sync_aggregate)?, + Merkleized::hash_tree_root(&self.bls_to_execution_changes)?, + Merkleized::hash_tree_root(&self.signed_execution_payload_bid)?, + Merkleized::hash_tree_root(&self.payload_attestations)?, + Merkleized::hash_tree_root(&self.parent_execution_requests)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for BeaconBlockBody { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for BeaconBlock { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for BeaconBlock { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.slot)?; + encoder.write_field(&self.proposer_index)?; + encoder.write_field(&self.parent_root)?; + encoder.write_field(&self.state_root)?; + encoder.write_field(&self.body)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for BeaconBlock { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + slot: decoder.deserialize_next::()?, + proposer_index: decoder.deserialize_next::()?, + parent_root: decoder.deserialize_next::()?, + state_root: decoder.deserialize_next::()?, + body: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for BeaconBlock { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.slot)?, + Merkleized::hash_tree_root(&self.proposer_index)?, + Merkleized::hash_tree_root(&self.parent_root)?, + Merkleized::hash_tree_root(&self.state_root)?, + Merkleized::hash_tree_root(&self.body)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for BeaconBlock { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SignedBeaconBlock { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SignedBeaconBlock { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.message)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SignedBeaconBlock { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + message: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SignedBeaconBlock { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.message)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SignedBeaconBlock { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/containers/builder.rs b/moonglass-core/src/containers/builder.rs new file mode 100644 index 0000000..f452a0e --- /dev/null +++ b/moonglass-core/src/containers/builder.rs @@ -0,0 +1,623 @@ +//! Builder-market containers. +//! +//! Builders supply execution payloads for proposed beacon blocks. They bid for +//! slots, beacon attestations add the state-transition quorum weight used to +//! release builder payments, and payload-timeliness committee votes feed fork +//! choice's local payload-status evidence. This module models the consensus +//! objects for that builder-supplied payload path. + +use crate::ssz::prelude::*; + +use crate::constants::PTC_SIZE; +use crate::primitives::{ + BLSPubkey, BLSSignature, BuilderIndex, Epoch, ExecutionAddress, Gwei, Root, Slot, + ValidatorIndex, +}; + +/// A single builder entry in the builder registry, indexed by [`BuilderIndex`]. +/// +/// The `pubkey` verifies a builder's bid signatures, and `balance` is the stake that backs +/// accepted bids and funds the payments owed by them. A builder stays active until +/// `withdrawable_epoch`, which holds `FAR_FUTURE_EPOCH` while the builder is live. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct Builder { + /// Builder's BLS public key used to verify bid signatures. + pub pubkey: BLSPubkey, + /// Builder registry record version. + pub version: u8, + /// Execution-layer address that receives builder withdrawals. + pub execution_address: ExecutionAddress, + /// Builder's stake balance backing accepted bids. + pub balance: Gwei, + /// Epoch at which the builder was added to the registry. + pub deposit_epoch: Epoch, + /// Epoch the balance becomes withdrawable, or `FAR_FUTURE_EPOCH` while active. + pub withdrawable_epoch: Epoch, +} + +/// Outbound payment a builder owes for an accepted bid, queued for the withdrawal sweep. +/// +/// Accepting a bid does not transfer funds immediately. It opens this obligation against the +/// builder's balance, payable to `fee_recipient`, which the withdrawal sweep settles once the +/// payment becomes due. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct BuilderPendingWithdrawal { + /// Execution-layer address the payment is destined for. + pub fee_recipient: ExecutionAddress, + /// Amount the builder owes. + pub amount: Gwei, + /// Builder making the payment. + pub builder_index: BuilderIndex, +} + +/// Builder payment accumulator entry for one accepted bid. +/// +/// A bid opens the `withdrawal` obligation with zero `weight`. Beacon +/// attestations for that proposal slot add effective balance only when they set +/// a new participation flag for the attester. The entry can be queued by +/// parent-payload handoff, queued at epoch aging if `weight` clears the quorum, +/// dropped when it ages out below quorum, or cleared by proposer slashing while +/// still in the two-epoch payment window. PTC votes are separate fork-choice +/// evidence about payload timeliness and blob data availability. They do not add +/// this payment weight. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct BuilderPendingPayment { + /// Sum of committee members' effective balance weighting toward payment quorum. + pub weight: Gwei, + /// The payment obligation queued by parent-payload handoff or quorum release. + pub withdrawal: BuilderPendingWithdrawal, + /// Proposer that opened this payment, so proposer slashing only clears the + /// payment when it slashes that same proposer. + pub proposer_index: ValidatorIndex, +} + +/// The vote a payload-timeliness committee member signs for a slot. +/// +/// It records whether the payload for `beacon_block_root` at `slot` was seen +/// (`payload_present`) and whether its blob data arrived alongside it (`blob_data_available`). +/// The same data appears inside block aggregates ([`PayloadAttestation`]) and gossip messages +/// ([`PayloadAttestationMessage`]), so a committee member's verdict travels unchanged whether +/// it is broadcast individually or folded into an aggregate. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct PayloadAttestationData { + /// Beacon block root the payload is associated with. + pub beacon_block_root: Root, + /// Slot the attestation is for. + pub slot: Slot, + /// True if the payload was seen on time, before its due time in the slot. + pub payload_present: bool, + /// True if blob data was observed alongside the payload. + pub blob_data_available: bool, +} + +/// Aggregated payload-timeliness vote carried in the block body: per-position bitfield plus +/// aggregate signature. +/// `aggregation_bits` is indexed by committee position, not by validator index, +/// so a set bit means the validator occupying that position attested. +/// [`BeaconState::process_payload_attestation`](crate::containers::BeaconState::process_payload_attestation) +/// validates this aggregate form. The current fork-choice replay path expands +/// those bits to validator indices and then reuses +/// [`crate::fork_choice::Store::on_payload_attestation_message()`], whose local store +/// write expands each validator back to every PTC position it occupies. Contrast +/// [`PayloadAttestationMessage`], which names a single validator index directly. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct PayloadAttestation { + /// Bit per committee position, set to 1 if that position's signature is included. + pub aggregation_bits: Bitvector, + /// The shared vote. + pub data: PayloadAttestationData, + /// Aggregate signature over `data`. + pub signature: BLSSignature, +} + +/// Single-member payload-timeliness vote used on gossip before aggregation. +/// +/// Handled by [`crate::fork_choice::Store::on_payload_attestation_message()`]. This form +/// names a validator index, and fork choice expands it to every PTC position the +/// validator occupies before updating payload vote vectors. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct PayloadAttestationMessage { + /// Committee member that produced the vote. + pub validator_index: ValidatorIndex, + /// The vote. + pub data: PayloadAttestationData, + /// Member's signature over `data`. + pub signature: BLSSignature, +} + +/// Payload-timeliness vote expanded into sorted participant indices. +/// +/// Built by [`BeaconState::get_indexed_payload_attestation`](crate::containers::BeaconState::get_indexed_payload_attestation) and validated by +/// [`BeaconState::validate_indexed_payload_attestation`](crate::containers::BeaconState::validate_indexed_payload_attestation). Unlike ordinary +/// indexed beacon attestations, duplicate validator indices are valid here +/// because PTC membership is position-based and a validator may occupy multiple +/// positions. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct IndexedPayloadAttestation { + /// Sorted indices of committee members that attested. Duplicates are valid + /// because a validator may occupy multiple PTC positions. + pub attesting_indices: List, + /// The shared vote. + pub data: PayloadAttestationData, + /// Aggregate signature. + pub signature: BLSSignature, +} + +impl SszSized for Builder { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for Builder { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.pubkey)?; + encoder.write_field(&self.version)?; + encoder.write_field(&self.execution_address)?; + encoder.write_field(&self.balance)?; + encoder.write_field(&self.deposit_epoch)?; + encoder.write_field(&self.withdrawable_epoch)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for Builder { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + pubkey: decoder.deserialize_next::()?, + version: decoder.deserialize_next::()?, + execution_address: decoder.deserialize_next::()?, + balance: decoder.deserialize_next::()?, + deposit_epoch: decoder.deserialize_next::()?, + withdrawable_epoch: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for Builder { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.pubkey)?, + Merkleized::hash_tree_root(&self.version)?, + Merkleized::hash_tree_root(&self.execution_address)?, + Merkleized::hash_tree_root(&self.balance)?, + Merkleized::hash_tree_root(&self.deposit_epoch)?, + Merkleized::hash_tree_root(&self.withdrawable_epoch)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for Builder { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for BuilderPendingWithdrawal { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for BuilderPendingWithdrawal { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.fee_recipient)?; + encoder.write_field(&self.amount)?; + encoder.write_field(&self.builder_index)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for BuilderPendingWithdrawal { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + fee_recipient: decoder.deserialize_next::()?, + amount: decoder.deserialize_next::()?, + builder_index: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for BuilderPendingWithdrawal { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.fee_recipient)?, + Merkleized::hash_tree_root(&self.amount)?, + Merkleized::hash_tree_root(&self.builder_index)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for BuilderPendingWithdrawal { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for BuilderPendingPayment { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for BuilderPendingPayment { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.weight)?; + encoder.write_field(&self.withdrawal)?; + encoder.write_field(&self.proposer_index)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for BuilderPendingPayment { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + weight: decoder.deserialize_next::()?, + withdrawal: decoder.deserialize_next::()?, + proposer_index: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for BuilderPendingPayment { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.weight)?, + Merkleized::hash_tree_root(&self.withdrawal)?, + Merkleized::hash_tree_root(&self.proposer_index)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for BuilderPendingPayment { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for PayloadAttestationData { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for PayloadAttestationData { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.beacon_block_root)?; + encoder.write_field(&self.slot)?; + encoder.write_field(&self.payload_present)?; + encoder.write_field(&self.blob_data_available)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for PayloadAttestationData { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + beacon_block_root: decoder.deserialize_next::()?, + slot: decoder.deserialize_next::()?, + payload_present: decoder.deserialize_next::()?, + blob_data_available: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for PayloadAttestationData { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.beacon_block_root)?, + Merkleized::hash_tree_root(&self.slot)?, + Merkleized::hash_tree_root(&self.payload_present)?, + Merkleized::hash_tree_root(&self.blob_data_available)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for PayloadAttestationData { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for PayloadAttestation { + fn is_variable_size() -> bool { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for PayloadAttestation { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.aggregation_bits)?; + encoder.write_field(&self.data)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for PayloadAttestation { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + aggregation_bits: decoder.deserialize_next::>()?, + data: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for PayloadAttestation { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.aggregation_bits)?, + Merkleized::hash_tree_root(&self.data)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for PayloadAttestation { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for PayloadAttestationMessage { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for PayloadAttestationMessage { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.validator_index)?; + encoder.write_field(&self.data)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for PayloadAttestationMessage { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + validator_index: decoder.deserialize_next::()?, + data: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for PayloadAttestationMessage { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.validator_index)?, + Merkleized::hash_tree_root(&self.data)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for PayloadAttestationMessage { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for IndexedPayloadAttestation { + fn is_variable_size() -> bool { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for IndexedPayloadAttestation { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.attesting_indices)?; + encoder.write_field(&self.data)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for IndexedPayloadAttestation { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + attesting_indices: decoder.deserialize_next::>()?, + data: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for IndexedPayloadAttestation { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.attesting_indices)?, + Merkleized::hash_tree_root(&self.data)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for IndexedPayloadAttestation { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/containers/chain.rs b/moonglass-core/src/containers/chain.rs new file mode 100644 index 0000000..2fd4d12 --- /dev/null +++ b/moonglass-core/src/containers/chain.rs @@ -0,0 +1,476 @@ +//! Chain metadata containers. +//! +//! Covers signing-version records, Casper finality checkpoints, historical +//! summaries, deposit-vote data, and signing-domain data. `Eth1Data` keeps the +//! historical spec name for the execution-layer deposit-chain vote. + +use crate::constants::{BLOB_SCHEDULE, ELECTRA_FORK_EPOCH, MAX_BLOBS_PER_BLOCK}; +use crate::primitives::{Domain, Epoch, Hash32, Root, Version}; +use crate::ssz::prelude::*; + +/// Active blob-parameter tuple used for request contexts and bid limits. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct BlobParameters { + /// Epoch at which this tuple became active. + pub epoch: Epoch, + /// Maximum blobs allowed per block under this tuple. + pub max_blobs_per_block: u64, +} + +impl SszSized for BlobParameters { + fn is_variable_size() -> bool { + let fields = [field_layout::(), field_layout::()]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [field_layout::(), field_layout::()]; + container_size_hint(&fields) + } +} + +impl Serialize for BlobParameters { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.epoch)?; + encoder.write_field(&self.max_blobs_per_block)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for BlobParameters { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [field_layout::(), field_layout::()]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + epoch: decoder.deserialize_next::()?, + max_blobs_per_block: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for BlobParameters { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.epoch)?, + Merkleized::hash_tree_root(&self.max_blobs_per_block)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for BlobParameters { + fn is_composite_type() -> bool { + true + } +} + +/// Return the blob-parameter tuple active at `epoch`. +pub fn get_blob_parameters(epoch: Epoch) -> BlobParameters { + BLOB_SCHEDULE + .iter() + .rev() + .find_map(|(entry_epoch, limit)| { + (epoch >= *entry_epoch).then_some(BlobParameters { + epoch: *entry_epoch, + max_blobs_per_block: *limit, + }) + }) + .unwrap_or(BlobParameters { + epoch: ELECTRA_FORK_EPOCH, + max_blobs_per_block: MAX_BLOBS_PER_BLOCK, + }) +} + +/// Tracks the active signing version and the version it succeeded. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct Fork { + /// Signing version active before `epoch`. + pub previous_version: Version, + /// Signing version active from `epoch` onward. + pub current_version: Version, + /// Epoch at which the current version became active. + pub epoch: Epoch, +} + +/// Domain-separation tuple fed into signing-domain construction. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct ForkData { + /// Signing version used for domain separation. + pub current_version: Version, + /// Root of the validator set at genesis, tying the signature to this chain. + pub genesis_validators_root: Root, +} + +/// Casper finality checkpoint: the `(epoch, block_root)` pair finality refers to. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Checkpoint { + /// Epoch the checkpoint is at. + pub epoch: Epoch, + /// Block root at the start of `epoch`. + pub root: Root, +} + +/// Per-historical-period roots stored in `BeaconState.historical_summaries`. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct HistoricalSummary { + /// Merkle root of the block-roots ring buffer for the period. + pub block_summary_root: Root, + /// Merkle root of the state-roots ring buffer for the period. + pub state_summary_root: Root, +} + +/// Deposit-voting data observed in a block and aggregated across the voting period. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct Eth1Data { + /// Root of the deposit-contract Merkle tree at the voted deposit-chain block. + pub deposit_root: Root, + /// Total deposits observed up to and including the voted deposit-chain block. + pub deposit_count: u64, + /// Hash of the voted deposit-chain block. + pub block_hash: Hash32, +} + +/// Helper tuple whose tree root is the BLS signing message. +/// +/// This is not a network message. It exists so signatures bind an object root +/// to the domain that identifies its message kind and signing context. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct SigningData { + /// Tree root of the object being signed. + pub object_root: Root, + /// Signing domain combining the domain type and signing-version root. + pub domain: Domain, +} + +impl SszSized for Fork { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for Fork { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.previous_version)?; + encoder.write_field(&self.current_version)?; + encoder.write_field(&self.epoch)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for Fork { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + previous_version: decoder.deserialize_next::()?, + current_version: decoder.deserialize_next::()?, + epoch: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for Fork { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.previous_version)?, + Merkleized::hash_tree_root(&self.current_version)?, + Merkleized::hash_tree_root(&self.epoch)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for Fork { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for ForkData { + fn is_variable_size() -> bool { + let fields = [field_layout::(), field_layout::()]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [field_layout::(), field_layout::()]; + container_size_hint(&fields) + } +} + +impl Serialize for ForkData { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.current_version)?; + encoder.write_field(&self.genesis_validators_root)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for ForkData { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [field_layout::(), field_layout::()]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + current_version: decoder.deserialize_next::()?, + genesis_validators_root: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for ForkData { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.current_version)?, + Merkleized::hash_tree_root(&self.genesis_validators_root)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for ForkData { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for Checkpoint { + fn is_variable_size() -> bool { + let fields = [field_layout::(), field_layout::()]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [field_layout::(), field_layout::()]; + container_size_hint(&fields) + } +} + +impl Serialize for Checkpoint { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.epoch)?; + encoder.write_field(&self.root)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for Checkpoint { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [field_layout::(), field_layout::()]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + epoch: decoder.deserialize_next::()?, + root: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for Checkpoint { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.epoch)?, + Merkleized::hash_tree_root(&self.root)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for Checkpoint { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for HistoricalSummary { + fn is_variable_size() -> bool { + let fields = [field_layout::(), field_layout::()]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [field_layout::(), field_layout::()]; + container_size_hint(&fields) + } +} + +impl Serialize for HistoricalSummary { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.block_summary_root)?; + encoder.write_field(&self.state_summary_root)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for HistoricalSummary { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [field_layout::(), field_layout::()]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + block_summary_root: decoder.deserialize_next::()?, + state_summary_root: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for HistoricalSummary { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.block_summary_root)?, + Merkleized::hash_tree_root(&self.state_summary_root)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for HistoricalSummary { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for Eth1Data { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for Eth1Data { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.deposit_root)?; + encoder.write_field(&self.deposit_count)?; + encoder.write_field(&self.block_hash)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for Eth1Data { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + deposit_root: decoder.deserialize_next::()?, + deposit_count: decoder.deserialize_next::()?, + block_hash: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for Eth1Data { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.deposit_root)?, + Merkleized::hash_tree_root(&self.deposit_count)?, + Merkleized::hash_tree_root(&self.block_hash)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for Eth1Data { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SigningData { + fn is_variable_size() -> bool { + let fields = [field_layout::(), field_layout::()]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [field_layout::(), field_layout::()]; + container_size_hint(&fields) + } +} + +impl Serialize for SigningData { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.object_root)?; + encoder.write_field(&self.domain)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SigningData { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [field_layout::(), field_layout::()]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + object_root: decoder.deserialize_next::()?, + domain: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SigningData { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.object_root)?, + Merkleized::hash_tree_root(&self.domain)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SigningData { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/containers/data_availability.rs b/moonglass-core/src/containers/data_availability.rs new file mode 100644 index 0000000..33805c2 --- /dev/null +++ b/moonglass-core/src/containers/data_availability.rs @@ -0,0 +1,833 @@ +//! Data-availability sidecars and validation helpers. + +use crate::ssz::prelude::*; +use sha2::{Digest, Sha256}; +use thiserror::Error; + +use super::SignedBeaconBlock; +use crate::constants::{ + DATA_COLUMN_SIDECAR_SUBNET_COUNT, MAX_BLOB_COMMITMENTS_PER_BLOCK, MAX_REQUEST_BLOCKS_DENEB, + NUMBER_OF_COLUMNS, NUMBER_OF_CUSTODY_GROUPS, +}; +use crate::crypto::kzg::{ + EthereumKzgSetup, KzgError, compute_cells_and_kzg_proofs, recover_cells_and_kzg_proofs, + verify_cell_kzg_proof_batch, +}; +use crate::primitives::{ + Cell, CellIndex, ColumnIndex, CustodyIndex, KZGCommitment, KZGProof, NodeId, Root, RowIndex, + Slot, SubnetId, +}; + +/// Version byte for partial data-column message group identifiers. +pub const PARTIAL_DATA_COLUMN_GROUP_ID_VERSION: u8 = 0x01; + +/// Errors returned by pure data-availability helpers. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +#[non_exhaustive] +pub enum DataAvailabilityError { + /// Requested more custody groups than the protocol defines. + #[error("requested {requested} custody groups but limit is {limit}")] + TooManyCustodyGroups { + /// Requested custody group count. + requested: u64, + /// Maximum custody group count. + limit: u64, + }, + + /// Custody group index exceeds the configured group count. + #[error("custody group {index} out of range, limit {limit}")] + CustodyGroupOutOfRange { + /// Supplied custody group index. + index: u64, + /// Exclusive upper bound. + limit: u64, + }, + + /// Decimal node identifier contained a non-digit byte. + #[error("node identifier must be a decimal uint256")] + InvalidNodeIdDecimal, + + /// Decimal node identifier exceeded `uint256`. + #[error("node identifier exceeds uint256")] + NodeIdOverflow, + + /// More blob rows were supplied than a sidecar list can carry. + #[error("too many blob rows: {rows}, limit {limit}")] + TooManyBlobRows { + /// Supplied row count. + rows: usize, + /// Maximum row count. + limit: usize, + }, + + /// Cell and proof vectors for one row have different lengths. + #[error("row {row} cell/proof length mismatch: cells {cells}, proofs {proofs}")] + CellsProofsLengthMismatch { + /// Row index. + row: usize, + /// Cell count. + cells: usize, + /// Proof count. + proofs: usize, + }, + + /// Cell/proof vectors for one row do not cover every column. + #[error("row {row} column count mismatch: expected {expected}, cells {cells}, proofs {proofs}")] + CellsProofsColumnCountMismatch { + /// Row index. + row: usize, + /// Expected column count. + expected: usize, + /// Cell count. + cells: usize, + /// Proof count. + proofs: usize, + }, + + /// SSZ serialization failed. + #[error(transparent)] + SszSerialize(#[from] SerializeError), + + /// SSZ hash-tree-root computation failed. + #[error(transparent)] + Merkleization(#[from] MerkleizationError), +} + +/// Column sidecar carrying cells and proofs for a beacon block. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct DataColumnSidecar { + /// Column index. This is also the cell index for every blob commitment. + pub index: ColumnIndex, + /// Cells for this column, one per blob commitment. + pub column: List, + /// KZG proofs for the column cells. + pub kzg_proofs: List, + /// Slot of the beacon block this sidecar belongs to. + pub slot: Slot, + /// Root of the beacon block this sidecar belongs to. + pub beacon_block_root: Root, +} + +/// Request identifier for data-column sidecars by block root. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct DataColumnsByRootIdentifier { + /// Beacon block root to request columns for. + pub block_root: Root, + /// Requested column indices. + pub columns: List, +} + +/// Matrix entry pairing one cell with its proof and coordinates. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct MatrixEntry { + /// Cell bytes. + pub cell: Cell, + /// KZG proof for the cell. + pub kzg_proof: KZGProof, + /// Column coordinate. + pub column_index: ColumnIndex, + /// Row coordinate. + pub row_index: RowIndex, +} + +/// Partial column sidecar used by partial-message dissemination. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct PartialDataColumnSidecar { + /// Bitmap marking which blob rows are present. + pub cells_present_bitmap: Bitlist, + /// Present cells in bitmap order. + pub partial_column: List, + /// Proofs for present cells in bitmap order. + pub kzg_proofs: List, +} + +/// Group identifier for partial data-column messages. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct PartialDataColumnGroupID { + /// Slot of the associated beacon block. + pub slot: Slot, + /// Root of the associated beacon block. + pub beacon_block_root: Root, +} + +/// A blob row represented by all extended cells and proofs. +pub type CellsAndKzgProofs = (Vec, Vec); + +/// Parse a decimal `uint256` node identifier into little-endian bytes. +pub fn node_id_from_decimal(decimal: &str) -> Result { + let mut out = [0u8; 32]; + let trimmed = decimal.trim(); + if trimmed.is_empty() { + return Err(DataAvailabilityError::InvalidNodeIdDecimal); + } + for byte in trimmed.bytes() { + if !byte.is_ascii_digit() { + return Err(DataAvailabilityError::InvalidNodeIdDecimal); + } + let digit = byte - b'0'; + multiply_node_id_by_10_and_add(&mut out, digit)?; + } + Ok(NodeId::from_le_bytes(out)) +} + +/// Return the custody groups assigned to a node identifier. +pub fn get_custody_groups( + node_id: NodeId, + custody_group_count: u64, +) -> Result, DataAvailabilityError> { + let group_limit = NUMBER_OF_CUSTODY_GROUPS as u64; + if custody_group_count > group_limit { + return Err(DataAvailabilityError::TooManyCustodyGroups { + requested: custody_group_count, + limit: group_limit, + }); + } + if custody_group_count == group_limit { + return Ok((0..NUMBER_OF_CUSTODY_GROUPS) + .map(|index| CustodyIndex::new(index as u64)) + .collect()); + } + + let custody_group_len = usize::try_from(custody_group_count).map_err(|_| { + DataAvailabilityError::TooManyCustodyGroups { + requested: custody_group_count, + limit: group_limit, + } + })?; + let mut current_id = node_id.to_le_bytes(); + let mut custody_groups = Vec::with_capacity(custody_group_len); + while custody_groups.len() < custody_group_len { + let digest = Sha256::digest(current_id); + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&digest[..8]); + let custody_group = CustodyIndex::new(u64::from_le_bytes(bytes) % group_limit); + if !custody_groups.contains(&custody_group) { + custody_groups.push(custody_group); + } + increment_node_id(&mut current_id); + } + custody_groups.sort_unstable(); + Ok(custody_groups) +} + +/// Return the columns assigned to a custody group. +pub fn compute_columns_for_custody_group( + custody_group: CustodyIndex, +) -> Result, DataAvailabilityError> { + let group_limit = NUMBER_OF_CUSTODY_GROUPS as u64; + if custody_group.as_u64() >= group_limit { + return Err(DataAvailabilityError::CustodyGroupOutOfRange { + index: custody_group.as_u64(), + limit: group_limit, + }); + } + let columns_per_group = NUMBER_OF_COLUMNS / NUMBER_OF_CUSTODY_GROUPS; + Ok((0..columns_per_group) + .map(|i| ColumnIndex::new(NUMBER_OF_CUSTODY_GROUPS as u64 * i as u64 + custody_group.0)) + .collect()) +} + +/// Return the maximum number of data-column sidecars in a request. +pub const fn compute_max_request_data_column_sidecars() -> u64 { + MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS as u64 +} + +/// Return the subnet for a data-column sidecar. +pub const fn compute_subnet_for_data_column_sidecar(column_index: ColumnIndex) -> SubnetId { + SubnetId::new(column_index.0 % DATA_COLUMN_SIDECAR_SUBNET_COUNT) +} + +/// Compute the flattened data-availability matrix for blobs. +pub fn compute_matrix>( + setup: &EthereumKzgSetup, + blobs: &[B], +) -> Result, KzgError> { + let mut matrix = Vec::new(); + for (blob_index, blob) in blobs.iter().enumerate() { + let (cells, proofs) = compute_cells_and_kzg_proofs(setup, blob.as_ref())?; + for (cell_index, (cell, proof)) in cells.into_iter().zip(proofs).enumerate() { + matrix.push(MatrixEntry { + cell, + kzg_proof: proof, + column_index: ColumnIndex::new(cell_index as u64), + row_index: RowIndex::new(blob_index as u64), + }); + } + } + Ok(matrix) +} + +/// Recover a full flattened matrix from partial entries. +pub fn recover_matrix( + setup: &EthereumKzgSetup, + partial_matrix: &[MatrixEntry], + blob_count: u64, +) -> Result, KzgError> { + let mut matrix = Vec::new(); + for blob_index in 0..blob_count { + let mut row_entries = partial_matrix + .iter() + .copied() + .filter(|entry| entry.row_index.as_u64() == blob_index) + .collect::>(); + row_entries.sort_unstable_by_key(|entry| entry.column_index.as_u64()); + + let cell_indices = row_entries + .iter() + .map(|entry| CellIndex::new(entry.column_index.as_u64())) + .collect::>(); + let cells = row_entries + .iter() + .map(|entry| entry.cell) + .collect::>(); + let (recovered_cells, recovered_proofs) = + recover_cells_and_kzg_proofs(setup, cell_indices, cells)?; + for (cell_index, (cell, proof)) in recovered_cells + .into_iter() + .zip(recovered_proofs) + .enumerate() + { + matrix.push(MatrixEntry { + cell, + kzg_proof: proof, + column_index: ColumnIndex::new(cell_index as u64), + row_index: RowIndex::new(blob_index), + }); + } + } + Ok(matrix) +} + +/// Assemble data-column sidecars from blob-row cells and proofs. +pub fn get_data_column_sidecars( + beacon_block_root: Root, + slot: Slot, + cells_and_kzg_proofs: &[CellsAndKzgProofs], +) -> Result, DataAvailabilityError> { + validate_cells_and_kzg_proofs(cells_and_kzg_proofs)?; + + let mut sidecars = Vec::with_capacity(NUMBER_OF_COLUMNS); + for column_index in 0..NUMBER_OF_COLUMNS { + let mut column = List::default(); + let mut kzg_proofs = List::default(); + for (cells, proofs) in cells_and_kzg_proofs { + column.push(cells[column_index]).map_err(|_| { + DataAvailabilityError::TooManyBlobRows { + rows: cells_and_kzg_proofs.len(), + limit: MAX_BLOB_COMMITMENTS_PER_BLOCK, + } + })?; + kzg_proofs.push(proofs[column_index]).map_err(|_| { + DataAvailabilityError::TooManyBlobRows { + rows: cells_and_kzg_proofs.len(), + limit: MAX_BLOB_COMMITMENTS_PER_BLOCK, + } + })?; + } + sidecars.push(DataColumnSidecar { + index: ColumnIndex::new(column_index as u64), + column, + kzg_proofs, + slot, + beacon_block_root, + }); + } + Ok(sidecars) +} + +/// Assemble data-column sidecars using a signed block as the block identifier. +pub fn get_data_column_sidecars_from_block( + signed_block: &SignedBeaconBlock, + cells_and_kzg_proofs: &[CellsAndKzgProofs], +) -> Result, DataAvailabilityError> { + let block = signed_block.message.clone(); + let block_root = Root::from(Merkleized::hash_tree_root(&block)?); + get_data_column_sidecars(block_root, signed_block.message.slot, cells_and_kzg_proofs) +} + +/// Rebuild sidecars from one known sidecar and full blob-row cells and proofs. +pub fn get_data_column_sidecars_from_column_sidecar( + sidecar: &DataColumnSidecar, + cells_and_kzg_proofs: &[CellsAndKzgProofs], +) -> Result, DataAvailabilityError> { + get_data_column_sidecars( + sidecar.beacon_block_root, + sidecar.slot, + cells_and_kzg_proofs, + ) +} + +/// Verify the shape of a data column sidecar against block commitments. +pub fn verify_data_column_sidecar( + sidecar: &DataColumnSidecar, + kzg_commitments: &List, +) -> bool { + if sidecar.index.as_usize() >= NUMBER_OF_COLUMNS { + return false; + } + if sidecar.column.is_empty() { + return false; + } + if sidecar.column.len() != kzg_commitments.len() { + return false; + } + if sidecar.column.len() != sidecar.kzg_proofs.len() { + return false; + } + true +} + +/// Verify the KZG proofs carried by a data column sidecar. +pub fn verify_data_column_sidecar_kzg_proofs( + sidecar: &DataColumnSidecar, + kzg_commitments: &List, + setup: &EthereumKzgSetup, +) -> Result { + let cell_indices = vec![CellIndex::new(sidecar.index.as_u64()); sidecar.column.len()]; + let commitments = kzg_commitments.iter().copied().collect::>(); + let cells = sidecar.column.iter().copied().collect::>(); + let proofs = sidecar.kzg_proofs.iter().copied().collect::>(); + verify_cell_kzg_proof_batch(setup, &commitments, &cell_indices, &cells, &proofs) +} + +/// Verify the shape of a partial data column sidecar. +pub fn verify_partial_data_column_sidecar( + sidecar: &PartialDataColumnSidecar, + kzg_commitments: &List, +) -> bool { + if sidecar.cells_present_bitmap.len() != kzg_commitments.len() { + return false; + } + if sidecar.cells_present_bitmap.count_ones() == 0 { + return false; + } + if sidecar.partial_column.len() != sidecar.kzg_proofs.len() { + return false; + } + if sidecar.partial_column.len() != sidecar.cells_present_bitmap.count_ones() { + return false; + } + true +} + +/// Verify KZG proofs for a partial data column sidecar. +pub fn verify_partial_data_column_sidecar_kzg_proofs( + sidecar: &PartialDataColumnSidecar, + kzg_commitments: &List, + column_index: ColumnIndex, + setup: &EthereumKzgSetup, +) -> Result { + if column_index.as_usize() >= NUMBER_OF_COLUMNS { + return Ok(false); + } + if !verify_partial_data_column_sidecar(sidecar, kzg_commitments) { + return Ok(false); + } + + let commitments = kzg_commitments + .iter() + .copied() + .zip(sidecar.cells_present_bitmap.iter().copied()) + .filter_map(|(commitment, present)| present.then_some(commitment)) + .collect::>(); + let cell_indices = vec![CellIndex::new(column_index.as_u64()); commitments.len()]; + let cells = sidecar.partial_column.iter().copied().collect::>(); + let proofs = sidecar.kzg_proofs.iter().copied().collect::>(); + verify_cell_kzg_proof_batch(setup, &commitments, &cell_indices, &cells, &proofs) +} + +/// Return the encoded partial data-column group identifier. +pub fn partial_data_column_group_id_bytes( + group_id: &PartialDataColumnGroupID, +) -> Result, DataAvailabilityError> { + let mut bytes = vec![PARTIAL_DATA_COLUMN_GROUP_ID_VERSION]; + group_id.serialize(&mut bytes)?; + Ok(bytes) +} + +/// Validate cell/proof row shape before sidecar construction. +pub fn validate_cells_and_kzg_proofs( + cells_and_kzg_proofs: &[CellsAndKzgProofs], +) -> Result<(), DataAvailabilityError> { + if cells_and_kzg_proofs.len() > MAX_BLOB_COMMITMENTS_PER_BLOCK { + return Err(DataAvailabilityError::TooManyBlobRows { + rows: cells_and_kzg_proofs.len(), + limit: MAX_BLOB_COMMITMENTS_PER_BLOCK, + }); + } + for (row, (cells, proofs)) in cells_and_kzg_proofs.iter().enumerate() { + if cells.len() != proofs.len() { + return Err(DataAvailabilityError::CellsProofsLengthMismatch { + row, + cells: cells.len(), + proofs: proofs.len(), + }); + } + if cells.len() != NUMBER_OF_COLUMNS || proofs.len() != NUMBER_OF_COLUMNS { + return Err(DataAvailabilityError::CellsProofsColumnCountMismatch { + row, + expected: NUMBER_OF_COLUMNS, + cells: cells.len(), + proofs: proofs.len(), + }); + } + } + Ok(()) +} + +/// Multiply a little-endian `uint256` by 10 and add one decimal digit. +pub fn multiply_node_id_by_10_and_add( + bytes: &mut [u8; 32], + digit: u8, +) -> Result<(), DataAvailabilityError> { + let mut carry = u16::from(digit); + for byte in bytes { + let next = u16::from(*byte) * 10 + carry; + #[allow(clippy::cast_possible_truncation)] + { + *byte = (next & 0xff) as u8; + } + carry = next >> 8; + } + if carry != 0 { + return Err(DataAvailabilityError::NodeIdOverflow); + } + Ok(()) +} + +/// Increment a little-endian `uint256`, wrapping to zero on overflow. +pub fn increment_node_id(bytes: &mut [u8; 32]) { + for byte in bytes { + let (next, overflow) = byte.overflowing_add(1); + *byte = next; + if !overflow { + break; + } + } +} + +impl SszSized for DataColumnSidecar { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for DataColumnSidecar { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.index)?; + encoder.write_field(&self.column)?; + encoder.write_field(&self.kzg_proofs)?; + encoder.write_field(&self.slot)?; + encoder.write_field(&self.beacon_block_root)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for DataColumnSidecar { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + index: decoder.deserialize_next::()?, + column: decoder.deserialize_next::>()?, + kzg_proofs: decoder + .deserialize_next::>()?, + slot: decoder.deserialize_next::()?, + beacon_block_root: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for DataColumnSidecar { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.index)?, + Merkleized::hash_tree_root(&self.column)?, + Merkleized::hash_tree_root(&self.kzg_proofs)?, + Merkleized::hash_tree_root(&self.slot)?, + Merkleized::hash_tree_root(&self.beacon_block_root)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for DataColumnSidecar { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for DataColumnsByRootIdentifier { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::>(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::>(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for DataColumnsByRootIdentifier { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.block_root)?; + encoder.write_field(&self.columns)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for DataColumnsByRootIdentifier { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::>(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + block_root: decoder.deserialize_next::()?, + columns: decoder.deserialize_next::>()?, + }) + } +} + +impl Merkleized for DataColumnsByRootIdentifier { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.block_root)?, + Merkleized::hash_tree_root(&self.columns)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for DataColumnsByRootIdentifier { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for MatrixEntry { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for MatrixEntry { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.cell)?; + encoder.write_field(&self.kzg_proof)?; + encoder.write_field(&self.column_index)?; + encoder.write_field(&self.row_index)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for MatrixEntry { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + cell: decoder.deserialize_next::()?, + kzg_proof: decoder.deserialize_next::()?, + column_index: decoder.deserialize_next::()?, + row_index: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for MatrixEntry { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.cell)?, + Merkleized::hash_tree_root(&self.kzg_proof)?, + Merkleized::hash_tree_root(&self.column_index)?, + Merkleized::hash_tree_root(&self.row_index)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for MatrixEntry { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for PartialDataColumnSidecar { + fn is_variable_size() -> bool { + let fields = [ + field_layout::>(), + field_layout::>(), + field_layout::>(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::>(), + field_layout::>(), + field_layout::>(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for PartialDataColumnSidecar { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.cells_present_bitmap)?; + encoder.write_field(&self.partial_column)?; + encoder.write_field(&self.kzg_proofs)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for PartialDataColumnSidecar { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::>(), + field_layout::>(), + field_layout::>(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + cells_present_bitmap: decoder + .deserialize_next::>()?, + partial_column: decoder + .deserialize_next::>()?, + kzg_proofs: decoder + .deserialize_next::>()?, + }) + } +} + +impl Merkleized for PartialDataColumnSidecar { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.cells_present_bitmap)?, + Merkleized::hash_tree_root(&self.partial_column)?, + Merkleized::hash_tree_root(&self.kzg_proofs)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for PartialDataColumnSidecar { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for PartialDataColumnGroupID { + fn is_variable_size() -> bool { + let fields = [field_layout::(), field_layout::()]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [field_layout::(), field_layout::()]; + container_size_hint(&fields) + } +} + +impl Serialize for PartialDataColumnGroupID { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.slot)?; + encoder.write_field(&self.beacon_block_root)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for PartialDataColumnGroupID { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [field_layout::(), field_layout::()]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + slot: decoder.deserialize_next::()?, + beacon_block_root: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for PartialDataColumnGroupID { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.slot)?, + Merkleized::hash_tree_root(&self.beacon_block_root)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for PartialDataColumnGroupID { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/containers/execution.rs b/moonglass-core/src/containers/execution.rs new file mode 100644 index 0000000..3cccad2 --- /dev/null +++ b/moonglass-core/src/containers/execution.rs @@ -0,0 +1,719 @@ +//! Execution-layer data carried through consensus. +//! +//! Execution payloads contain opaque execution transactions and block-access +//! list bytes. Consensus does not inspect their internal execution semantics. +//! It tracks roots, hashes, requests, and builder commitments needed by the +//! beacon-state transition. +//! +//! Inclusion-list bid extensions are not represented in the current +//! `ExecutionPayloadBid` shape. + +use crate::constants::{ + BYTES_PER_LOGS_BLOOM, MAX_BLOB_COMMITMENTS_PER_BLOCK, MAX_BYTES_PER_TRANSACTION, + MAX_EXTRA_DATA_BYTES, MAX_TRANSACTIONS_PER_PAYLOAD, MAX_WITHDRAWALS_PER_PAYLOAD, +}; +use crate::containers::{ExecutionRequests, Withdrawal}; +use crate::primitives::{ + BLSSignature, BuilderIndex, Bytes32, ExecutionAddress, Gwei, Hash32, KZGCommitment, Root, Slot, + Uint256, +}; +use crate::ssz::prelude::*; + +/// Opaque RLP-encoded block access list. Layout is not unpacked by consensus. +pub type BlockAccessList = List; + +/// A single execution-layer transaction as an opaque byte list. +pub type Transaction = List; + +/// The list of transactions an `ExecutionPayload` carries. +pub type Transactions = List; + +/// Execution-layer proof-of-work block summary used by historical fork choice. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct PowBlock { + /// Execution-layer block hash. + pub block_hash: Hash32, + /// Parent execution-layer block hash. + pub parent_hash: Hash32, + /// Accumulated proof-of-work difficulty. + pub total_difficulty: Uint256, +} + +/// Execution-layer block payload delivered for a beacon block. +/// +/// Consensus treats transactions and block-access lists as opaque bytes here. +/// [`BeaconState::verify_execution_payload_envelope`](crate::containers::BeaconState::verify_execution_payload_envelope) checks the payload against the +/// accepted builder bid and expected consensus-side commitments. Execution +/// engine validity is outside the current boundary. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct ExecutionPayload { + /// Parent execution block hash. + pub parent_hash: Hash32, + /// Address that receives priority fees and builder payments. + pub fee_recipient: ExecutionAddress, + /// State root of the execution-layer trie after applying this payload. + pub state_root: Bytes32, + /// Root of the transaction-receipts trie. + pub receipts_root: Bytes32, + /// Bloom filter over event logs emitted by transactions in the payload. + pub logs_bloom: Vector, + /// RANDAO mix carried from the consensus layer for execution-layer use. + pub prev_randao: Bytes32, + /// Execution-layer block height. + pub block_number: u64, + /// Maximum gas budget for the block. + pub gas_limit: u64, + /// Gas actually consumed by the block's transactions. + pub gas_used: u64, + /// Unix timestamp the payload was produced for. + pub timestamp: u64, + /// Proposer-supplied freeform bytes (capped to 32). + pub extra_data: List, + /// Base fee per gas (little-endian 256-bit integer). + pub base_fee_per_gas: Uint256, + /// Execution-layer block hash. + pub block_hash: Hash32, + /// Opaque transactions, each itself a length-prefixed byte list. + pub transactions: Transactions, + /// Withdrawals applied at the execution layer (capped per payload). + pub withdrawals: List, + /// Blob gas consumed by this payload. + pub blob_gas_used: u64, + /// Excess blob gas used to price the next payload's blobs. + pub excess_blob_gas: u64, + /// Block-access list as opaque RLP-encoded bytes. + pub block_access_list: BlockAccessList, + /// Slot tag echoed back to the consensus layer. + pub slot_number: u64, +} + +/// Builder's bid for the proposer's slot. +/// +/// Non-self-build bids are signed by the builder. Self-build bids carry the +/// self-build sentinel and rely on the beacon proposer's block signature +/// instead. The proposer commits to the chosen bid by including it in the signed +/// beacon block body. +/// Consumed in the current block by +/// [`BeaconState::process_execution_payload_bid`](crate::containers::BeaconState::process_execution_payload_bid), which updates +/// `BeaconState::latest_execution_payload_bid` and the active +/// builder-payment accumulator. The next child block uses the bid's +/// `execution_requests_root` to prove the parent payload handoff. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct ExecutionPayloadBid { + /// Execution-layer parent block hash the bid is conditioned on. + pub parent_block_hash: Hash32, + /// Consensus-layer parent block root the bid is conditioned on. + pub parent_block_root: Root, + /// Hash the builder commits to producing as the next execution block. + pub block_hash: Hash32, + /// RANDAO value the builder expects the proposer to reveal for the slot. + pub prev_randao: Bytes32, + /// Address receiving priority fees and the accepted bid value. + pub fee_recipient: ExecutionAddress, + /// Gas budget the builder commits to honoring. + pub gas_limit: u64, + /// Builder offering the bid, or `BUILDER_INDEX_SELF_BUILD` for self-builds. + pub builder_index: BuilderIndex, + /// Slot the bid is for, which must equal the proposer's slot at inclusion time. + pub slot: Slot, + /// Trustless Gwei amount the builder will pay the proposer if accepted. + pub value: Gwei, + /// Trusted execution-layer payment marker, zero for broadcast bids. + pub execution_payment: Gwei, + /// KZG commitments the builder pre-commits to including blobs for. + pub blob_kzg_commitments: List, + /// Tree root of the execution-to-consensus requests the builder commits to. + pub execution_requests_root: Root, +} + +/// Builder bid plus its signature field. +/// +/// Included in [`crate::containers::BeaconBlockBody`] and verified by +/// [`BeaconState::process_execution_payload_bid`](crate::containers::BeaconState::process_execution_payload_bid). +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct SignedExecutionPayloadBid { + /// The bid being signed. + pub message: ExecutionPayloadBid, + /// Builder signature under `DOMAIN_BEACON_BUILDER`, or the point at infinity + /// for self-build bids. + pub signature: BLSSignature, +} + +/// Delivered payload plus execution-to-consensus requests and provenance roots. +/// +/// Checked by [`BeaconState::verify_execution_payload_envelope`](crate::containers::BeaconState::verify_execution_payload_envelope). Fork choice records a +/// checked envelope through [`crate::fork_choice::Store::on_execution_payload_envelope()`] +/// in [`crate::fork_choice::Store::payloads`]. That store entry means +/// "recorded after the current consensus-side envelope checks", not a complete +/// execution-engine or blob-availability verdict. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct ExecutionPayloadEnvelope { + /// The execution payload delivered for the bid. + pub payload: ExecutionPayload, + /// Execution-to-consensus requests carried by the payload. + pub execution_requests: ExecutionRequests, + /// Accepted bid's builder index, or the self-build sentinel. + pub builder_index: BuilderIndex, + /// Root of the beacon block this envelope is bound to. + pub beacon_block_root: Root, + /// Root of the parent beacon block. + pub parent_beacon_block_root: Root, +} + +/// Envelope plus the signature from the required envelope signer. +/// +/// Network-facing entry object for +/// [`crate::fork_choice::Store::on_execution_payload_envelope()`]. The state transition +/// validates the signer and bid commitments, and fork choice records the +/// envelope only after those checks pass. Non-self-build envelopes are signed by +/// the registered builder. Self-build envelopes are signed by the beacon +/// proposer. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct SignedExecutionPayloadEnvelope { + /// The envelope being signed. + pub message: ExecutionPayloadEnvelope, + /// Signature under `DOMAIN_BEACON_BUILDER` from the required envelope signer. + pub signature: BLSSignature, +} + +impl SszSized for PowBlock { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for PowBlock { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.block_hash)?; + encoder.write_field(&self.parent_hash)?; + encoder.write_field(&self.total_difficulty)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for PowBlock { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + block_hash: decoder.deserialize_next::()?, + parent_hash: decoder.deserialize_next::()?, + total_difficulty: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for PowBlock { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.block_hash)?, + Merkleized::hash_tree_root(&self.parent_hash)?, + Merkleized::hash_tree_root(&self.total_difficulty)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for PowBlock { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for ExecutionPayload { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for ExecutionPayload { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.parent_hash)?; + encoder.write_field(&self.fee_recipient)?; + encoder.write_field(&self.state_root)?; + encoder.write_field(&self.receipts_root)?; + encoder.write_field(&self.logs_bloom)?; + encoder.write_field(&self.prev_randao)?; + encoder.write_field(&self.block_number)?; + encoder.write_field(&self.gas_limit)?; + encoder.write_field(&self.gas_used)?; + encoder.write_field(&self.timestamp)?; + encoder.write_field(&self.extra_data)?; + encoder.write_field(&self.base_fee_per_gas)?; + encoder.write_field(&self.block_hash)?; + encoder.write_field(&self.transactions)?; + encoder.write_field(&self.withdrawals)?; + encoder.write_field(&self.blob_gas_used)?; + encoder.write_field(&self.excess_blob_gas)?; + encoder.write_field(&self.block_access_list)?; + encoder.write_field(&self.slot_number)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for ExecutionPayload { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + parent_hash: decoder.deserialize_next::()?, + fee_recipient: decoder.deserialize_next::()?, + state_root: decoder.deserialize_next::()?, + receipts_root: decoder.deserialize_next::()?, + logs_bloom: decoder.deserialize_next::>()?, + prev_randao: decoder.deserialize_next::()?, + block_number: decoder.deserialize_next::()?, + gas_limit: decoder.deserialize_next::()?, + gas_used: decoder.deserialize_next::()?, + timestamp: decoder.deserialize_next::()?, + extra_data: decoder.deserialize_next::>()?, + base_fee_per_gas: decoder.deserialize_next::()?, + block_hash: decoder.deserialize_next::()?, + transactions: decoder.deserialize_next::()?, + withdrawals: decoder + .deserialize_next::>()?, + blob_gas_used: decoder.deserialize_next::()?, + excess_blob_gas: decoder.deserialize_next::()?, + block_access_list: decoder.deserialize_next::()?, + slot_number: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for ExecutionPayload { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.parent_hash)?, + Merkleized::hash_tree_root(&self.fee_recipient)?, + Merkleized::hash_tree_root(&self.state_root)?, + Merkleized::hash_tree_root(&self.receipts_root)?, + Merkleized::hash_tree_root(&self.logs_bloom)?, + Merkleized::hash_tree_root(&self.prev_randao)?, + Merkleized::hash_tree_root(&self.block_number)?, + Merkleized::hash_tree_root(&self.gas_limit)?, + Merkleized::hash_tree_root(&self.gas_used)?, + Merkleized::hash_tree_root(&self.timestamp)?, + Merkleized::hash_tree_root(&self.extra_data)?, + Merkleized::hash_tree_root(&self.base_fee_per_gas)?, + Merkleized::hash_tree_root(&self.block_hash)?, + Merkleized::hash_tree_root(&self.transactions)?, + Merkleized::hash_tree_root(&self.withdrawals)?, + Merkleized::hash_tree_root(&self.blob_gas_used)?, + Merkleized::hash_tree_root(&self.excess_blob_gas)?, + Merkleized::hash_tree_root(&self.block_access_list)?, + Merkleized::hash_tree_root(&self.slot_number)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for ExecutionPayload { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for ExecutionPayloadBid { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for ExecutionPayloadBid { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.parent_block_hash)?; + encoder.write_field(&self.parent_block_root)?; + encoder.write_field(&self.block_hash)?; + encoder.write_field(&self.prev_randao)?; + encoder.write_field(&self.fee_recipient)?; + encoder.write_field(&self.gas_limit)?; + encoder.write_field(&self.builder_index)?; + encoder.write_field(&self.slot)?; + encoder.write_field(&self.value)?; + encoder.write_field(&self.execution_payment)?; + encoder.write_field(&self.blob_kzg_commitments)?; + encoder.write_field(&self.execution_requests_root)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for ExecutionPayloadBid { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + parent_block_hash: decoder.deserialize_next::()?, + parent_block_root: decoder.deserialize_next::()?, + block_hash: decoder.deserialize_next::()?, + prev_randao: decoder.deserialize_next::()?, + fee_recipient: decoder.deserialize_next::()?, + gas_limit: decoder.deserialize_next::()?, + builder_index: decoder.deserialize_next::()?, + slot: decoder.deserialize_next::()?, + value: decoder.deserialize_next::()?, + execution_payment: decoder.deserialize_next::()?, + blob_kzg_commitments: decoder + .deserialize_next::>()?, + execution_requests_root: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for ExecutionPayloadBid { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.parent_block_hash)?, + Merkleized::hash_tree_root(&self.parent_block_root)?, + Merkleized::hash_tree_root(&self.block_hash)?, + Merkleized::hash_tree_root(&self.prev_randao)?, + Merkleized::hash_tree_root(&self.fee_recipient)?, + Merkleized::hash_tree_root(&self.gas_limit)?, + Merkleized::hash_tree_root(&self.builder_index)?, + Merkleized::hash_tree_root(&self.slot)?, + Merkleized::hash_tree_root(&self.value)?, + Merkleized::hash_tree_root(&self.execution_payment)?, + Merkleized::hash_tree_root(&self.blob_kzg_commitments)?, + Merkleized::hash_tree_root(&self.execution_requests_root)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for ExecutionPayloadBid { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SignedExecutionPayloadBid { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SignedExecutionPayloadBid { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.message)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SignedExecutionPayloadBid { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + message: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SignedExecutionPayloadBid { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.message)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SignedExecutionPayloadBid { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for ExecutionPayloadEnvelope { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for ExecutionPayloadEnvelope { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.payload)?; + encoder.write_field(&self.execution_requests)?; + encoder.write_field(&self.builder_index)?; + encoder.write_field(&self.beacon_block_root)?; + encoder.write_field(&self.parent_beacon_block_root)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for ExecutionPayloadEnvelope { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + payload: decoder.deserialize_next::()?, + execution_requests: decoder.deserialize_next::()?, + builder_index: decoder.deserialize_next::()?, + beacon_block_root: decoder.deserialize_next::()?, + parent_beacon_block_root: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for ExecutionPayloadEnvelope { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.payload)?, + Merkleized::hash_tree_root(&self.execution_requests)?, + Merkleized::hash_tree_root(&self.builder_index)?, + Merkleized::hash_tree_root(&self.beacon_block_root)?, + Merkleized::hash_tree_root(&self.parent_beacon_block_root)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for ExecutionPayloadEnvelope { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SignedExecutionPayloadEnvelope { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SignedExecutionPayloadEnvelope { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.message)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SignedExecutionPayloadEnvelope { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + message: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SignedExecutionPayloadEnvelope { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.message)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SignedExecutionPayloadEnvelope { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/containers/gossip.rs b/moonglass-core/src/containers/gossip.rs new file mode 100644 index 0000000..9c14cd3 --- /dev/null +++ b/moonglass-core/src/containers/gossip.rs @@ -0,0 +1,164 @@ +//! Gossip-facing containers with consensus-defined SSZ shapes. + +use crate::primitives::{BLSSignature, ExecutionAddress, Root, Slot, ValidatorIndex}; +use crate::ssz::prelude::*; + +/// Proposer's execution-payload preferences for a future proposal slot. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProposerPreferences { + /// Dependent block root for the proposer lookahead. + pub dependent_root: Root, + /// Slot the preferences apply to. + pub proposal_slot: Slot, + /// Validator expected to propose at `proposal_slot`. + pub validator_index: ValidatorIndex, + /// Preferred execution-layer fee recipient. + pub fee_recipient: ExecutionAddress, + /// Preferred execution payload gas limit. + pub target_gas_limit: u64, +} + +/// Signed proposer preferences gossip object. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct SignedProposerPreferences { + /// Preferences being signed. + pub message: ProposerPreferences, + /// Validator signature over `message`. + pub signature: BLSSignature, +} + +impl SszSized for ProposerPreferences { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for ProposerPreferences { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.dependent_root)?; + encoder.write_field(&self.proposal_slot)?; + encoder.write_field(&self.validator_index)?; + encoder.write_field(&self.fee_recipient)?; + encoder.write_field(&self.target_gas_limit)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for ProposerPreferences { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + dependent_root: decoder.deserialize_next::()?, + proposal_slot: decoder.deserialize_next::()?, + validator_index: decoder.deserialize_next::()?, + fee_recipient: decoder.deserialize_next::()?, + target_gas_limit: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for ProposerPreferences { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.dependent_root)?, + Merkleized::hash_tree_root(&self.proposal_slot)?, + Merkleized::hash_tree_root(&self.validator_index)?, + Merkleized::hash_tree_root(&self.fee_recipient)?, + Merkleized::hash_tree_root(&self.target_gas_limit)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for ProposerPreferences { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SignedProposerPreferences { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SignedProposerPreferences { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.message)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SignedProposerPreferences { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + message: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SignedProposerPreferences { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.message)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SignedProposerPreferences { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/containers/state.rs b/moonglass-core/src/containers/state.rs new file mode 100644 index 0000000..763554b --- /dev/null +++ b/moonglass-core/src/containers/state.rs @@ -0,0 +1,586 @@ +//! The full consensus state. +//! +//! `BeaconState` combines the validator registry, historical ring buffers, +//! processing queues, builder state, and per-slot working data. +//! Conceptually, it is the chain clock and identity, recent block/state roots, +//! randomness, validator registry and balances, participation/finality records, +//! execution and withdrawal handoff, lifecycle queues, proposer lookahead, and +//! builder/payload-timeliness state. +//! +//! # Field groups +//! +//! Clock and history: `genesis_time`, `slot`, `latest_block_header`, +//! `block_roots`, `state_roots`, historical roots and summaries. Mutated by +//! [`BeaconState::process_slots`](crate::containers::BeaconState::process_slots), +//! [`BeaconState::process_slot`](crate::containers::BeaconState::process_slot), +//! and +//! [`BeaconState::process_block_header`](crate::containers::BeaconState::process_block_header). +//! +//! Validator registry and balances: `validators`, `balances`, slashing totals, +//! participation flags, inactivity scores, sync committees, proposer lookahead. +//! Mutated by operation and epoch-processing phases. +//! +//! Finality: justification checkpoints, finalization checkpoints, and +//! `justification_bits`. Mutated during epoch processing from accumulated +//! participation. +//! +//! Execution and withdrawal handoff: `latest_block_hash`, withdrawal cursors, +//! expected withdrawals, and execution-request queues. Mutated by withdrawals and +//! by the parent-payload handoff. +//! +//! Registry queues and churn: pending deposits, partial withdrawals, +//! consolidations, and balance budgets. Mutated by execution requests and epoch +//! queue processing. +//! +//! Builder registry and payments: `builders`, builder withdrawal cursor, pending +//! payments, pending withdrawals, and `latest_execution_payload_bid`. Mutated by +//! builder bids, beacon attestations for proposal slots, and the parent payload +//! handoff. +//! +//! Payload availability and PTC: `execution_payload_availability` and +//! `ptc_window`. Mutated by slot processing, parent-payload acceptance, and PTC +//! window updates. + +use crate::constants::{ + BUILDER_PAYMENT_WINDOW_LEN, BUILDER_PENDING_WITHDRAWALS_LIMIT, BUILDER_REGISTRY_LIMIT, + EPOCHS_PER_HISTORICAL_VECTOR, EPOCHS_PER_SLASHINGS_VECTOR, ETH1_DATA_VOTES_LEN, + HISTORICAL_ROOTS_LIMIT, JUSTIFICATION_BITS_LENGTH, MAX_WITHDRAWALS_PER_PAYLOAD, + PENDING_CONSOLIDATIONS_LIMIT, PENDING_DEPOSITS_LIMIT, PENDING_PARTIAL_WITHDRAWALS_LIMIT, + PROPOSER_LOOKAHEAD_LEN, PTC_SIZE, PTC_WINDOW_LEN, SLOTS_PER_HISTORICAL_ROOT, + UNSET_DEPOSIT_REQUESTS_START_INDEX, VALIDATOR_REGISTRY_LIMIT, +}; +use crate::containers::{ + BeaconBlockHeader, Builder, BuilderPendingPayment, BuilderPendingWithdrawal, Checkpoint, + Eth1Data, ExecutionPayloadBid, Fork, HistoricalSummary, PendingConsolidation, PendingDeposit, + PendingPartialWithdrawal, SyncCommittee, Validator, Withdrawal, +}; +use crate::primitives::{ + BuilderIndex, Bytes32, Epoch, Gwei, Hash32, ParticipationFlags, Root, Slot, ValidatorIndex, + WithdrawalIndex, +}; +use crate::ssz::prelude::*; + +/// Complete consensus snapshot needed to validate the next slot or block. +/// +/// Consensus is convergence on this object: validators that apply the same +/// valid slots and blocks should arrive at the same `BeaconState`. +/// The state transition replays this structure forward one block at a time. A +/// fork-choice [`Store`](crate::fork_choice::Store) may cache many post-states, +/// but only the transition mutates a `BeaconState`. When reading a handler, ask +/// whether it writes this object (consensus state) or writes the store (local +/// node view). +/// The default value is the SSZ zero state. It is useful for construction, but +/// it is not a valid initialized chain state on its own. Upgrade routines may +/// set fields such as `execution_payload_availability` to non-zero values. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BeaconState { + /// Unix timestamp the chain started at. + pub genesis_time: u64, + /// Root of the validator set at genesis. Tags this chain in domain separators. + pub genesis_validators_root: Root, + /// Current slot of the state. + pub slot: Slot, + /// Active signing-version pair. + pub fork: Fork, + /// Header of the most recent block applied to this state. + pub latest_block_header: BeaconBlockHeader, + /// Ring buffer of past block roots indexed by `slot % SLOTS_PER_HISTORICAL_ROOT`. + /// + /// Written by slot processing and read by attestation validation to decide + /// whether a vote names the canonical block at a historical slot. + pub block_roots: Vector, + /// Ring buffer of past state roots indexed by `slot % SLOTS_PER_HISTORICAL_ROOT`. + /// + /// Written by slot processing before advancing the clock. + pub state_roots: Vector, + /// Roll-up roots of historical block-root buffers, trimmed for cheap proofs. + pub historical_roots: List, + /// Currently winning deposit-chain vote (`Eth1Data` in the spec). + pub eth1_data: Eth1Data, + /// Deposit-chain votes accumulated over the current voting period. + pub eth1_data_votes: List, + /// Index of the next deposit to be processed from the deposit contract. + pub eth1_deposit_index: u64, + /// The validator registry. + pub validators: List, + /// Per-validator balances before effective-balance rounding. + pub balances: List, + /// Ring buffer of past RANDAO mixes. + pub randao_mixes: Vector, + /// Ring buffer of accumulated slashing totals per epoch. + pub slashings: Vector, + /// Per-validator participation flags from the previous epoch. + pub previous_epoch_participation: List, + /// Per-validator participation flags from the current epoch. + pub current_epoch_participation: List, + /// Rolling Casper finality-justification bitvector. + pub justification_bits: Bitvector, + /// Justified checkpoint from the previous epoch. + pub previous_justified_checkpoint: Checkpoint, + /// Justified checkpoint from the current epoch. + pub current_justified_checkpoint: Checkpoint, + /// Most recent finalized checkpoint. + pub finalized_checkpoint: Checkpoint, + /// Per-validator inactivity-leak scores. + pub inactivity_scores: List, + /// Sync committee active this period. + pub current_sync_committee: SyncCommittee, + /// Sync committee active next period, cached one period in advance. + pub next_sync_committee: SyncCommittee, + /// Hash of the most recently settled execution payload. + /// + /// The parent-payload handoff advances this when a child block proves and + /// applies the parent block's payload effects. The next bid must extend this + /// hash through `ExecutionPayloadBid::parent_block_hash`. + pub latest_block_hash: Hash32, + /// Sequence index of the next withdrawal to emit. + pub next_withdrawal_index: WithdrawalIndex, + /// Sweep cursor over the validator registry for the next withdrawal scan. + pub next_withdrawal_validator_index: ValidatorIndex, + /// Summarized history beyond the live ring buffer. + pub historical_summaries: List, + /// Starting index for processing deposit-contract requests. + pub deposit_requests_start_index: u64, + /// Remaining deposit-balance budget for the current epoch. + pub deposit_balance_to_consume: Gwei, + /// Remaining exit-balance budget for the current epoch. + pub exit_balance_to_consume: Gwei, + /// Earliest epoch a new exit may be scheduled at. + pub earliest_exit_epoch: Epoch, + /// Remaining consolidation-balance budget for the current epoch. + pub consolidation_balance_to_consume: Gwei, + /// Earliest epoch a new consolidation may be scheduled at. + pub earliest_consolidation_epoch: Epoch, + /// Queue of deposits awaiting signature verification and activation. + pub pending_deposits: List, + /// Queue of scheduled partial withdrawals. + pub pending_partial_withdrawals: + List, + /// Queue of scheduled consolidations. + pub pending_consolidations: List, + /// Lookahead proposer assignments for the next few slots. + pub proposer_lookahead: Vector, + /// The builder registry. + pub builders: List, + /// Sweep cursor over the builder registry for the next withdrawal scan. + pub next_withdrawal_builder_index: BuilderIndex, + /// Per-slot bit indicating whether the slot's payload was settled available. + /// + /// Cleared by slot processing for the next slot and set by the child block's + /// parent-payload handoff. Beacon attestations read this bit when deciding + /// whether a historical head vote matches the empty or full branch. + pub execution_payload_availability: Bitvector, + /// Rolling 2-epoch accumulator of builder-payment quorum weights. + /// + /// A bid opens one entry and later-included beacon attestations for that + /// bid's slot add effective balance to its `weight`. If a child block + /// accepts the parent payload, the parent-payload handoff releases the + /// payment unconditionally. Otherwise epoch-boundary aging uses the quorum + /// threshold to decide whether the payment is released or dropped. + pub builder_pending_payments: Vector, + /// Queue of builder payments awaiting the next withdrawal sweep. + pub builder_pending_withdrawals: + List, + /// Most-recently accepted builder bid for the current slot. + /// + /// `process_execution_payload_bid` writes this commitment. The later + /// envelope path must match it, and the child block uses its request root + /// when settling the parent payload. + pub latest_execution_payload_bid: ExecutionPayloadBid, + /// Withdrawals the next payload is expected to include. + /// + /// Withdrawal processing computes this list before bid/envelope validation. + /// Envelope validation rejects a payload whose withdrawals do not match. + pub payload_expected_withdrawals: List, + /// Per-slot payload-timeliness committee assignments over the lookahead window. + /// + /// Payload attestation validation and fork-choice PTC gossip both use this + /// window to translate between committee positions and validator indices. + pub ptc_window: Vector, PTC_WINDOW_LEN>, +} + +/// SSZ zero state, field by field. +/// +/// # Warning +/// This is **not** a valid initialized chain state. It is the all-zero SSZ +/// value, useful only as a starting point for construction (genesis seeding, +/// upgrade routines). Treating it as a live state will produce +/// spec-invalid behavior. Genesis state is built by the spec's initialization +/// routine. Later spec upgrade routines may set fields such as +/// `execution_payload_availability` to non-zero starting values. +impl Default for BeaconState { + fn default() -> Self { + Self { + genesis_time: u64::default(), + genesis_validators_root: Root::default(), + slot: Slot::default(), + fork: Fork::default(), + latest_block_header: BeaconBlockHeader::default(), + block_roots: Vector::default(), + state_roots: Vector::default(), + historical_roots: List::default(), + eth1_data: Eth1Data::default(), + eth1_data_votes: List::default(), + eth1_deposit_index: u64::default(), + validators: List::default(), + balances: List::default(), + randao_mixes: Vector::default(), + slashings: Vector::default(), + previous_epoch_participation: List::default(), + current_epoch_participation: List::default(), + justification_bits: Bitvector::default(), + previous_justified_checkpoint: Checkpoint::default(), + current_justified_checkpoint: Checkpoint::default(), + finalized_checkpoint: Checkpoint::default(), + inactivity_scores: List::default(), + current_sync_committee: SyncCommittee::default(), + next_sync_committee: SyncCommittee::default(), + latest_block_hash: Hash32::default(), + next_withdrawal_index: WithdrawalIndex::default(), + next_withdrawal_validator_index: ValidatorIndex::default(), + historical_summaries: List::default(), + deposit_requests_start_index: UNSET_DEPOSIT_REQUESTS_START_INDEX, + deposit_balance_to_consume: Gwei::default(), + exit_balance_to_consume: Gwei::default(), + earliest_exit_epoch: Epoch::default(), + consolidation_balance_to_consume: Gwei::default(), + earliest_consolidation_epoch: Epoch::default(), + pending_deposits: List::default(), + pending_partial_withdrawals: List::default(), + pending_consolidations: List::default(), + proposer_lookahead: Vector::default(), + builders: List::default(), + next_withdrawal_builder_index: BuilderIndex::default(), + execution_payload_availability: Bitvector::default(), + builder_pending_payments: Vector::default(), + builder_pending_withdrawals: List::default(), + latest_execution_payload_bid: ExecutionPayloadBid::default(), + payload_expected_withdrawals: List::default(), + ptc_window: Vector::default(), + } + } +} + +impl SszSized for BeaconState { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::, PTC_WINDOW_LEN>>(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::, PTC_WINDOW_LEN>>(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for BeaconState { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.genesis_time)?; + encoder.write_field(&self.genesis_validators_root)?; + encoder.write_field(&self.slot)?; + encoder.write_field(&self.fork)?; + encoder.write_field(&self.latest_block_header)?; + encoder.write_field(&self.block_roots)?; + encoder.write_field(&self.state_roots)?; + encoder.write_field(&self.historical_roots)?; + encoder.write_field(&self.eth1_data)?; + encoder.write_field(&self.eth1_data_votes)?; + encoder.write_field(&self.eth1_deposit_index)?; + encoder.write_field(&self.validators)?; + encoder.write_field(&self.balances)?; + encoder.write_field(&self.randao_mixes)?; + encoder.write_field(&self.slashings)?; + encoder.write_field(&self.previous_epoch_participation)?; + encoder.write_field(&self.current_epoch_participation)?; + encoder.write_field(&self.justification_bits)?; + encoder.write_field(&self.previous_justified_checkpoint)?; + encoder.write_field(&self.current_justified_checkpoint)?; + encoder.write_field(&self.finalized_checkpoint)?; + encoder.write_field(&self.inactivity_scores)?; + encoder.write_field(&self.current_sync_committee)?; + encoder.write_field(&self.next_sync_committee)?; + encoder.write_field(&self.latest_block_hash)?; + encoder.write_field(&self.next_withdrawal_index)?; + encoder.write_field(&self.next_withdrawal_validator_index)?; + encoder.write_field(&self.historical_summaries)?; + encoder.write_field(&self.deposit_requests_start_index)?; + encoder.write_field(&self.deposit_balance_to_consume)?; + encoder.write_field(&self.exit_balance_to_consume)?; + encoder.write_field(&self.earliest_exit_epoch)?; + encoder.write_field(&self.consolidation_balance_to_consume)?; + encoder.write_field(&self.earliest_consolidation_epoch)?; + encoder.write_field(&self.pending_deposits)?; + encoder.write_field(&self.pending_partial_withdrawals)?; + encoder.write_field(&self.pending_consolidations)?; + encoder.write_field(&self.proposer_lookahead)?; + encoder.write_field(&self.builders)?; + encoder.write_field(&self.next_withdrawal_builder_index)?; + encoder.write_field(&self.execution_payload_availability)?; + encoder.write_field(&self.builder_pending_payments)?; + encoder.write_field(&self.builder_pending_withdrawals)?; + encoder.write_field(&self.latest_execution_payload_bid)?; + encoder.write_field(&self.payload_expected_withdrawals)?; + encoder.write_field(&self.ptc_window)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for BeaconState { + #[allow(clippy::too_many_lines)] + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::, PTC_WINDOW_LEN>>(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + genesis_time: decoder.deserialize_next::()?, + genesis_validators_root: decoder.deserialize_next::()?, + slot: decoder.deserialize_next::()?, + fork: decoder.deserialize_next::()?, + latest_block_header: decoder.deserialize_next::()?, + block_roots: decoder.deserialize_next::>()?, + state_roots: decoder.deserialize_next::>()?, + historical_roots: decoder.deserialize_next::>()?, + eth1_data: decoder.deserialize_next::()?, + eth1_data_votes: decoder.deserialize_next::>()?, + eth1_deposit_index: decoder.deserialize_next::()?, + validators: decoder.deserialize_next::>()?, + balances: decoder.deserialize_next::>()?, + randao_mixes: decoder.deserialize_next::>()?, + slashings: decoder.deserialize_next::>()?, + previous_epoch_participation: decoder.deserialize_next::>()?, + current_epoch_participation: decoder.deserialize_next::>()?, + justification_bits: decoder.deserialize_next::>()?, + previous_justified_checkpoint: decoder.deserialize_next::()?, + current_justified_checkpoint: decoder.deserialize_next::()?, + finalized_checkpoint: decoder.deserialize_next::()?, + inactivity_scores: decoder.deserialize_next::>()?, + current_sync_committee: decoder.deserialize_next::()?, + next_sync_committee: decoder.deserialize_next::()?, + latest_block_hash: decoder.deserialize_next::()?, + next_withdrawal_index: decoder.deserialize_next::()?, + next_withdrawal_validator_index: decoder.deserialize_next::()?, + historical_summaries: decoder.deserialize_next::>()?, + deposit_requests_start_index: decoder.deserialize_next::()?, + deposit_balance_to_consume: decoder.deserialize_next::()?, + exit_balance_to_consume: decoder.deserialize_next::()?, + earliest_exit_epoch: decoder.deserialize_next::()?, + consolidation_balance_to_consume: decoder.deserialize_next::()?, + earliest_consolidation_epoch: decoder.deserialize_next::()?, + pending_deposits: decoder.deserialize_next::>()?, + pending_partial_withdrawals: decoder.deserialize_next::>()?, + pending_consolidations: decoder.deserialize_next::>()?, + proposer_lookahead: decoder.deserialize_next::>()?, + builders: decoder.deserialize_next::>()?, + next_withdrawal_builder_index: decoder.deserialize_next::()?, + execution_payload_availability: decoder.deserialize_next::>()?, + builder_pending_payments: decoder.deserialize_next::>()?, + builder_pending_withdrawals: decoder.deserialize_next::>()?, + latest_execution_payload_bid: decoder.deserialize_next::()?, + payload_expected_withdrawals: decoder.deserialize_next::>()?, + ptc_window: decoder.deserialize_next::, PTC_WINDOW_LEN>>()?, + }) + } +} + +impl Merkleized for BeaconState { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.genesis_time)?, + Merkleized::hash_tree_root(&self.genesis_validators_root)?, + Merkleized::hash_tree_root(&self.slot)?, + Merkleized::hash_tree_root(&self.fork)?, + Merkleized::hash_tree_root(&self.latest_block_header)?, + Merkleized::hash_tree_root(&self.block_roots)?, + Merkleized::hash_tree_root(&self.state_roots)?, + Merkleized::hash_tree_root(&self.historical_roots)?, + Merkleized::hash_tree_root(&self.eth1_data)?, + Merkleized::hash_tree_root(&self.eth1_data_votes)?, + Merkleized::hash_tree_root(&self.eth1_deposit_index)?, + Merkleized::hash_tree_root(&self.validators)?, + Merkleized::hash_tree_root(&self.balances)?, + Merkleized::hash_tree_root(&self.randao_mixes)?, + Merkleized::hash_tree_root(&self.slashings)?, + Merkleized::hash_tree_root(&self.previous_epoch_participation)?, + Merkleized::hash_tree_root(&self.current_epoch_participation)?, + Merkleized::hash_tree_root(&self.justification_bits)?, + Merkleized::hash_tree_root(&self.previous_justified_checkpoint)?, + Merkleized::hash_tree_root(&self.current_justified_checkpoint)?, + Merkleized::hash_tree_root(&self.finalized_checkpoint)?, + Merkleized::hash_tree_root(&self.inactivity_scores)?, + Merkleized::hash_tree_root(&self.current_sync_committee)?, + Merkleized::hash_tree_root(&self.next_sync_committee)?, + Merkleized::hash_tree_root(&self.latest_block_hash)?, + Merkleized::hash_tree_root(&self.next_withdrawal_index)?, + Merkleized::hash_tree_root(&self.next_withdrawal_validator_index)?, + Merkleized::hash_tree_root(&self.historical_summaries)?, + Merkleized::hash_tree_root(&self.deposit_requests_start_index)?, + Merkleized::hash_tree_root(&self.deposit_balance_to_consume)?, + Merkleized::hash_tree_root(&self.exit_balance_to_consume)?, + Merkleized::hash_tree_root(&self.earliest_exit_epoch)?, + Merkleized::hash_tree_root(&self.consolidation_balance_to_consume)?, + Merkleized::hash_tree_root(&self.earliest_consolidation_epoch)?, + Merkleized::hash_tree_root(&self.pending_deposits)?, + Merkleized::hash_tree_root(&self.pending_partial_withdrawals)?, + Merkleized::hash_tree_root(&self.pending_consolidations)?, + Merkleized::hash_tree_root(&self.proposer_lookahead)?, + Merkleized::hash_tree_root(&self.builders)?, + Merkleized::hash_tree_root(&self.next_withdrawal_builder_index)?, + Merkleized::hash_tree_root(&self.execution_payload_availability)?, + Merkleized::hash_tree_root(&self.builder_pending_payments)?, + Merkleized::hash_tree_root(&self.builder_pending_withdrawals)?, + Merkleized::hash_tree_root(&self.latest_execution_payload_bid)?, + Merkleized::hash_tree_root(&self.payload_expected_withdrawals)?, + Merkleized::hash_tree_root(&self.ptc_window)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for BeaconState { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/containers/sync.rs b/moonglass-core/src/containers/sync.rs new file mode 100644 index 0000000..8b6a298 --- /dev/null +++ b/moonglass-core/src/containers/sync.rs @@ -0,0 +1,521 @@ +//! Sync-committee machinery. + +use crate::ssz::prelude::*; + +use crate::constants::{SYNC_COMMITTEE_SIZE, SYNC_COMMITTEE_SUBNET_COUNT}; +use crate::primitives::{BLSPubkey, BLSSignature, Root, Slot, ValidatorIndex}; + +/// Set of validators rotated in to sign sync updates each sync period. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct SyncCommittee { + /// Member public keys, in committee order. + pub pubkeys: Vector, + /// Sum of `pubkeys`, used for fast aggregate verification. + pub aggregate_pubkey: BLSPubkey, +} + +/// Aggregated sync-committee signature over the previous slot's block root. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct SyncAggregate { + /// Bit per committee member, set to 1 if the member's signature is included. + pub sync_committee_bits: Bitvector, + /// Aggregate signature of the participating committee members. + pub sync_committee_signature: BLSSignature, +} + +/// Single sync-committee message before aggregation. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct SyncCommitteeMessage { + /// Slot the message signs for. + pub slot: Slot, + /// Block root being signed. + pub beacon_block_root: Root, + /// Validator that produced the message. + pub validator_index: ValidatorIndex, + /// Validator signature over the sync message. + pub signature: BLSSignature, +} + +/// Aggregated sync-committee contribution for one subcommittee. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct SyncCommitteeContribution { + /// Slot the contribution signs for. + pub slot: Slot, + /// Block root being signed. + pub beacon_block_root: Root, + /// Sync subcommittee index. + pub subcommittee_index: u64, + /// Participation bits for this subcommittee. + pub aggregation_bits: Bitvector<{ SYNC_COMMITTEE_SIZE / SYNC_COMMITTEE_SUBNET_COUNT }>, + /// Aggregate signature for participating subcommittee members. + pub signature: BLSSignature, +} + +/// Sync contribution plus the aggregator's selection proof. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct ContributionAndProof { + /// Validator that aggregated the contribution. + pub aggregator_index: ValidatorIndex, + /// Aggregated sync contribution. + pub contribution: SyncCommitteeContribution, + /// Signature proving the validator was selected to aggregate. + pub selection_proof: BLSSignature, +} + +/// Signed sync contribution gossip object. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct SignedContributionAndProof { + /// Contribution and selection proof being signed. + pub message: ContributionAndProof, + /// Aggregator signature over `message`. + pub signature: BLSSignature, +} + +/// Selection message signed by sync aggregators. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct SyncAggregatorSelectionData { + /// Slot being aggregated. + pub slot: Slot, + /// Sync subcommittee being aggregated. + pub subcommittee_index: u64, +} + +impl SszSized for SyncCommittee { + fn is_variable_size() -> bool { + let fields = [ + field_layout::>(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::>(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SyncCommittee { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.pubkeys)?; + encoder.write_field(&self.aggregate_pubkey)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SyncCommittee { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::>(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + pubkeys: decoder.deserialize_next::>()?, + aggregate_pubkey: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SyncCommittee { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.pubkeys)?, + Merkleized::hash_tree_root(&self.aggregate_pubkey)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SyncCommittee { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SyncAggregate { + fn is_variable_size() -> bool { + let fields = [ + field_layout::>(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::>(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SyncAggregate { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.sync_committee_bits)?; + encoder.write_field(&self.sync_committee_signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SyncAggregate { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::>(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + sync_committee_bits: decoder.deserialize_next::>()?, + sync_committee_signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SyncAggregate { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.sync_committee_bits)?, + Merkleized::hash_tree_root(&self.sync_committee_signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SyncAggregate { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SyncCommitteeMessage { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SyncCommitteeMessage { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.slot)?; + encoder.write_field(&self.beacon_block_root)?; + encoder.write_field(&self.validator_index)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SyncCommitteeMessage { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + slot: decoder.deserialize_next::()?, + beacon_block_root: decoder.deserialize_next::()?, + validator_index: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SyncCommitteeMessage { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.slot)?, + Merkleized::hash_tree_root(&self.beacon_block_root)?, + Merkleized::hash_tree_root(&self.validator_index)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SyncCommitteeMessage { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SyncCommitteeContribution { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SyncCommitteeContribution { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.slot)?; + encoder.write_field(&self.beacon_block_root)?; + encoder.write_field(&self.subcommittee_index)?; + encoder.write_field(&self.aggregation_bits)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SyncCommitteeContribution { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::>(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + slot: decoder.deserialize_next::()?, + beacon_block_root: decoder.deserialize_next::()?, + subcommittee_index: decoder.deserialize_next::()?, + aggregation_bits: decoder.deserialize_next::>()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SyncCommitteeContribution { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.slot)?, + Merkleized::hash_tree_root(&self.beacon_block_root)?, + Merkleized::hash_tree_root(&self.subcommittee_index)?, + Merkleized::hash_tree_root(&self.aggregation_bits)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SyncCommitteeContribution { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for ContributionAndProof { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for ContributionAndProof { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.aggregator_index)?; + encoder.write_field(&self.contribution)?; + encoder.write_field(&self.selection_proof)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for ContributionAndProof { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + aggregator_index: decoder.deserialize_next::()?, + contribution: decoder.deserialize_next::()?, + selection_proof: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for ContributionAndProof { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.aggregator_index)?, + Merkleized::hash_tree_root(&self.contribution)?, + Merkleized::hash_tree_root(&self.selection_proof)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for ContributionAndProof { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SignedContributionAndProof { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SignedContributionAndProof { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.message)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SignedContributionAndProof { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + message: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SignedContributionAndProof { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.message)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SignedContributionAndProof { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SyncAggregatorSelectionData { + fn is_variable_size() -> bool { + let fields = [field_layout::(), field_layout::()]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [field_layout::(), field_layout::()]; + container_size_hint(&fields) + } +} + +impl Serialize for SyncAggregatorSelectionData { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.slot)?; + encoder.write_field(&self.subcommittee_index)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SyncAggregatorSelectionData { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [field_layout::(), field_layout::()]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + slot: decoder.deserialize_next::()?, + subcommittee_index: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SyncAggregatorSelectionData { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.slot)?, + Merkleized::hash_tree_root(&self.subcommittee_index)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SyncAggregatorSelectionData { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/containers/validator.rs b/moonglass-core/src/containers/validator.rs new file mode 100644 index 0000000..35afb03 --- /dev/null +++ b/moonglass-core/src/containers/validator.rs @@ -0,0 +1,362 @@ +//! Validator registry record and the pending queues that gate registry transitions. + +use crate::primitives::{BLSPubkey, BLSSignature, Bytes32, Epoch, Gwei, Slot, ValidatorIndex}; +use crate::ssz::prelude::*; + +/// A single validator entry in the registry, indexed by [`ValidatorIndex`]. +/// +/// The lifecycle epochs (`activation_eligibility_epoch`, `activation_epoch`, `exit_epoch`, +/// `withdrawable_epoch`) gate when the validator may act and when its balance leaves the +/// consensus layer, and most hold `FAR_FUTURE_EPOCH` until the corresponding transition is +/// scheduled. The `effective_balance` is the quantized stake that drives weight and rewards, +/// not the spendable `balance` tracked separately in [`crate::containers::BeaconState`]. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct Validator { + /// BLS public key used to verify the validator's signatures. + pub pubkey: BLSPubkey, + /// 32-byte credential whose leading byte selects the withdrawal-credential variant. + pub withdrawal_credentials: Bytes32, + /// Quantized balance contributing to weight, voting power, and base-reward math. + pub effective_balance: Gwei, + /// True once the validator has been successfully slashed. + pub slashed: bool, + /// Epoch at which the validator became eligible to enter the activation queue. + pub activation_eligibility_epoch: Epoch, + /// Epoch at which the validator became active. + pub activation_epoch: Epoch, + /// Epoch at which the validator exited (or `FAR_FUTURE_EPOCH` if not exited). + pub exit_epoch: Epoch, + /// Epoch at which the balance becomes withdrawable. + pub withdrawable_epoch: Epoch, +} + +/// Entry in the deferred-deposit queue awaiting signature verification and activation. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct PendingDeposit { + /// Depositing validator's public key. + pub pubkey: BLSPubkey, + /// Withdrawal credential the deposit binds the validator to. + pub withdrawal_credentials: Bytes32, + /// Deposit amount. + pub amount: Gwei, + /// Signature over the deposit message, checked when the deposit is processed. + pub signature: BLSSignature, + /// Slot the deposit was observed in. + pub slot: Slot, +} + +/// Scheduled partial withdrawal of a validator's surplus balance. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct PendingPartialWithdrawal { + /// Validator the withdrawal applies to. + pub validator_index: ValidatorIndex, + /// Amount to withdraw at `withdrawable_epoch`. + pub amount: Gwei, + /// Earliest epoch the withdrawal becomes due. + pub withdrawable_epoch: Epoch, +} + +/// Pending merge of one validator's balance into another's. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct PendingConsolidation { + /// Source validator (balance is moved out of this one). + pub source_index: ValidatorIndex, + /// Target validator (balance is folded into this one). + pub target_index: ValidatorIndex, +} + +impl SszSized for Validator { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for Validator { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.pubkey)?; + encoder.write_field(&self.withdrawal_credentials)?; + encoder.write_field(&self.effective_balance)?; + encoder.write_field(&self.slashed)?; + encoder.write_field(&self.activation_eligibility_epoch)?; + encoder.write_field(&self.activation_epoch)?; + encoder.write_field(&self.exit_epoch)?; + encoder.write_field(&self.withdrawable_epoch)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for Validator { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + pubkey: decoder.deserialize_next::()?, + withdrawal_credentials: decoder.deserialize_next::()?, + effective_balance: decoder.deserialize_next::()?, + slashed: decoder.deserialize_next::()?, + activation_eligibility_epoch: decoder.deserialize_next::()?, + activation_epoch: decoder.deserialize_next::()?, + exit_epoch: decoder.deserialize_next::()?, + withdrawable_epoch: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for Validator { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.pubkey)?, + Merkleized::hash_tree_root(&self.withdrawal_credentials)?, + Merkleized::hash_tree_root(&self.effective_balance)?, + Merkleized::hash_tree_root(&self.slashed)?, + Merkleized::hash_tree_root(&self.activation_eligibility_epoch)?, + Merkleized::hash_tree_root(&self.activation_epoch)?, + Merkleized::hash_tree_root(&self.exit_epoch)?, + Merkleized::hash_tree_root(&self.withdrawable_epoch)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for Validator { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for PendingDeposit { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for PendingDeposit { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.pubkey)?; + encoder.write_field(&self.withdrawal_credentials)?; + encoder.write_field(&self.amount)?; + encoder.write_field(&self.signature)?; + encoder.write_field(&self.slot)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for PendingDeposit { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + pubkey: decoder.deserialize_next::()?, + withdrawal_credentials: decoder.deserialize_next::()?, + amount: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + slot: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for PendingDeposit { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.pubkey)?, + Merkleized::hash_tree_root(&self.withdrawal_credentials)?, + Merkleized::hash_tree_root(&self.amount)?, + Merkleized::hash_tree_root(&self.signature)?, + Merkleized::hash_tree_root(&self.slot)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for PendingDeposit { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for PendingPartialWithdrawal { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for PendingPartialWithdrawal { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.validator_index)?; + encoder.write_field(&self.amount)?; + encoder.write_field(&self.withdrawable_epoch)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for PendingPartialWithdrawal { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + validator_index: decoder.deserialize_next::()?, + amount: decoder.deserialize_next::()?, + withdrawable_epoch: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for PendingPartialWithdrawal { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.validator_index)?, + Merkleized::hash_tree_root(&self.amount)?, + Merkleized::hash_tree_root(&self.withdrawable_epoch)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for PendingPartialWithdrawal { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for PendingConsolidation { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for PendingConsolidation { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.source_index)?; + encoder.write_field(&self.target_index)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for PendingConsolidation { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + source_index: decoder.deserialize_next::()?, + target_index: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for PendingConsolidation { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.source_index)?, + Merkleized::hash_tree_root(&self.target_index)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for PendingConsolidation { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/containers/withdrawal.rs b/moonglass-core/src/containers/withdrawal.rs new file mode 100644 index 0000000..20d9e9b --- /dev/null +++ b/moonglass-core/src/containers/withdrawal.rs @@ -0,0 +1,1120 @@ +//! Validator lifecycle and execution-to-consensus request containers. +//! +//! These containers cover deposits, exits, withdrawals, credential changes, and +//! requests created by the execution layer for the consensus transition to +//! consume. +//! +//! The lifecycle path is: a deposit or deposit request enters a pending queue, +//! the validator becomes active, performs duties, may change withdrawal +//! credentials, may request voluntary exit, waits through churn scheduling, and +//! eventually becomes withdrawable. Consensus-layer withdrawals move balances +//! out. Execution-layer withdrawal requests ask consensus to schedule that +//! movement. +//! +//! Validator balances are consensus-layer accounting. Deposits and withdrawals +//! are controlled movements between that accounting and execution-layer ETH. + +use crate::constants::{ + DEPOSIT_PROOF_LEN, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, + MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, + MAX_DEPOSIT_REQUESTS_PER_PAYLOAD, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, +}; +use crate::primitives::{ + BLSPubkey, BLSSignature, Bytes32, Epoch, ExecutionAddress, Gwei, ValidatorIndex, + WithdrawalIndex, +}; +use crate::ssz::prelude::*; + +/// Atomic balance movement from the consensus layer to an execution-layer address. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct Withdrawal { + /// Monotonic index uniquely identifying this withdrawal across history. + pub index: WithdrawalIndex, + /// Validator whose balance is being withdrawn. + pub validator_index: ValidatorIndex, + /// Execution-layer destination address. + pub address: ExecutionAddress, + /// Withdrawn amount. + pub amount: Gwei, +} + +/// Request to swap a BLS withdrawal credential for an execution address. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct BLSToExecutionChange { + /// Validator initiating the credential change. + pub validator_index: ValidatorIndex, + /// BLS key authorising the change (must match the current withdrawal credential). + pub from_bls_pubkey: BLSPubkey, + /// New execution address for future withdrawals. + pub to_execution_address: ExecutionAddress, +} + +/// Credential-change request plus the BLS signature authorising it. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct SignedBLSToExecutionChange { + /// The credential-change being signed. + pub message: BLSToExecutionChange, + /// Signature over the domain-separated signing root of `message`. + pub signature: BLSSignature, +} + +/// Validator-initiated request to leave the active set. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct VoluntaryExit { + /// Epoch the exit is being signed at. The exit cannot take effect earlier. + pub epoch: Epoch, + /// Validator requesting to exit. + pub validator_index: ValidatorIndex, +} + +/// Voluntary exit plus the validator's signature authorising it. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct SignedVoluntaryExit { + /// The voluntary exit being signed. + pub message: VoluntaryExit, + /// Signature over the domain-separated signing root of `message`. + pub signature: BLSSignature, +} + +/// Unsigned validator deposit message. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct DepositMessage { + /// Depositing validator's public key. + pub pubkey: BLSPubkey, + /// Withdrawal credential the deposit binds the validator to. + pub withdrawal_credentials: Bytes32, + /// Deposit amount. + pub amount: Gwei, +} + +/// Deposit payload as written to the execution-layer deposit contract. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct DepositData { + /// Depositing validator's public key. + pub pubkey: BLSPubkey, + /// Withdrawal credential the deposit binds the validator to. + pub withdrawal_credentials: Bytes32, + /// Deposit amount. + pub amount: Gwei, + /// Signature over the deposit message (the unsigned tuple). + pub signature: BLSSignature, +} + +/// A deposit-contract event packaged with its Merkle inclusion proof. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct Deposit { + /// Merkle proof of inclusion in the deposit-contract tree (depth + 1 nodes). + pub proof: Vector, + /// The deposit payload being proven. + pub data: DepositData, +} + +/// Execution-layer deposit request that supersedes deposit-vote-driven deposits. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct DepositRequest { + /// Depositing validator's public key. + pub pubkey: BLSPubkey, + /// Withdrawal credential the deposit binds the validator to. + pub withdrawal_credentials: Bytes32, + /// Deposit amount. + pub amount: Gwei, + /// Signature over the deposit message. + pub signature: BLSSignature, + /// Sequence index assigned by the deposit contract. + pub index: u64, +} + +/// Execution-layer request to withdraw or fully exit a validator. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct WithdrawalRequest { + /// Execution-layer address authorising the request (must match the credential). + pub source_address: ExecutionAddress, + /// Validator targeted by the request. + pub validator_pubkey: BLSPubkey, + /// Amount to withdraw, or `FULL_EXIT_REQUEST_AMOUNT` for a full exit. + pub amount: Gwei, +} + +/// Execution-layer request to consolidate one validator's balance into another. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct ConsolidationRequest { + /// Execution-layer address authorising the consolidation. + pub source_address: ExecutionAddress, + /// Source validator's public key (balance moved out). + pub source_pubkey: BLSPubkey, + /// Target validator's public key (balance folded in). + pub target_pubkey: BLSPubkey, +} + +/// Execution-layer request to deposit stake for a builder. +/// +/// A builder deposit either registers a new builder or tops up an existing one. +/// Registering a new builder checks the signature under a builder-specific +/// domain, so it cannot be confused with a validator deposit. A top-up to an +/// existing builder skips the signature check. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct BuilderDepositRequest { + /// Builder's public key. + pub pubkey: BLSPubkey, + /// Withdrawal credential the deposit binds the builder to. + pub withdrawal_credentials: Bytes32, + /// Deposit amount. + pub amount: Gwei, + /// Signature over the deposit message under the builder-deposit domain. + pub signature: BLSSignature, +} + +/// Execution-layer request to exit a builder from the registry. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct BuilderExitRequest { + /// Execution-layer address authorising the exit (must match the builder's). + pub source_address: ExecutionAddress, + /// Builder's public key. + pub pubkey: BLSPubkey, +} + +/// All execution-to-consensus requests delivered by a payload, grouped by kind. +/// +/// The delivered envelope carries these requests with the payload. The child +/// block carries the same requests in +/// [`crate::containers::BeaconBlockBody::parent_execution_requests`], where +/// [`BeaconState::process_parent_execution_payload`](crate::containers::BeaconState::process_parent_execution_payload) checks their root against +/// the accepted parent bid before dispatching deposit, withdrawal, consolidation, +/// and builder handlers. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct ExecutionRequests { + /// Execution-layer deposit requests. + pub deposits: List, + /// Execution-layer partial-withdrawal and full-exit requests. + pub withdrawals: List, + /// Execution-layer consolidation requests. + pub consolidations: List, + /// Execution-layer builder deposit requests. + pub builder_deposits: List, + /// Execution-layer builder exit requests. + pub builder_exits: List, +} + +impl ExecutionRequests { + /// True when the payload carried no execution-to-consensus requests. + pub fn is_empty(&self) -> bool { + self.deposits.is_empty() + && self.withdrawals.is_empty() + && self.consolidations.is_empty() + && self.builder_deposits.is_empty() + && self.builder_exits.is_empty() + } +} + +impl SszSized for Withdrawal { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for Withdrawal { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.index)?; + encoder.write_field(&self.validator_index)?; + encoder.write_field(&self.address)?; + encoder.write_field(&self.amount)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for Withdrawal { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + index: decoder.deserialize_next::()?, + validator_index: decoder.deserialize_next::()?, + address: decoder.deserialize_next::()?, + amount: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for Withdrawal { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.index)?, + Merkleized::hash_tree_root(&self.validator_index)?, + Merkleized::hash_tree_root(&self.address)?, + Merkleized::hash_tree_root(&self.amount)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for Withdrawal { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for BLSToExecutionChange { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for BLSToExecutionChange { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.validator_index)?; + encoder.write_field(&self.from_bls_pubkey)?; + encoder.write_field(&self.to_execution_address)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for BLSToExecutionChange { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + validator_index: decoder.deserialize_next::()?, + from_bls_pubkey: decoder.deserialize_next::()?, + to_execution_address: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for BLSToExecutionChange { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.validator_index)?, + Merkleized::hash_tree_root(&self.from_bls_pubkey)?, + Merkleized::hash_tree_root(&self.to_execution_address)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for BLSToExecutionChange { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SignedBLSToExecutionChange { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SignedBLSToExecutionChange { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.message)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SignedBLSToExecutionChange { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + message: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SignedBLSToExecutionChange { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.message)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SignedBLSToExecutionChange { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for VoluntaryExit { + fn is_variable_size() -> bool { + let fields = [field_layout::(), field_layout::()]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [field_layout::(), field_layout::()]; + container_size_hint(&fields) + } +} + +impl Serialize for VoluntaryExit { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.epoch)?; + encoder.write_field(&self.validator_index)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for VoluntaryExit { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [field_layout::(), field_layout::()]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + epoch: decoder.deserialize_next::()?, + validator_index: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for VoluntaryExit { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.epoch)?, + Merkleized::hash_tree_root(&self.validator_index)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for VoluntaryExit { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for SignedVoluntaryExit { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for SignedVoluntaryExit { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.message)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for SignedVoluntaryExit { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + message: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SignedVoluntaryExit { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.message)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for SignedVoluntaryExit { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for DepositMessage { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for DepositMessage { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.pubkey)?; + encoder.write_field(&self.withdrawal_credentials)?; + encoder.write_field(&self.amount)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for DepositMessage { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + pubkey: decoder.deserialize_next::()?, + withdrawal_credentials: decoder.deserialize_next::()?, + amount: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for DepositMessage { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.pubkey)?, + Merkleized::hash_tree_root(&self.withdrawal_credentials)?, + Merkleized::hash_tree_root(&self.amount)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for DepositMessage { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for DepositData { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for DepositData { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.pubkey)?; + encoder.write_field(&self.withdrawal_credentials)?; + encoder.write_field(&self.amount)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for DepositData { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + pubkey: decoder.deserialize_next::()?, + withdrawal_credentials: decoder.deserialize_next::()?, + amount: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for DepositData { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.pubkey)?, + Merkleized::hash_tree_root(&self.withdrawal_credentials)?, + Merkleized::hash_tree_root(&self.amount)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for DepositData { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for Deposit { + fn is_variable_size() -> bool { + let fields = [ + field_layout::>(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::>(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for Deposit { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.proof)?; + encoder.write_field(&self.data)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for Deposit { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::>(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + proof: decoder.deserialize_next::>()?, + data: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for Deposit { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.proof)?, + Merkleized::hash_tree_root(&self.data)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for Deposit { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for DepositRequest { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for DepositRequest { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.pubkey)?; + encoder.write_field(&self.withdrawal_credentials)?; + encoder.write_field(&self.amount)?; + encoder.write_field(&self.signature)?; + encoder.write_field(&self.index)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for DepositRequest { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + pubkey: decoder.deserialize_next::()?, + withdrawal_credentials: decoder.deserialize_next::()?, + amount: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + index: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for DepositRequest { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.pubkey)?, + Merkleized::hash_tree_root(&self.withdrawal_credentials)?, + Merkleized::hash_tree_root(&self.amount)?, + Merkleized::hash_tree_root(&self.signature)?, + Merkleized::hash_tree_root(&self.index)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for DepositRequest { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for WithdrawalRequest { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for WithdrawalRequest { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.source_address)?; + encoder.write_field(&self.validator_pubkey)?; + encoder.write_field(&self.amount)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for WithdrawalRequest { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + source_address: decoder.deserialize_next::()?, + validator_pubkey: decoder.deserialize_next::()?, + amount: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for WithdrawalRequest { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.source_address)?, + Merkleized::hash_tree_root(&self.validator_pubkey)?, + Merkleized::hash_tree_root(&self.amount)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for WithdrawalRequest { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for ConsolidationRequest { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for ConsolidationRequest { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.source_address)?; + encoder.write_field(&self.source_pubkey)?; + encoder.write_field(&self.target_pubkey)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for ConsolidationRequest { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + source_address: decoder.deserialize_next::()?, + source_pubkey: decoder.deserialize_next::()?, + target_pubkey: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for ConsolidationRequest { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.source_address)?, + Merkleized::hash_tree_root(&self.source_pubkey)?, + Merkleized::hash_tree_root(&self.target_pubkey)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for ConsolidationRequest { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for BuilderDepositRequest { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for BuilderDepositRequest { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.pubkey)?; + encoder.write_field(&self.withdrawal_credentials)?; + encoder.write_field(&self.amount)?; + encoder.write_field(&self.signature)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for BuilderDepositRequest { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + pubkey: decoder.deserialize_next::()?, + withdrawal_credentials: decoder.deserialize_next::()?, + amount: decoder.deserialize_next::()?, + signature: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for BuilderDepositRequest { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.pubkey)?, + Merkleized::hash_tree_root(&self.withdrawal_credentials)?, + Merkleized::hash_tree_root(&self.amount)?, + Merkleized::hash_tree_root(&self.signature)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for BuilderDepositRequest { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for BuilderExitRequest { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for BuilderExitRequest { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.source_address)?; + encoder.write_field(&self.pubkey)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for BuilderExitRequest { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + source_address: decoder.deserialize_next::()?, + pubkey: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for BuilderExitRequest { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.source_address)?, + Merkleized::hash_tree_root(&self.pubkey)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for BuilderExitRequest { + fn is_composite_type() -> bool { + true + } +} + +impl SszSized for ExecutionRequests { + fn is_variable_size() -> bool { + let fields = [ + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for ExecutionRequests { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.deposits)?; + encoder.write_field(&self.withdrawals)?; + encoder.write_field(&self.consolidations)?; + encoder.write_field(&self.builder_deposits)?; + encoder.write_field(&self.builder_exits)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for ExecutionRequests { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + deposits: decoder.deserialize_next::>()?, + withdrawals: decoder.deserialize_next::>()?, + consolidations: decoder.deserialize_next::>()?, + builder_deposits: decoder.deserialize_next::>()?, + builder_exits: decoder.deserialize_next::>()?, + }) + } +} + +impl Merkleized for ExecutionRequests { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.deposits)?, + Merkleized::hash_tree_root(&self.withdrawals)?, + Merkleized::hash_tree_root(&self.consolidations)?, + Merkleized::hash_tree_root(&self.builder_deposits)?, + Merkleized::hash_tree_root(&self.builder_exits)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for ExecutionRequests { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass/src/crypto.rs b/moonglass-core/src/crypto.rs similarity index 100% rename from moonglass/src/crypto.rs rename to moonglass-core/src/crypto.rs diff --git a/moonglass/src/crypto/bls.rs b/moonglass-core/src/crypto/bls.rs similarity index 85% rename from moonglass/src/crypto/bls.rs rename to moonglass-core/src/crypto/bls.rs index 32ac195..07e733d 100644 --- a/moonglass/src/crypto/bls.rs +++ b/moonglass-core/src/crypto/bls.rs @@ -25,15 +25,15 @@ use sha2::Sha256; use crate::constants::BLS_DST; use crate::error::{SignatureError, TransitionError}; -use crate::primitives::{BLSPubkey, BLSSignature, Root}; +use crate::primitives::{BLS_G1_COMPRESSED_BYTES, BLSPubkey, BLSSignature, Root}; /// Ethereum's hash-to-G2 implementation over BLS12-381. -type EthG2Hasher = +pub type EthG2Hasher = MapToCurveBasedHasher, WBMap>; /// Compressed G1 encoding of the point at infinity, rejected for public keys. -const G1_POINT_AT_INFINITY: [u8; 48] = { - let mut bytes = [0u8; 48]; +pub const G1_POINT_AT_INFINITY: [u8; BLS_G1_COMPRESSED_BYTES] = { + let mut bytes = [0u8; BLS_G1_COMPRESSED_BYTES]; bytes[0] = 0xC0; bytes }; @@ -65,7 +65,7 @@ pub fn aggregate_pubkeys(pubkeys: &[BLSPubkey]) -> Result Result { +pub fn parse_pubkey( + pubkey: &BLSPubkey, + on_fail: SignatureError, +) -> Result { if pubkey.0 == G1_POINT_AT_INFINITY { return Err(on_fail.into()); } - let pk = G1Affine::deserialize_compressed(&pubkey.0[..]).map_err(|_| on_fail)?; - if pk.is_zero() { - return Err(on_fail.into()); - } - Ok(pk) + G1Affine::deserialize_compressed(&pubkey.0[..]).map_err(|_| on_fail.into()) } /// Parse a compressed G2 signature. -fn parse_signature( +pub fn parse_signature( signature: &BLSSignature, on_fail: SignatureError, ) -> Result { @@ -123,13 +122,16 @@ fn parse_signature( } /// Hash a signing root to the BLS12-381 G2 curve using Ethereum's DST. -fn hash_to_g2(signing_root: Root, on_fail: SignatureError) -> Result { +pub fn hash_to_g2( + signing_root: Root, + on_fail: SignatureError, +) -> Result { let hasher = EthG2Hasher::new(BLS_DST).map_err(|_| on_fail)?; hasher.hash(&signing_root.0).map_err(|_| on_fail.into()) } /// Sum compressed public keys into a projective G1 aggregate. -fn aggregate_pubkey( +pub fn aggregate_pubkey_point( pubkeys: &[BLSPubkey], on_fail: SignatureError, ) -> Result { @@ -142,7 +144,7 @@ fn aggregate_pubkey( } /// Check the BLS pairing equation for one public key, message hash, and signature. -fn verify_pairing( +pub fn verify_pairing( pubkey: G1Affine, message_hash: G2Affine, signature: G2Affine, diff --git a/moonglass-core/src/crypto/kzg.rs b/moonglass-core/src/crypto/kzg.rs new file mode 100644 index 0000000..83ef5bb --- /dev/null +++ b/moonglass-core/src/crypto/kzg.rs @@ -0,0 +1,29 @@ +//! KZG polynomial commitments over arkworks pairings. +//! +//! The production entry point is the cell-proof / data-availability code in +//! [`cells`], which consensus drives to compute, recover, and batch-verify the +//! cells and proofs of an extended blob (`compute_cells_and_kzg_proofs`, +//! `recover_cells_and_kzg_proofs`, `verify_cell_kzg_proof_batch`). +//! +//! [`opening`] holds the generic single-point primitive. The implementation +//! keeps the equations visible where the terms are built. +//! Commitments use `C = [p(tau)]_1 = sum_i p_i [tau^i]_1`. +//! Openings use `q(X) = (p(X)-p(z)) / (X-z)` and `pi = [q(tau)]_1`. +//! Verification checks `e(C-[p(z)]_1, [1]_2) = e(pi, [tau-z]_2)`. +//! [`fk`] computes all openings at the roots of unity in one batch. + +pub mod cells; +pub mod error; +pub mod fk; +pub mod opening; +pub mod setup; +pub mod trusted_setup; + +pub use cells::*; +pub use error::{KzgError, SetupFileError}; +pub use fk::open_fk; +pub use opening::{commit, open, verify}; +pub use setup::{EthereumKzgSetup, KzgSetup}; +pub use trusted_setup::{ + EthereumTrustedSetup, PowersOfTau, get_powers_from_bytes, get_powers_from_text, +}; diff --git a/moonglass-core/src/crypto/kzg/cells.rs b/moonglass-core/src/crypto/kzg/cells.rs new file mode 100644 index 0000000..3254268 --- /dev/null +++ b/moonglass-core/src/crypto/kzg/cells.rs @@ -0,0 +1,957 @@ +//! Cell computation, recovery, and batch proof verification for +//! data-availability sampling. + +use std::sync::LazyLock; + +use ark_bls12_381::{Bls12_381, Fr, G1Affine, G1Projective, G2Affine}; +use ark_ec::{AffineRepr, CurveGroup, VariableBaseMSM, pairing::Pairing}; +use ark_ff::{BigInteger, FftField, Field, One, PrimeField, Zero}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use sha2::{Digest, Sha256}; + +use crate::constants::{ + BYTES_PER_BLOB, BYTES_PER_CELL, BYTES_PER_FIELD_ELEMENT, CELLS_PER_EXT_BLOB, + FIELD_ELEMENTS_PER_BLOB, FIELD_ELEMENTS_PER_CELL, FIELD_ELEMENTS_PER_EXT_BLOB, + PRIMITIVE_ROOT_OF_UNITY, RANDOM_CHALLENGE_KZG_CELL_BATCH_DOMAIN, +}; +use crate::primitives::{ + Cell, CellIndex, CommitmentIndex, KZG_COMMITMENT_BYTES, KZG_PROOF_BYTES, KZGCommitment, + KZGProof, +}; + +use super::{EthereumKzgSetup, KzgError}; + +/// BLS12-381 scalar field element used by KZG. +pub type BLSFieldElement = Fr; + +/// Polynomial coefficients in monomial order. +pub type PolynomialCoeff = Vec; + +/// Evaluation domain for one cell. +pub type Coset = Vec; + +/// Evaluations over one cell coset. +pub type CosetEvals = Vec; + +/// Parsed inputs for the cell KZG batch verifier. +#[derive(Clone)] +pub struct ParsedCellKzgProofBatch { + /// Deduplicated commitment bytes. + pub commitments_bytes: Vec, + /// Deduplicated commitment points. + pub commitments: Vec, + /// Indices into the deduplicated commitment list. + pub commitment_indices: Vec, + /// Cell indices for every proof tuple. + pub cell_indices: Vec, + /// Parsed cell evaluations. + pub cosets_evals: Vec, + /// Serialized proof bytes. + pub proofs_bytes: Vec, + /// Parsed proof points. + pub proofs: Vec, +} + +/// Compressed G1 point at infinity. +pub const G1_POINT_AT_INFINITY: [u8; KZG_COMMITMENT_BYTES] = { + let mut bytes = [0; KZG_COMMITMENT_BYTES]; + bytes[0] = 0xC0; + bytes +}; + +/// BLS12-381 scalar-field modulus as big-endian bytes. +pub const BLS_MODULUS_BYTES: [u8; BYTES_PER_FIELD_ELEMENT] = [ + 0x73, 0xed, 0xa7, 0x53, 0x29, 0x9d, 0x7d, 0x48, 0x33, 0x39, 0xd8, 0x08, 0x09, 0xa1, 0xd8, 0x05, + 0x53, 0xbd, 0xa4, 0x02, 0xff, 0xfe, 0x5b, 0xfe, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, +]; + +/// Hash bytes into the scalar field using the spec's challenge conversion. +pub fn hash_to_bls_field(data: &[u8]) -> BLSFieldElement { + let hashed_data = Sha256::digest(data); + Fr::from_be_bytes_mod_order(&hashed_data) +} + +/// Convert trusted scalar bytes into a field element. +pub fn bytes_to_bls_field( + bytes: [u8; BYTES_PER_FIELD_ELEMENT], +) -> Result { + if bytes >= BLS_MODULUS_BYTES { + return Err(KzgError::InvalidFieldElement); + } + Ok(Fr::from_be_bytes_mod_order(&bytes)) +} + +/// Convert a field element into canonical big-endian bytes. +pub fn bls_field_to_bytes(value: BLSFieldElement) -> [u8; BYTES_PER_FIELD_ELEMENT] { + let bytes = value.into_bigint().to_bytes_be(); + let mut out = [0u8; BYTES_PER_FIELD_ELEMENT]; + out[BYTES_PER_FIELD_ELEMENT - bytes.len()..].copy_from_slice(&bytes); + out +} + +/// Validate compressed KZG G1 bytes and return the affine point. +pub fn validate_kzg_g1(bytes: &[u8; KZG_COMMITMENT_BYTES]) -> Result { + if bytes == &G1_POINT_AT_INFINITY { + return Ok(G1Affine::zero()); + } + G1Affine::deserialize_compressed(&bytes[..]).map_err(|_| KzgError::InvalidG1) +} + +/// Convert commitment bytes into a validated G1 point. +pub fn bytes_to_kzg_commitment(commitment: KZGCommitment) -> Result { + Ok(validate_kzg_g1(&commitment.0)?.into_group()) +} + +/// Convert proof bytes into a validated G1 point. +pub fn bytes_to_kzg_proof(proof: KZGProof) -> Result { + Ok(validate_kzg_g1(&proof.0)?.into_group()) +} + +/// Convert a G1 proof point into compressed KZG proof bytes. +pub fn g1_to_kzg_proof(proof: G1Projective) -> Result { + let affine = proof.into_affine(); + let mut bytes = Vec::with_capacity(KZG_PROOF_BYTES); + affine + .serialize_compressed(&mut bytes) + .map_err(|_| KzgError::InvalidG1)?; + let bytes = bytes.try_into().map_err(|_: Vec| KzgError::InvalidG1)?; + Ok(KZGProof(bytes)) +} + +/// Convert blob bytes into field evaluations. +pub fn blob_to_polynomial(blob: &[u8]) -> Result { + if blob.len() != BYTES_PER_BLOB { + return Err(KzgError::InvalidBlobLength { + expected: BYTES_PER_BLOB, + got: blob.len(), + }); + } + let mut polynomial = Vec::with_capacity(FIELD_ELEMENTS_PER_BLOB); + for chunk in blob.chunks_exact(BYTES_PER_FIELD_ELEMENT) { + let mut bytes = [0u8; BYTES_PER_FIELD_ELEMENT]; + bytes.copy_from_slice(chunk); + polynomial.push(bytes_to_bls_field(bytes)?); + } + Ok(polynomial) +} + +/// Convert a cell into field evaluations over its coset. +pub fn cell_to_coset_evals(cell: Cell) -> Result { + let mut evals = Vec::with_capacity(FIELD_ELEMENTS_PER_CELL); + for chunk in cell.0.chunks_exact(BYTES_PER_FIELD_ELEMENT) { + let mut bytes = [0u8; BYTES_PER_FIELD_ELEMENT]; + bytes.copy_from_slice(chunk); + evals.push(bytes_to_bls_field(bytes)?); + } + Ok(evals) +} + +/// Convert field evaluations back into a serialized cell. +pub fn coset_evals_to_cell(coset_evals: &[BLSFieldElement]) -> Result { + if coset_evals.len() != FIELD_ELEMENTS_PER_CELL { + return Err(KzgError::InvalidCosetLength { + expected: FIELD_ELEMENTS_PER_CELL, + got: coset_evals.len(), + }); + } + let mut bytes = [0u8; BYTES_PER_CELL]; + for (i, value) in coset_evals.iter().copied().enumerate() { + let start = i * BYTES_PER_FIELD_ELEMENT; + bytes[start..start + BYTES_PER_FIELD_ELEMENT].copy_from_slice(&bls_field_to_bytes(value)); + } + Ok(Cell(bytes)) +} + +/// Return `x` to the powers `0..n`. +pub fn compute_powers(x: BLSFieldElement, n: usize) -> Vec { + let mut current_power = Fr::one(); + let mut powers = Vec::with_capacity(n); + for _ in 0..n { + powers.push(current_power); + current_power *= x; + } + powers +} + +/// Return roots of unity for the requested order. +pub fn compute_roots_of_unity(order: usize) -> Result, KzgError> { + if order == 0 || !order.is_power_of_two() { + return Err(KzgError::UnsupportedDomainSize(order)); + } + let root_of_unity = + Fr::get_root_of_unity(order as u64).ok_or(KzgError::UnsupportedDomainSize(order))?; + Ok(compute_powers(root_of_unity, order)) +} + +/// Reverse the low bits needed to index a sequence of `order` items. +pub fn reverse_bits(value: usize, order: usize) -> usize { + let bit_count = order.trailing_zeros(); + let mut out = 0usize; + for i in 0..bit_count { + out = (out << 1) | ((value >> i) & 1); + } + out +} + +/// Return a bit-reversal permutation of `values`. +pub fn bit_reversal_permutation(values: &[T]) -> Vec { + let mut pairs = values + .iter() + .cloned() + .enumerate() + .map(|(index, value)| (reverse_bits(index, values.len()), value)) + .collect::>(); + pairs.sort_by_key(|(index, _)| *index); + pairs.into_iter().map(|(_, value)| value).collect() +} + +/// Compute an FFT or inverse FFT over the supplied root domain. +pub fn fft_field( + vals: &[BLSFieldElement], + roots_of_unity: &[BLSFieldElement], + inv: bool, +) -> Result, KzgError> { + if vals.len() != roots_of_unity.len() { + return Err(KzgError::LengthMismatch { + context: "FFT domain", + expected: vals.len(), + got: roots_of_unity.len(), + }); + } + if vals.is_empty() || !vals.len().is_power_of_two() { + return Err(KzgError::UnsupportedDomainSize(vals.len())); + } + if inv { + let mut inverse_roots = Vec::with_capacity(roots_of_unity.len()); + inverse_roots.push(roots_of_unity[0]); + inverse_roots.extend(roots_of_unity[1..].iter().rev().copied()); + let inv_len = BLSFieldElement::from(vals.len() as u64) + .inverse() + .ok_or(KzgError::DivisionByZero)?; + let mut out = fft_field_impl(vals, &inverse_roots); + for value in &mut out { + *value *= inv_len; + } + Ok(out) + } else { + Ok(fft_field_impl(vals, roots_of_unity)) + } +} + +/// Recursive radix-2 FFT over field values. +pub fn fft_field_impl( + vals: &[BLSFieldElement], + roots_of_unity: &[BLSFieldElement], +) -> Vec { + if vals.len() == 1 { + return vals.to_vec(); + } + let even_vals = vals.iter().copied().step_by(2).collect::>(); + let odd_vals = vals.iter().copied().skip(1).step_by(2).collect::>(); + let even_roots = roots_of_unity + .iter() + .copied() + .step_by(2) + .collect::>(); + let left = fft_field_impl(&even_vals, &even_roots); + let right = fft_field_impl(&odd_vals, &even_roots); + let mut out = vec![Fr::zero(); vals.len()]; + for (i, (x, y)) in left.iter().copied().zip(right.iter().copied()).enumerate() { + let y_times_root = y * roots_of_unity[i]; + out[i] = x + y_times_root; + out[i + left.len()] = x - y_times_root; + } + out +} + +/// Compute an FFT or inverse FFT over a coset of the supplied root domain. +pub fn coset_fft_field( + vals: &[BLSFieldElement], + roots_of_unity: &[BLSFieldElement], + inv: bool, +) -> Result, KzgError> { + let shift_factor = BLSFieldElement::from(PRIMITIVE_ROOT_OF_UNITY); + if inv { + let vals = fft_field(vals, roots_of_unity, true)?; + shift_vals( + &vals, + shift_factor.inverse().ok_or(KzgError::DivisionByZero)?, + ) + } else { + let shifted = shift_vals(vals, shift_factor)?; + fft_field(&shifted, roots_of_unity, false) + } +} + +/// Multiply each value by succeeding powers of `factor`. +pub fn shift_vals( + vals: &[BLSFieldElement], + factor: BLSFieldElement, +) -> Result, KzgError> { + let mut shifted = Vec::with_capacity(vals.len()); + let mut shift = Fr::one(); + for value in vals { + shifted.push(*value * shift); + shift *= factor; + } + Ok(shifted) +} + +/// Bit-reversed roots of unity over the extended blob domain, derived once. The +/// per-cell coset helpers index into this rather than rebuilding the +/// [`FIELD_ELEMENTS_PER_EXT_BLOB`]-element table on every call. The build is +/// fallible because the domain root must exist, so a failure is cached and +/// surfaced to callers as an error. +static EXT_ROOTS_OF_UNITY_BRP: LazyLock, KzgError>> = + LazyLock::new(|| { + compute_roots_of_unity(FIELD_ELEMENTS_PER_EXT_BLOB) + .map(|roots| bit_reversal_permutation(&roots)) + }); + +/// Get the coset shift for a cell index. +pub fn coset_shift_for_cell(cell_index: CellIndex) -> Result { + if cell_index.as_usize() >= CELLS_PER_EXT_BLOB { + return Err(KzgError::CellIndexOutOfRange { + index: cell_index.as_u64(), + limit: CELLS_PER_EXT_BLOB, + }); + } + let roots_of_unity_brp = EXT_ROOTS_OF_UNITY_BRP.as_ref().map_err(|err| *err)?; + Ok(roots_of_unity_brp[FIELD_ELEMENTS_PER_CELL * cell_index.as_usize()]) +} + +/// Get the full coset for a cell index. +pub fn coset_for_cell(cell_index: CellIndex) -> Result { + if cell_index.as_usize() >= CELLS_PER_EXT_BLOB { + return Err(KzgError::CellIndexOutOfRange { + index: cell_index.as_u64(), + limit: CELLS_PER_EXT_BLOB, + }); + } + let roots_of_unity_brp = EXT_ROOTS_OF_UNITY_BRP.as_ref().map_err(|err| *err)?; + let start = FIELD_ELEMENTS_PER_CELL * cell_index.as_usize(); + Ok(roots_of_unity_brp[start..start + FIELD_ELEMENTS_PER_CELL].to_vec()) +} + +/// Add two coefficient-form polynomials. +pub fn add_polynomialcoeff(a: &[BLSFieldElement], b: &[BLSFieldElement]) -> PolynomialCoeff { + let len = a.len().max(b.len()); + let mut out = vec![Fr::zero(); len]; + for (i, value) in a.iter().copied().enumerate() { + out[i] += value; + } + for (i, value) in b.iter().copied().enumerate() { + out[i] += value; + } + out +} + +/// Multiply two coefficient-form polynomials. +pub fn multiply_polynomialcoeff(a: &[BLSFieldElement], b: &[BLSFieldElement]) -> PolynomialCoeff { + if a.is_empty() || b.is_empty() { + return Vec::new(); + } + let mut out = vec![Fr::zero(); a.len() + b.len() - 1]; + for (i, lhs) in a.iter().copied().enumerate() { + for (j, rhs) in b.iter().copied().enumerate() { + out[i + j] += lhs * rhs; + } + } + out +} + +/// Divide coefficient-form polynomial `a` by `b`. +pub fn divide_polynomialcoeff( + a: &[BLSFieldElement], + b: &[BLSFieldElement], +) -> Result { + if b.is_empty() { + return Err(KzgError::EmptyDivisor); + } + let Some(leading_inverse) = b[b.len() - 1].inverse() else { + return Err(KzgError::DivisionByZero); + }; + if a.len() < b.len() { + return Ok(Vec::new()); + } + let mut remainder = a.to_vec(); + let mut quotient = vec![Fr::zero(); a.len() - b.len() + 1]; + for quotient_index in (0..quotient.len()).rev() { + let remainder_index = quotient_index + b.len() - 1; + let quotient_value = remainder[remainder_index] * leading_inverse; + quotient[quotient_index] = quotient_value; + for (divisor_index, divisor_value) in b.iter().copied().enumerate() { + remainder[quotient_index + divisor_index] -= divisor_value * quotient_value; + } + } + Ok(quotient) +} + +/// Compute a coefficient-form vanishing polynomial over `xs`. +pub fn vanishing_polynomialcoeff(xs: &[BLSFieldElement]) -> PolynomialCoeff { + let mut out = vec![Fr::one()]; + for x in xs { + out = multiply_polynomialcoeff(&out, &[-*x, Fr::one()]); + } + out +} + +/// Evaluate a coefficient-form polynomial at `z`. +pub fn evaluate_polynomialcoeff( + polynomial_coeff: &[BLSFieldElement], + z: BLSFieldElement, +) -> BLSFieldElement { + let mut y = Fr::zero(); + for coefficient in polynomial_coeff.iter().rev().copied() { + y = y * z + coefficient; + } + y +} + +/// Convert blob evaluation form into polynomial coefficient form. +pub fn polynomial_eval_to_coeff( + polynomial: &[BLSFieldElement], +) -> Result { + if polynomial.len() != FIELD_ELEMENTS_PER_BLOB { + return Err(KzgError::LengthMismatch { + context: "blob polynomial", + expected: FIELD_ELEMENTS_PER_BLOB, + got: polynomial.len(), + }); + } + let roots_of_unity = compute_roots_of_unity(FIELD_ELEMENTS_PER_BLOB)?; + fft_field(&bit_reversal_permutation(polynomial), &roots_of_unity, true) +} + +/// Interpolate a polynomial in coefficient form from points and evaluations. +pub fn interpolate_polynomialcoeff( + xs: &[BLSFieldElement], + ys: &[BLSFieldElement], +) -> Result { + if xs.len() != ys.len() { + return Err(KzgError::LengthMismatch { + context: "interpolation", + expected: xs.len(), + got: ys.len(), + }); + } + let mut out = vec![Fr::zero(); xs.len()]; + for (i, x_i) in xs.iter().copied().enumerate() { + let mut basis = vec![Fr::one()]; + let mut denominator = Fr::one(); + for (j, x_j) in xs.iter().copied().enumerate() { + if i == j { + continue; + } + basis = multiply_polynomialcoeff(&basis, &[-x_j, Fr::one()]); + denominator *= x_i - x_j; + } + let denominator_inverse = denominator + .inverse() + .ok_or(KzgError::DuplicateInterpolationPoint)?; + let scale = ys[i] * denominator_inverse; + for (index, coefficient) in basis.iter().copied().enumerate() { + out[index] += coefficient * scale; + } + } + Ok(out) +} + +/// Linearly combine G1 points with scalar weights. +pub fn g1_lincomb( + points: &[G1Projective], + scalars: &[BLSFieldElement], +) -> Result { + if points.len() != scalars.len() { + return Err(KzgError::LengthMismatch { + context: "G1 linear combination", + expected: points.len(), + got: scalars.len(), + }); + } + if points.is_empty() { + return Ok(G1Projective::zero()); + } + let affine_points = G1Projective::normalize_batch(points); + Ok(G1Projective::msm_unchecked(&affine_points, scalars)) +} + +/// Linearly combine setup G1 affine powers with scalar weights. +pub fn setup_g1_lincomb( + setup_g1_affine_powers: &[G1Affine], + scalars: &[BLSFieldElement], +) -> Result { + if setup_g1_affine_powers.len() < scalars.len() { + return Err(KzgError::PolynomialTooLarge { + coefficients: scalars.len(), + setup_powers: setup_g1_affine_powers.len(), + }); + } + Ok(G1Projective::msm_unchecked( + &setup_g1_affine_powers[..scalars.len()], + scalars, + )) +} + +/// Compute a KZG multi-proof and the evaluations for one coset. +pub fn compute_kzg_proof_multi_impl( + setup: &EthereumKzgSetup, + polynomial_coeff: &[BLSFieldElement], + zs: &[BLSFieldElement], +) -> Result<(KZGProof, CosetEvals), KzgError> { + let ys = zs + .iter() + .copied() + .map(|z| evaluate_polynomialcoeff(polynomial_coeff, z)) + .collect::>(); + let denominator_poly = vanishing_polynomialcoeff(zs); + let quotient_polynomial = divide_polynomialcoeff(polynomial_coeff, &denominator_poly)?; + let proof = setup_g1_lincomb(setup.g1_affine_powers(), "ient_polynomial)?; + Ok((g1_to_kzg_proof(proof)?, ys)) +} + +/// Given a blob, extend it and return all cells of the extended blob. +pub fn compute_cells(blob: &[u8]) -> Result, KzgError> { + let polynomial = blob_to_polynomial(blob)?; + let polynomial_coeff = polynomial_eval_to_coeff(&polynomial)?; + compute_cells_polynomialcoeff(&polynomial_coeff) +} + +/// Compute all cells from a coefficient-form polynomial. +pub fn compute_cells_polynomialcoeff( + polynomial_coeff: &[BLSFieldElement], +) -> Result, KzgError> { + let mut cells = Vec::with_capacity(CELLS_PER_EXT_BLOB); + for i in 0..CELLS_PER_EXT_BLOB { + let coset = coset_for_cell(CellIndex::new(i as u64))?; + let ys = coset + .iter() + .copied() + .map(|z| evaluate_polynomialcoeff(polynomial_coeff, z)) + .collect::>(); + cells.push(coset_evals_to_cell(&ys)?); + } + Ok(cells) +} + +/// Compute all cell proofs for a polynomial in coefficient form. +pub fn compute_cells_and_kzg_proofs_polynomialcoeff( + setup: &EthereumKzgSetup, + polynomial_coeff: &[BLSFieldElement], +) -> Result<(Vec, Vec), KzgError> { + let mut cells = Vec::with_capacity(CELLS_PER_EXT_BLOB); + let mut proofs = Vec::with_capacity(CELLS_PER_EXT_BLOB); + for i in 0..CELLS_PER_EXT_BLOB { + let coset = coset_for_cell(CellIndex::new(i as u64))?; + let (proof, ys) = compute_kzg_proof_multi_impl(setup, polynomial_coeff, &coset)?; + cells.push(coset_evals_to_cell(&ys)?); + proofs.push(proof); + } + Ok((cells, proofs)) +} + +/// Compute all cells and cell proofs for an extended blob. +pub fn compute_cells_and_kzg_proofs( + setup: &EthereumKzgSetup, + blob: &[u8], +) -> Result<(Vec, Vec), KzgError> { + let polynomial = blob_to_polynomial(blob)?; + let polynomial_coeff = polynomial_eval_to_coeff(&polynomial)?; + compute_cells_and_kzg_proofs_polynomialcoeff(setup, &polynomial_coeff) +} + +/// Construct the vanishing polynomial for missing cell indices. +pub fn construct_vanishing_polynomial( + missing_cell_indices: &[CellIndex], +) -> Result, KzgError> { + // One factor per missing index yields one more coefficient than there are + // roots, so the spread write below stays inside the extended blob only when + // fewer than every cell is missing. + if missing_cell_indices.len() >= CELLS_PER_EXT_BLOB { + return Err(KzgError::TooManyCells { + maximum: CELLS_PER_EXT_BLOB - 1, + got: missing_cell_indices.len(), + }); + } + let roots_of_unity_reduced = compute_roots_of_unity(CELLS_PER_EXT_BLOB)?; + let roots = missing_cell_indices + .iter() + .map(|missing_cell_index| { + let index = missing_cell_index.as_usize(); + if index >= CELLS_PER_EXT_BLOB { + return Err(KzgError::CellIndexOutOfRange { + index: missing_cell_index.as_u64(), + limit: CELLS_PER_EXT_BLOB, + }); + } + Ok(roots_of_unity_reduced[reverse_bits(index, CELLS_PER_EXT_BLOB)]) + }) + .collect::, _>>()?; + let short_zero_poly = vanishing_polynomialcoeff(&roots); + let mut zero_poly_coeff = vec![Fr::zero(); FIELD_ELEMENTS_PER_EXT_BLOB]; + for (i, coeff) in short_zero_poly.into_iter().enumerate() { + zero_poly_coeff[i * FIELD_ELEMENTS_PER_CELL] = coeff; + } + Ok(zero_poly_coeff) +} + +/// Recover a blob polynomial from at least half of its cells. +pub fn recover_polynomialcoeff( + cell_indices: Vec, + cosets_evals: Vec, +) -> Result { + if cell_indices.len() != cosets_evals.len() { + return Err(KzgError::LengthMismatch { + context: "recovery cells", + expected: cell_indices.len(), + got: cosets_evals.len(), + }); + } + let roots_of_unity_extended = compute_roots_of_unity(FIELD_ELEMENTS_PER_EXT_BLOB)?; + let mut extended_evaluation_rbo = vec![Fr::zero(); FIELD_ELEMENTS_PER_EXT_BLOB]; + let mut present = [false; CELLS_PER_EXT_BLOB]; + for (cell_index, cell) in cell_indices.into_iter().zip(cosets_evals) { + if cell.len() != FIELD_ELEMENTS_PER_CELL { + return Err(KzgError::InvalidCosetLength { + expected: FIELD_ELEMENTS_PER_CELL, + got: cell.len(), + }); + } + let index = cell_index.as_usize(); + if index >= CELLS_PER_EXT_BLOB { + return Err(KzgError::CellIndexOutOfRange { + index: cell_index.as_u64(), + limit: CELLS_PER_EXT_BLOB, + }); + } + let start = index * FIELD_ELEMENTS_PER_CELL; + extended_evaluation_rbo[start..start + FIELD_ELEMENTS_PER_CELL].copy_from_slice(&cell); + present[index] = true; + } + let extended_evaluation = bit_reversal_permutation(&extended_evaluation_rbo); + let missing_cell_indices = (0..CELLS_PER_EXT_BLOB) + .filter(|cell_index| !present[*cell_index]) + .map(|cell_index| CellIndex::new(cell_index as u64)) + .collect::>(); + let zero_poly_coeff = construct_vanishing_polynomial(&missing_cell_indices)?; + let zero_poly_eval = fft_field(&zero_poly_coeff, &roots_of_unity_extended, false)?; + let extended_evaluation_times_zero = zero_poly_eval + .iter() + .copied() + .zip(extended_evaluation.iter().copied()) + .map(|(a, b)| a * b) + .collect::>(); + let extended_evaluation_times_zero_coeffs = fft_field( + &extended_evaluation_times_zero, + &roots_of_unity_extended, + true, + )?; + let extended_evaluations_over_coset = coset_fft_field( + &extended_evaluation_times_zero_coeffs, + &roots_of_unity_extended, + false, + )?; + let zero_poly_over_coset = coset_fft_field(&zero_poly_coeff, &roots_of_unity_extended, false)?; + let mut reconstructed_poly_over_coset = + Vec::with_capacity(extended_evaluations_over_coset.len()); + for (numerator, denominator) in extended_evaluations_over_coset + .iter() + .copied() + .zip(zero_poly_over_coset.iter().copied()) + { + let denominator_inverse = denominator.inverse().ok_or(KzgError::CosetDivisionByZero)?; + reconstructed_poly_over_coset.push(numerator * denominator_inverse); + } + let reconstructed_poly_coeff = coset_fft_field( + &reconstructed_poly_over_coset, + &roots_of_unity_extended, + true, + )?; + Ok(reconstructed_poly_coeff[..FIELD_ELEMENTS_PER_BLOB].to_vec()) +} + +/// Recover all cells and proofs from at least half of a blob's cells. +pub fn recover_cells_and_kzg_proofs( + setup: &EthereumKzgSetup, + cell_indices: Vec, + cells: Vec, +) -> Result<(Vec, Vec), KzgError> { + validate_recovery_inputs(&cell_indices, &cells)?; + let cosets_evals = cells + .into_iter() + .map(cell_to_coset_evals) + .collect::, _>>()?; + let polynomial_coeff = recover_polynomialcoeff(cell_indices, cosets_evals)?; + compute_cells_and_kzg_proofs_polynomialcoeff(setup, &polynomial_coeff) +} + +/// Validate recovery preconditions. +pub fn validate_recovery_inputs( + cell_indices: &[CellIndex], + cells: &[Cell], +) -> Result<(), KzgError> { + if cell_indices.len() != cells.len() { + return Err(KzgError::LengthMismatch { + context: "recovery inputs", + expected: cell_indices.len(), + got: cells.len(), + }); + } + let minimum = CELLS_PER_EXT_BLOB / 2; + if cell_indices.len() < minimum { + return Err(KzgError::NotEnoughCells { + minimum, + got: cell_indices.len(), + }); + } + if cell_indices.len() > CELLS_PER_EXT_BLOB { + return Err(KzgError::TooManyCells { + maximum: CELLS_PER_EXT_BLOB, + got: cell_indices.len(), + }); + } + let mut previous = None; + let mut seen = [false; CELLS_PER_EXT_BLOB]; + for cell_index in cell_indices { + let index = cell_index.as_usize(); + if index >= CELLS_PER_EXT_BLOB { + return Err(KzgError::CellIndexOutOfRange { + index: cell_index.as_u64(), + limit: CELLS_PER_EXT_BLOB, + }); + } + if previous.is_some_and(|previous| previous > index) { + return Err(KzgError::CellIndicesNotSorted); + } + if seen[index] { + return Err(KzgError::DuplicateCellIndex { + index: cell_index.as_u64(), + }); + } + previous = Some(index); + seen[index] = true; + } + Ok(()) +} + +/// Compute the Fiat-Shamir challenge for batch cell-proof verification. +pub fn compute_verify_cell_kzg_proof_batch_challenge( + commitments: &[KZGCommitment], + commitment_indices: &[CommitmentIndex], + cell_indices: &[CellIndex], + cosets_evals: &[CosetEvals], + proofs: &[KZGProof], +) -> BLSFieldElement { + let mut hashinput = Vec::new(); + hashinput.extend_from_slice(RANDOM_CHALLENGE_KZG_CELL_BATCH_DOMAIN); + hashinput.extend_from_slice(&(FIELD_ELEMENTS_PER_BLOB as u64).to_be_bytes()); + hashinput.extend_from_slice(&(FIELD_ELEMENTS_PER_CELL as u64).to_be_bytes()); + hashinput.extend_from_slice(&(commitments.len() as u64).to_be_bytes()); + hashinput.extend_from_slice(&(cell_indices.len() as u64).to_be_bytes()); + for commitment in commitments { + hashinput.extend_from_slice(&commitment.0); + } + for (k, coset_evals) in cosets_evals.iter().enumerate() { + hashinput.extend_from_slice(&commitment_indices[k].as_u64().to_be_bytes()); + hashinput.extend_from_slice(&cell_indices[k].as_u64().to_be_bytes()); + for coset_eval in coset_evals { + hashinput.extend_from_slice(&bls_field_to_bytes(*coset_eval)); + } + hashinput.extend_from_slice(&proofs[k].0); + } + hash_to_bls_field(&hashinput) +} + +/// Verify batch cell KZG proofs from serialized commitments, cells, and proofs. +pub fn verify_cell_kzg_proof_batch( + setup: &EthereumKzgSetup, + commitments_bytes: &[KZGCommitment], + cell_indices: &[CellIndex], + cells: &[Cell], + proofs_bytes: &[KZGProof], +) -> Result { + if commitments_bytes.len() != cell_indices.len() + || commitments_bytes.len() != cells.len() + || commitments_bytes.len() != proofs_bytes.len() + { + return Err(KzgError::BatchLengthMismatch { + commitments: commitments_bytes.len(), + cell_indices: cell_indices.len(), + cells: cells.len(), + proofs: proofs_bytes.len(), + }); + } + for cell_index in cell_indices { + if cell_index.as_usize() >= CELLS_PER_EXT_BLOB { + return Err(KzgError::CellIndexOutOfRange { + index: cell_index.as_u64(), + limit: CELLS_PER_EXT_BLOB, + }); + } + } + + let mut deduplicated_commitments_bytes = Vec::new(); + let mut deduplicated_commitments = Vec::new(); + let mut commitment_indices = Vec::with_capacity(commitments_bytes.len()); + for commitment_bytes in commitments_bytes { + if let Some(index) = deduplicated_commitments_bytes + .iter() + .position(|stored| stored == commitment_bytes) + { + commitment_indices.push(CommitmentIndex::new(index as u64)); + continue; + } + let commitment = bytes_to_kzg_commitment(*commitment_bytes)?; + commitment_indices.push(CommitmentIndex::new( + deduplicated_commitments_bytes.len() as u64 + )); + deduplicated_commitments_bytes.push(*commitment_bytes); + deduplicated_commitments.push(commitment); + } + + let cosets_evals = cells + .iter() + .copied() + .map(cell_to_coset_evals) + .collect::, _>>()?; + let proofs = proofs_bytes + .iter() + .copied() + .map(bytes_to_kzg_proof) + .collect::, _>>()?; + + let batch = ParsedCellKzgProofBatch { + commitments_bytes: deduplicated_commitments_bytes, + commitments: deduplicated_commitments, + commitment_indices, + cell_indices: cell_indices.to_vec(), + cosets_evals, + proofs_bytes: proofs_bytes.to_vec(), + proofs, + }; + + verify_cell_kzg_proof_batch_impl(setup, &batch) +} + +/// Verify batch cell KZG proofs from parsed commitments, cells, and proofs. +pub fn verify_cell_kzg_proof_batch_impl( + setup: &EthereumKzgSetup, + batch: &ParsedCellKzgProofBatch, +) -> Result { + let num_cells = batch.cell_indices.len(); + validate_parsed_commitments(batch)?; + if batch.commitment_indices.len() != num_cells { + return Err(KzgError::LengthMismatch { + context: "commitment indices", + expected: num_cells, + got: batch.commitment_indices.len(), + }); + } + if batch.cosets_evals.len() != num_cells { + return Err(KzgError::LengthMismatch { + context: "coset evaluations", + expected: num_cells, + got: batch.cosets_evals.len(), + }); + } + if batch.proofs_bytes.len() != num_cells || batch.proofs.len() != num_cells { + return Err(KzgError::LengthMismatch { + context: "proofs", + expected: num_cells, + got: batch.proofs_bytes.len().min(batch.proofs.len()), + }); + } + for commitment_index in &batch.commitment_indices { + if commitment_index.as_usize() >= batch.commitments.len() { + return Err(KzgError::CommitmentIndexOutOfRange { + index: commitment_index.as_u64(), + commitments: batch.commitments.len(), + }); + } + } + for coset_evals in &batch.cosets_evals { + if coset_evals.len() != FIELD_ELEMENTS_PER_CELL { + return Err(KzgError::InvalidCosetLength { + expected: FIELD_ELEMENTS_PER_CELL, + got: coset_evals.len(), + }); + } + } + if num_cells == 0 { + return Ok(true); + } + + let r = compute_verify_cell_kzg_proof_batch_challenge( + &batch.commitments_bytes, + &batch.commitment_indices, + &batch.cell_indices, + &batch.cosets_evals, + &batch.proofs_bytes, + ); + let r_powers = compute_powers(r, num_cells); + + let ll = g1_lincomb(&batch.proofs, &r_powers)?; + let lr = setup + .g2_powers() + .get(FIELD_ELEMENTS_PER_CELL) + .copied() + .ok_or(KzgError::MissingG2Power { + index: FIELD_ELEMENTS_PER_CELL, + available: setup.g2_powers().len(), + })?; + + let mut weights = vec![Fr::zero(); batch.commitments.len()]; + for (k, commitment_index) in batch.commitment_indices.iter().copied().enumerate() { + weights[commitment_index.as_usize()] += r_powers[k]; + } + let rlc = g1_lincomb(&batch.commitments, &weights)?; + + let mut sum_interp_polys_coeff = vec![Fr::zero(); FIELD_ELEMENTS_PER_CELL]; + for (k, r_power) in r_powers.iter().copied().enumerate().take(num_cells) { + let interp_poly_coeff = interpolate_polynomialcoeff( + &coset_for_cell(batch.cell_indices[k])?, + &batch.cosets_evals[k], + )?; + let interp_poly_scaled_coeff = multiply_polynomialcoeff(&[r_power], &interp_poly_coeff); + sum_interp_polys_coeff = + add_polynomialcoeff(&sum_interp_polys_coeff, &interp_poly_scaled_coeff); + } + let rli = setup_g1_lincomb(setup.g1_affine_powers(), &sum_interp_polys_coeff)?; + + let mut weighted_r_powers = Vec::with_capacity(num_cells); + for (k, r_power) in r_powers.iter().copied().enumerate().take(num_cells) { + let h_k = coset_shift_for_cell(batch.cell_indices[k])?; + weighted_r_powers.push(r_power * h_k.pow([FIELD_ELEMENTS_PER_CELL as u64])); + } + let rlp = g1_lincomb(&batch.proofs, &weighted_r_powers)?; + let rl = rlc - rli + rlp; + + let lhs = Bls12_381::pairing(ll, lr); + let rhs = Bls12_381::pairing(rl, G2Affine::generator()); + Ok(lhs == rhs) +} + +/// Validate the byte and point views of deduplicated commitments. +pub fn validate_parsed_commitments(batch: &ParsedCellKzgProofBatch) -> Result<(), KzgError> { + if batch.commitments_bytes.len() != batch.commitments.len() { + return Err(KzgError::CommitmentBytesLengthMismatch { + bytes: batch.commitments_bytes.len(), + points: batch.commitments.len(), + }); + } + + for (index, (commitment_bytes, commitment)) in batch + .commitments_bytes + .iter() + .copied() + .zip(batch.commitments.iter().copied()) + .enumerate() + { + let parsed = bytes_to_kzg_commitment(commitment_bytes)?; + if parsed != commitment { + return Err(KzgError::CommitmentBytesPointMismatch { index }); + } + } + + Ok(()) +} diff --git a/moonglass-core/src/crypto/kzg/error.rs b/moonglass-core/src/crypto/kzg/error.rs new file mode 100644 index 0000000..eeec930 --- /dev/null +++ b/moonglass-core/src/crypto/kzg/error.rs @@ -0,0 +1,282 @@ +//! Error types shared by KZG operations. + +use thiserror::Error; + +/// KZG operation failures. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +#[non_exhaustive] +pub enum KzgError { + /// Polynomial has more coefficients than the trusted-setup G1 powers cover. + #[error( + "polynomial has {coefficients} coefficients but setup only has {setup_powers} G1 powers" + )] + PolynomialTooLarge { + /// Polynomial coefficient count requested by the operation. + coefficients: usize, + /// Number of G1 powers available in the setup. + setup_powers: usize, + }, + + /// FK opening was requested on an empty polynomial. + #[error("FK opening requires a non-empty polynomial")] + EmptyPolynomial, + + /// FK evaluation domain size does not match the polynomial length. + #[error("FK domain size {domain_size} does not match {coefficients} coefficients")] + DomainSizeMismatch { + /// Polynomial coefficient count. + coefficients: usize, + /// Radix-2 evaluation domain size. + domain_size: usize, + }, + + /// Doubling the polynomial length to derive the FK domain overflowed `usize`. + #[error("FK domain size overflow for {coefficients} coefficients")] + DomainSizeOverflow { + /// Polynomial coefficient count that overflowed the doubled domain. + coefficients: usize, + }, + + /// The scalar field does not support a radix-2 domain of the requested size. + #[error("unsupported radix-2 domain size {0}")] + UnsupportedDomainSize(usize), + + /// Serialized scalar is outside the BLS scalar field. + #[error("invalid BLS scalar field element")] + InvalidFieldElement, + + /// Serialized compressed G1 point is not a valid KZG point. + #[error("invalid KZG G1 point")] + InvalidG1, + + /// Batch input lists have different lengths. + #[error( + "batch length mismatch: commitments {commitments}, cell_indices {cell_indices}, cells {cells}, proofs {proofs}" + )] + BatchLengthMismatch { + /// Number of commitments. + commitments: usize, + /// Number of cell indices. + cell_indices: usize, + /// Number of cells. + cells: usize, + /// Number of proofs. + proofs: usize, + }, + + /// Two paired slices have different lengths. + #[error("{context} length mismatch: expected {expected}, got {got}")] + LengthMismatch { + /// Static context for the paired inputs. + context: &'static str, + /// Expected length. + expected: usize, + /// Actual length. + got: usize, + }, + + /// Cell index exceeds the extended blob domain. + #[error("cell index {index} out of range, limit {limit}")] + CellIndexOutOfRange { + /// Cell index supplied by the caller. + index: u64, + /// Exclusive upper bound. + limit: usize, + }, + + /// Commitment index exceeds the deduplicated commitment list. + #[error("commitment index {index} out of range, commitments {commitments}")] + CommitmentIndexOutOfRange { + /// Commitment index supplied by the caller. + index: u64, + /// Number of commitments. + commitments: usize, + }, + + /// A cell or cell-derived byte sequence has the wrong byte length. + #[error("invalid cell length: expected {expected}, got {got}")] + InvalidCellLength { + /// Expected byte length. + expected: usize, + /// Actual byte length. + got: usize, + }, + + /// A cell coset has the wrong number of field elements. + #[error("invalid coset length: expected {expected}, got {got}")] + InvalidCosetLength { + /// Expected field-element count. + expected: usize, + /// Actual field-element count. + got: usize, + }, + + /// A blob has the wrong byte length. + #[error("invalid blob length: expected {expected}, got {got}")] + InvalidBlobLength { + /// Expected byte length. + expected: usize, + /// Actual byte length. + got: usize, + }, + + /// Recovery was asked to run with fewer than half of all cells. + #[error("not enough cells: minimum {minimum}, got {got}")] + NotEnoughCells { + /// Minimum number of cells. + minimum: usize, + /// Actual number of cells. + got: usize, + }, + + /// Recovery was asked to run with more than all cells. + #[error("too many cells: maximum {maximum}, got {got}")] + TooManyCells { + /// Maximum number of cells. + maximum: usize, + /// Actual number of cells. + got: usize, + }, + + /// Recovery input repeated a cell index. + #[error("duplicate cell index {index}")] + DuplicateCellIndex { + /// Repeated cell index. + index: u64, + }, + + /// Recovery input cell indices are not sorted ascending. + #[error("cell indices are not sorted ascending")] + CellIndicesNotSorted, + + /// Polynomial division was given an empty divisor. + #[error("polynomial division by an empty divisor")] + EmptyDivisor, + + /// Polynomial division was given a divisor with zero leading coefficient. + #[error("polynomial division by a divisor with zero leading coefficient")] + DivisionByZero, + + /// Coset division found a zero divisor. + #[error("coset division by zero")] + CosetDivisionByZero, + + /// The interpolation domain contained a duplicate point. + #[error("duplicate interpolation point")] + DuplicateInterpolationPoint, + + /// The setup does not include the requested G2 power. + #[error("missing G2 power {index}; setup has {available} powers")] + MissingG2Power { + /// Requested G2 power index. + index: usize, + /// Number of G2 powers available. + available: usize, + }, + + /// Parsed commitment bytes and points do not have the same length. + #[error("commitment bytes length {bytes} does not match parsed points {points}")] + CommitmentBytesLengthMismatch { + /// Serialized commitment count. + bytes: usize, + /// Parsed commitment point count. + points: usize, + }, + + /// Parsed commitment bytes do not decode to the supplied point. + #[error("commitment bytes at index {index} do not match parsed point")] + CommitmentBytesPointMismatch { + /// Deduplicated commitment index. + index: usize, + }, +} + +/// Trusted-setup parsing failures. +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum SetupFileError { + /// Required `[tau]_2` point is missing. + #[error("trusted setup does not include [tau]_2")] + MissingTauG2, + + /// Text setup is not UTF-8. + #[error("trusted setup text is not UTF-8 at byte {valid_up_to}")] + InvalidUtf8 { + /// Number of valid bytes before the invalid sequence. + valid_up_to: usize, + /// Length of the invalid sequence when known. + error_len: Option, + }, + + /// Text setup has the wrong number of non-empty lines. + #[error("trusted setup text has {got} lines, expected {expected}")] + InvalidTextLineCount { + /// Expected number of non-empty lines. + expected: usize, + /// Actual number of non-empty lines. + got: usize, + }, + + /// Text setup count line is not a decimal integer. + #[error("invalid trusted setup count on line {line}")] + InvalidTextCount { + /// One-based line number. + line: usize, + }, + + /// Text setup count line exceeded `usize`. + #[error("trusted setup count overflows usize on line {line}")] + TextCountOverflow { + /// One-based line number. + line: usize, + }, + + /// A compressed point hex line has the wrong length. + #[error("line {line}: expected {expected} hex chars, got {got}")] + InvalidHexLength { + /// One-based line number. + line: usize, + /// Expected hex-character count. + expected: usize, + /// Actual hex-character count. + got: usize, + }, + + /// A compressed point hex line contains a non-hex character. + #[error("line {line}: invalid hex character {value:?}")] + InvalidHexCharacter { + /// One-based line number. + line: usize, + /// Invalid character. + value: char, + }, + + /// Curve point parsing failed on a G1 setup line. + #[error("line {line}: invalid G1 point")] + InvalidG1Point { + /// One-based line number. + line: usize, + }, + + /// Curve point parsing failed on a G2 setup line. + #[error("line {line}: invalid G2 point")] + InvalidG2Point { + /// One-based line number. + line: usize, + }, + + /// Size arithmetic overflowed. + #[error("setup size overflow")] + SizeOverflow, +} + +/// Ensure the setup has enough G1 powers for `coefficients` polynomial terms. +pub fn ensure_supported(coefficients: usize, setup_powers: usize) -> Result<(), KzgError> { + if coefficients > setup_powers { + return Err(KzgError::PolynomialTooLarge { + coefficients, + setup_powers, + }); + } + Ok(()) +} diff --git a/moonglass/src/crypto/kzg/fk.rs b/moonglass-core/src/crypto/kzg/fk.rs similarity index 100% rename from moonglass/src/crypto/kzg/fk.rs rename to moonglass-core/src/crypto/kzg/fk.rs diff --git a/moonglass/src/crypto/kzg/opening.rs b/moonglass-core/src/crypto/kzg/opening.rs similarity index 80% rename from moonglass/src/crypto/kzg/opening.rs rename to moonglass-core/src/crypto/kzg/opening.rs index 8bf0b18..d59dfad 100644 --- a/moonglass/src/crypto/kzg/opening.rs +++ b/moonglass-core/src/crypto/kzg/opening.rs @@ -1,4 +1,9 @@ //! Single-point KZG commitments, openings, and verification. +//! +//! These primitives are currently unexercised by the reference test suite, +//! which covers only the cell and data availability sampling path. Treat them +//! as unverified cryptographic scaffolding for the external driver until +//! known answer coverage is added. use ark_ec::{AffineRepr, VariableBaseMSM, pairing::Pairing}; use ark_ff::{FftField, Field}; @@ -30,7 +35,7 @@ where /// Open a polynomial at `point`. /// /// For `z = point`, the proof is `pi = [q(tau)]_1` where -/// `q(X) = (p(X) - p(z)) / (X - z)`. +/// `q(X) = (p(X)-p(z)) / (X-z)`. pub fn open( setup: &KzgSetup, polynomial: &DensePolynomial, @@ -45,11 +50,11 @@ where // y = p(z). let value = polynomial.evaluate(point); - // The numerator is n(X) = p(X) - y. + // The numerator is n(X)=p(X)-y. let value_polynomial = DensePolynomial::from_coefficients_slice(&[value]); let numerator = polynomial - &value_polynomial; - // The denominator is d(X) = X - z. + // The denominator is d(X)=X-z. let denominator = DensePolynomial::from_coefficients_slice(&[-*point, E::ScalarField::ONE]); // The witness polynomial is q(X) = n(X) / d(X). @@ -61,7 +66,7 @@ where /// Verify an opening proof for a polynomial commitment at `point`. /// -/// Checks `e(C - [y]_1, [1]_2) = e(pi, [tau - z]_2)`. +/// Checks `e(C-[y]_1, [1]_2) = e(pi, [tau-z]_2)`. pub fn verify( setup: &KzgSetup, commitment: E::G1, @@ -78,10 +83,10 @@ where // [z]_2 = z * [1]_2. let point_g2 = E::G2Affine::generator() * point; - // Left side: e(C - [y]_1, [1]_2) = e([p(tau) - y]_1, [1]_2). + // Left side: e(C-[y]_1, [1]_2) = e([p(tau)-y]_1, [1]_2). let lhs = E::pairing(commitment - value_g1, E::G2Affine::generator()); - // Right side: e(pi, [tau]_2 - [z]_2) = e([q(tau)]_1, [tau - z]_2). + // Right side: e(pi, [tau]_2-[z]_2) = e([q(tau)]_1, [tau-z]_2). let rhs = E::pairing(proof, setup.tau_g2 - point_g2); Ok(lhs == rhs) diff --git a/moonglass/src/crypto/kzg/setup.rs b/moonglass-core/src/crypto/kzg/setup.rs similarity index 62% rename from moonglass/src/crypto/kzg/setup.rs rename to moonglass-core/src/crypto/kzg/setup.rs index 0078e5a..8e21393 100644 --- a/moonglass/src/crypto/kzg/setup.rs +++ b/moonglass-core/src/crypto/kzg/setup.rs @@ -4,10 +4,8 @@ use ark_bls12_381::Bls12_381; use ark_ec::{AffineRepr, CurveGroup, pairing::Pairing}; use ark_ff::Field; -use super::trusted_setup::{ - ETHEREUM_TRUSTED_SETUP_TEXT, PowersOfTau, SetupFileError, get_powers_from_file, - get_powers_from_text, -}; +use super::error::SetupFileError; +use super::trusted_setup::{ETHEREUM_TRUSTED_SETUP_TEXT, PowersOfTau, get_powers_from_text}; /// Ethereum's BLS12-381 KZG setup type. pub type EthereumKzgSetup = KzgSetup; @@ -15,106 +13,125 @@ pub type EthereumKzgSetup = KzgSetup; /// KZG setup material. /// /// For a secret `tau`, the structured reference string contains: -/// - G1 powers: `[tau^0]_1, [tau^1]_1, ...` -/// - one G2 power: `[tau]_2` +/// G1 powers `[tau^i]_1` and G2 powers `[tau^i]_2`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct KzgSetup { /// G1 powers of tau as projective points for FK operations. - pub(super) g1_powers: Vec, + pub g1_powers: Vec, /// G1 powers of tau as affine points for MSM commitment operations. - pub(super) g1_affine_powers: Vec, + pub g1_affine_powers: Vec, + /// G2 powers of tau as projective points for proof verification. + pub g2_powers: Vec, /// The `[tau]_2` point used to verify KZG openings. - pub(super) tau_g2: E::G2, + pub tau_g2: E::G2, } impl KzgSetup where E: Pairing, { - /// Build setup material from a trusted-setup file. - /// - /// Supports Ethereum c-kzg `trusted_setup.txt`. - pub fn from_trusted_setup_file( - path: impl AsRef, - ) -> Result { - Self::from_monomial_powers(get_powers_from_file::(path)?) - } - /// Build setup material from Ethereum c-kzg trusted-setup text. pub fn from_trusted_setup_text(text: &str) -> Result { Self::from_monomial_powers(get_powers_from_text::(text)?) } /// Build setup material from parsed monomial powers. - fn from_monomial_powers( + pub fn from_monomial_powers( (g1_affine_powers, g2_affine_powers): PowersOfTau, ) -> Result { - // [tau]_2 is the second G2 point because the monomial block starts with [1]_2. + // `[tau]_2` is the second G2 point because the monomial block starts with `[1]_2`. let tau_g2 = g2_affine_powers .get(1) .copied() .ok_or(SetupFileError::MissingTauG2)? .into_group(); - Ok(Self::from_g1_affine_powers(g1_affine_powers, tau_g2)) + let g1_powers = g1_affine_powers + .iter() + .copied() + .map(AffineRepr::into_group) + .collect(); + let g2_powers = g2_affine_powers + .iter() + .copied() + .map(AffineRepr::into_group) + .collect(); + + Ok(Self { + g1_powers, + g1_affine_powers, + g2_powers, + tau_g2, + }) } - /// Build setup material from an explicit secret. Only use this in tests. - #[must_use] + /// Build setup material from an explicit secret for controlled experiments. pub fn setup(secret: E::ScalarField, powers: usize) -> Self { - let generator = E::G1Affine::generator(); + let g1_generator = E::G1Affine::generator(); let mut tau_power = E::ScalarField::ONE; let mut g1_powers = Vec::with_capacity(powers); // [tau^i]_1 = tau^i * [1]_1 for every supported coefficient index i. for _ in 0..powers { - g1_powers.push(generator * tau_power); + g1_powers.push(g1_generator * tau_power); tau_power *= secret; } // Keep affine powers for MSM and projective powers for FK convolution. let g1_affine_powers = E::G1::normalize_batch(&g1_powers); + let g2_generator = E::G2Affine::generator(); + let mut tau_power = E::ScalarField::ONE; + let mut g2_powers = Vec::with_capacity(powers); + for _ in 0..powers { + g2_powers.push(g2_generator * tau_power); + tau_power *= secret; + } + // [tau]_2 = tau * [1]_2. - let tau_g2 = E::G2Affine::generator() * secret; + let tau_g2 = g2_generator * secret; Self { g1_powers, g1_affine_powers, + g2_powers, tau_g2, } } /// Build setup material from G1 powers and a G2 tau point. - #[must_use] pub fn from_g1_affine_powers(g1_affine_powers: Vec, tau_g2: E::G2) -> Self { let g1_powers = g1_affine_powers .iter() .copied() .map(AffineRepr::into_group) .collect(); + let g2_powers = vec![E::G2Affine::generator().into_group(), tau_g2]; Self { g1_powers, g1_affine_powers, + g2_powers, tau_g2, } } /// Powers of tau in G1 as projective points. - #[must_use] pub fn g1_powers(&self) -> &[E::G1] { &self.g1_powers } /// Powers of tau in G1 as affine points. - #[must_use] pub fn g1_affine_powers(&self) -> &[E::G1Affine] { &self.g1_affine_powers } + /// Powers of tau in G2 as projective points. + pub fn g2_powers(&self) -> &[E::G2] { + &self.g2_powers + } + /// Tau in G2. - #[must_use] pub fn tau_g2(&self) -> E::G2 { self.tau_g2 } diff --git a/moonglass/src/crypto/kzg/trusted_setup.rs b/moonglass-core/src/crypto/kzg/trusted_setup.rs similarity index 57% rename from moonglass/src/crypto/kzg/trusted_setup.rs rename to moonglass-core/src/crypto/kzg/trusted_setup.rs index b1e388c..ff12f6f 100644 --- a/moonglass/src/crypto/kzg/trusted_setup.rs +++ b/moonglass-core/src/crypto/kzg/trusted_setup.rs @@ -1,25 +1,32 @@ //! Ethereum c-kzg trusted setup parser. //! //! The only supported setup format is `trusted_setup.txt` from -//! `ethereum/c-kzg-4844`. +//! the upstream Ethereum c-kzg trusted setup text. -use std::{path::Path, str}; +use std::{num::IntErrorKind, ops::Range, str}; use ark_ec::{AffineRepr, pairing::Pairing}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress}; -use thiserror::Error; + +use super::error::SetupFileError; /// Vendored Ethereum mainnet `trusted_setup.txt` contents. -pub(crate) const ETHEREUM_TRUSTED_SETUP_TEXT: &str = include_str!(concat!( +pub const ETHEREUM_TRUSTED_SETUP_TEXT: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/assets/trusted_setup.txt" )); /// Number of count lines at the start of Ethereum c-kzg setup text. -const TEXT_HEADER_LINES: usize = 2; +pub const TEXT_HEADER_LINES: usize = 2; /// Non-empty setup text line with its one-based source line number. -type SetupTextLine<'a> = (usize, &'a str); +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SetupTextLine { + /// One-based source line number. + pub line: usize, + /// Trimmed setup text. + pub text: String, +} /// G1 and G2 monomial powers of tau. pub type PowersOfTau = (Vec<::G1Affine>, Vec<::G2Affine>); @@ -27,21 +34,21 @@ pub type PowersOfTau = (Vec<::G1Affine>, Vec<::G2 /// Ethereum c-kzg `trusted_setup.txt`. /// /// The file starts with two counts: -/// - `n`, the number of G1 points in both bases -/// - `m`, the number of G2 monomial points +/// `n` is the number of G1 points in both bases. `m` is the number of G2 +/// monomial points. /// /// The remaining lines are ordered as: -/// - `L_i = [ell_i(tau)]_1`, the G1 lagrange-basis points -/// - `T_i = [tau^i]_2`, the G2 monomial powers -/// - `M_i = [tau^i]_1`, the G1 monomial powers +/// `L_i = [ell_i(tau)]_1` gives the G1 lagrange-basis points. +/// `T_i = [tau^i]_2` gives the G2 monomial powers. +/// `M_i = [tau^i]_1` gives the G1 monomial powers. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct EthereumTrustedSetup { /// G1 lagrange-basis points from the setup file. - g1_lagrange: Vec, + pub g1_lagrange: Vec, /// G2 monomial powers from the setup file. - g2_monomial: Vec, + pub g2_monomial: Vec, /// G1 monomial powers from the setup file. - g1_monomial: Vec, + pub g1_monomial: Vec, } impl EthereumTrustedSetup { @@ -55,10 +62,8 @@ impl EthereumTrustedSetup { }); } - let (g1_count_line, g1_count_text) = lines[0]; - let (g2_count_line, g2_count_text) = lines[1]; - let g1_count = parse_text_count(g1_count_text, g1_count_line)?; - let g2_count = parse_text_count(g2_count_text, g2_count_line)?; + let g1_count = parse_text_count(&lines[0].text, lines[0].line)?; + let g2_count = parse_text_count(&lines[1].text, lines[1].line)?; let expected_lines = g1_count .checked_mul(2) @@ -101,44 +106,34 @@ impl EthereumTrustedSetup { } /// G1 lagrange-basis powers. - #[must_use] pub fn g1_lagrange_powers(&self) -> &[E::G1Affine] { &self.g1_lagrange } /// G1 monomial powers `[tau^i]_1`. - #[must_use] pub fn g1_monomial_powers(&self) -> &[E::G1Affine] { &self.g1_monomial } /// G2 monomial powers `[tau^i]_2`. - #[must_use] pub fn g2_monomial_powers(&self) -> &[E::G2Affine] { &self.g2_monomial } /// Consume the parsed setup into the monomial powers KZG code uses. - #[must_use] pub fn into_monomial_powers(self) -> PowersOfTau { (self.g1_monomial, self.g2_monomial) } } -/// Load G1 and G2 monomial powers from an Ethereum c-kzg setup file. -pub fn get_powers_from_file( - path: impl AsRef, -) -> Result, SetupFileError> { - let file_data = - std::fs::read(path.as_ref()).map_err(|e| SetupFileError::FileError(e.to_string()))?; - get_powers_from_bytes::(&file_data) -} - /// Parse G1 and G2 monomial powers from Ethereum c-kzg setup bytes. pub fn get_powers_from_bytes( file_data: &[u8], ) -> Result, SetupFileError> { - let text = str::from_utf8(file_data).map_err(|e| SetupFileError::InvalidUtf8(e.to_string()))?; + let text = str::from_utf8(file_data).map_err(|e| SetupFileError::InvalidUtf8 { + valid_up_to: e.valid_up_to(), + error_len: e.error_len(), + })?; get_powers_from_text::(text) } @@ -148,43 +143,46 @@ pub fn get_powers_from_text(text: &str) -> Result, Se } /// Normalize setup text into trimmed non-empty lines with one-based numbers. -fn setup_text_lines(text: &str) -> Vec> { +pub fn setup_text_lines(text: &str) -> Vec { text.lines() .enumerate() .filter_map(|(index, line)| { let trimmed = line.trim(); - (!trimmed.is_empty()).then_some((index + 1, trimmed)) + (!trimmed.is_empty()).then(|| SetupTextLine { + line: index + 1, + text: trimmed.to_owned(), + }) }) .collect() } /// Parse a setup count line. -fn parse_text_count(text: &str, line: usize) -> Result { - text.parse::() - .map_err(|e| SetupFileError::InvalidTextCount { - line, - value: text.to_owned(), - reason: e.to_string(), - }) +pub fn parse_text_count(text: &str, line: usize) -> Result { + text.parse::().map_err(|e| { + if matches!( + e.kind(), + IntErrorKind::PosOverflow | IntErrorKind::NegOverflow + ) { + SetupFileError::TextCountOverflow { line } + } else { + SetupFileError::InvalidTextCount { line } + } + }) } /// Parse a range of compressed G1 points from setup text lines. -fn parse_g1_range( - lines: &[SetupTextLine<'_>], - range: std::ops::Range, +pub fn parse_g1_range( + lines: &[SetupTextLine], + range: Range, ) -> Result, SetupFileError> { let element_size = E::G1Affine::generator().serialized_size(Compress::Yes); let mut powers = Vec::with_capacity(range.len()); for index in range { - let (line, text) = lines[index]; - let bytes = parse_hex_line(text, line, element_size)?; - let element = E::G1Affine::deserialize_compressed(bytes.as_slice()).map_err(|e| { - SetupFileError::TextPointParse { - line, - reason: e.to_string(), - } - })?; + let line = lines[index].line; + let bytes = parse_hex_line(&lines[index].text, line, element_size)?; + let element = E::G1Affine::deserialize_compressed(bytes.as_slice()) + .map_err(|_| SetupFileError::InvalidG1Point { line })?; powers.push(element); } @@ -192,22 +190,18 @@ fn parse_g1_range( } /// Parse a range of compressed G2 points from setup text lines. -fn parse_g2_range( - lines: &[SetupTextLine<'_>], - range: std::ops::Range, +pub fn parse_g2_range( + lines: &[SetupTextLine], + range: Range, ) -> Result, SetupFileError> { let element_size = E::G2Affine::generator().serialized_size(Compress::Yes); let mut powers = Vec::with_capacity(range.len()); for index in range { - let (line, text) = lines[index]; - let bytes = parse_hex_line(text, line, element_size)?; - let element = E::G2Affine::deserialize_compressed(bytes.as_slice()).map_err(|e| { - SetupFileError::TextPointParse { - line, - reason: e.to_string(), - } - })?; + let line = lines[index].line; + let bytes = parse_hex_line(&lines[index].text, line, element_size)?; + let element = E::G2Affine::deserialize_compressed(bytes.as_slice()) + .map_err(|_| SetupFileError::InvalidG2Point { line })?; powers.push(element); } @@ -215,7 +209,7 @@ fn parse_g2_range( } /// Decode one compressed point line from hex. -fn parse_hex_line( +pub fn parse_hex_line( text: &str, line: usize, expected_bytes: usize, @@ -242,7 +236,7 @@ fn parse_hex_line( } /// Convert one ASCII hex byte to its nibble value. -fn hex_value(byte: u8, line: usize) -> Result { +pub fn hex_value(byte: u8, line: usize) -> Result { match byte { b'0'..=b'9' => Ok(byte - b'0'), b'a'..=b'f' => Ok(byte - b'a' + 10), @@ -253,73 +247,3 @@ fn hex_value(byte: u8, line: usize) -> Result { }), } } - -/// Trusted-setup parsing failures. -#[derive(Error, Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub enum SetupFileError { - /// Required `[tau]_2` point is missing. - #[error("trusted setup does not include [tau]_2")] - MissingTauG2, - - /// File IO failed. - #[error("file error: {0}")] - FileError(String), - - /// Text setup is not UTF-8. - #[error("trusted setup text is not UTF-8: {0}")] - InvalidUtf8(String), - - /// Text setup has the wrong number of non-empty lines. - #[error("trusted setup text has {got} lines, expected {expected}")] - InvalidTextLineCount { - /// Expected number of non-empty lines. - expected: usize, - /// Actual number of non-empty lines. - got: usize, - }, - - /// Text setup count line could not be parsed. - #[error("invalid trusted setup count on line {line}: {value:?}: {reason}")] - InvalidTextCount { - /// One-based line number. - line: usize, - /// Raw count text. - value: String, - /// Parser error text. - reason: String, - }, - - /// A compressed point hex line has the wrong length. - #[error("line {line}: expected {expected} hex chars, got {got}")] - InvalidHexLength { - /// One-based line number. - line: usize, - /// Expected hex-character count. - expected: usize, - /// Actual hex-character count. - got: usize, - }, - - /// A compressed point hex line contains a non-hex character. - #[error("line {line}: invalid hex character {value:?}")] - InvalidHexCharacter { - /// One-based line number. - line: usize, - /// Invalid character. - value: char, - }, - - /// Curve point parsing failed on a setup line. - #[error("line {line}: point parse error: {reason}")] - TextPointParse { - /// One-based line number. - line: usize, - /// Curve parser error text. - reason: String, - }, - - /// Size arithmetic overflowed. - #[error("setup size overflow")] - SizeOverflow, -} diff --git a/moonglass/src/error.rs b/moonglass-core/src/error.rs similarity index 77% rename from moonglass/src/error.rs rename to moonglass-core/src/error.rs index 8353e82..3070ae9 100644 --- a/moonglass/src/error.rs +++ b/moonglass-core/src/error.rs @@ -8,14 +8,15 @@ //! `PrimitivesError` covers operations on primitive protocol values and is //! surfaced by transition helpers when primitive validation is part of a phase. -mod block; -mod fork_choice; -mod merkle; -mod operation; -mod primitive; -mod registry; -mod signature; -mod slot; +pub mod block; +pub mod fork_choice; +pub mod merkle; +pub mod operation; +pub mod primitive; +pub mod registry; +pub mod signature; +pub mod slot; +pub mod transition; pub use block::*; pub use fork_choice::*; @@ -25,6 +26,7 @@ pub use primitive::*; pub use registry::*; pub use signature::*; pub use slot::*; +pub use transition::*; use thiserror::Error; @@ -75,4 +77,16 @@ pub enum TransitionError { /// invalid. #[error("balance arithmetic overflow")] BalanceOverflow, + + /// Shared arithmetic failed outside a more specific error domain. + #[error("state-transition arithmetic overflow in {0:?}")] + ArithmeticOverflow(TransitionArithmetic), + + /// A bounded consensus list reached its capacity. + #[error("bounded consensus list is full: {0:?}")] + BoundedListFull(BoundedList), + + /// Internal state shape broke a transition invariant. + #[error(transparent)] + Invariant(#[from] StateTransitionInvariant), } diff --git a/moonglass/src/error/block.rs b/moonglass-core/src/error/block.rs similarity index 96% rename from moonglass/src/error/block.rs rename to moonglass-core/src/error/block.rs index 6b42b06..9c69384 100644 --- a/moonglass/src/error/block.rs +++ b/moonglass-core/src/error/block.rs @@ -69,6 +69,10 @@ pub enum BlockError { #[error("parent payload requests root mismatch")] ParentPayloadRequestsMismatch, + /// Parent payload was not expected to carry execution requests. + #[error("parent payload must not carry execution requests")] + ParentPayloadUnexpectedRequests, + /// Payload envelope's builder index does not match the accepted bid. #[error("execution payload envelope builder mismatch")] EnvelopeBuilderMismatch, diff --git a/moonglass/src/error/fork_choice.rs b/moonglass-core/src/error/fork_choice.rs similarity index 80% rename from moonglass/src/error/fork_choice.rs rename to moonglass-core/src/error/fork_choice.rs index 4c485f1..a825642 100644 --- a/moonglass/src/error/fork_choice.rs +++ b/moonglass-core/src/error/fork_choice.rs @@ -6,8 +6,9 @@ use thiserror::Error; +use crate::crypto::kzg::{KzgError, SetupFileError}; use crate::error::{MerkleError, TransitionError}; -use crate::primitives::{Root, Slot, ValidatorIndex}; +use crate::primitives::{ColumnIndex, Root, Slot, ValidatorIndex}; /// Failures raised by fork-choice rule evaluation. #[derive(Debug, Clone, Error)] @@ -85,9 +86,43 @@ pub enum ForkChoiceError { #[error("payload envelope for unknown block {0:?}")] PayloadEnvelopeForUnknownBlock(Root), - /// Payload data was not available for the block, so the envelope is rejected. - #[error("payload data unavailable for block {0:?}")] - PayloadDataUnavailable(Root), + /// Data-column sidecar failed the non-cryptographic shape checks. + #[error("invalid data column sidecar {column:?} for block {block:?}")] + DataColumnSidecarInvalid { + /// Block root the sidecar belongs to. + block: Root, + /// Column index carried by the sidecar. + column: ColumnIndex, + }, + + /// Data-column sidecar does not match the known block slot. + #[error("data column sidecar slot {sidecar_slot:?} does not match block slot {block_slot:?}")] + DataColumnSidecarSlotMismatch { + /// Slot carried by the sidecar. + sidecar_slot: Slot, + /// Slot carried by the known block. + block_slot: Slot, + }, + + /// Data-column sidecar KZG proof verification returned false. + #[error("data column sidecar KZG proof invalid for block {block:?}, column {column:?}")] + DataColumnSidecarProofInvalid { + /// Block root the sidecar belongs to. + block: Root, + /// Column index carried by the sidecar. + column: ColumnIndex, + }, + + /// Data-column sidecar KZG proof verification failed before producing a verdict. + #[error("data column sidecar KZG proof error for block {block:?}, column {column:?}: {source}")] + DataColumnSidecarProofError { + /// Block root the sidecar belongs to. + block: Root, + /// Column index carried by the sidecar. + column: ColumnIndex, + /// KZG verifier error. + source: KzgError, + }, /// The execution engine reported the payload invalid for the block. #[error("execution engine rejected payload for block {0:?}")] @@ -105,6 +140,10 @@ pub enum ForkChoiceError { #[error("payload attestation validator not in PTC")] PayloadAttestationValidatorNotInPtc, + /// Payload attestation participant list is full. + #[error("payload attestation participant list is full")] + PayloadAttestationParticipantsFull, + /// Payload attestation slot does not match the store's current slot. #[error("payload attestation slot does not match current slot")] PayloadAttestationWrongSlot, @@ -170,6 +209,10 @@ pub enum ForkChoiceError { /// Merkle proof or branch check failed during fork-choice evaluation. #[error(transparent)] Merkle(#[from] MerkleError), + + /// KZG trusted setup could not be loaded. + #[error(transparent)] + KzgSetup(#[from] SetupFileError), } /// A broken structural invariant of the fork-choice store, found by @@ -198,5 +241,5 @@ pub enum StoreInvariant { /// A block does not sit in a later slot than its in-store parent. #[error("block {0:?} does not sit after its parent in slot order")] - NonDecreasingParentSlot(Root), + BlockNotAfterParent(Root), } diff --git a/moonglass/src/error/merkle.rs b/moonglass-core/src/error/merkle.rs similarity index 100% rename from moonglass/src/error/merkle.rs rename to moonglass-core/src/error/merkle.rs diff --git a/moonglass/src/error/operation.rs b/moonglass-core/src/error/operation.rs similarity index 92% rename from moonglass/src/error/operation.rs rename to moonglass-core/src/error/operation.rs index a4d55a2..c70831f 100644 --- a/moonglass/src/error/operation.rs +++ b/moonglass-core/src/error/operation.rs @@ -89,18 +89,6 @@ pub enum OperationError { /// Proposer slashing targets a non-slashable validator. #[error("proposer slashing target validator {0} not slashable")] ProposerSlashingNotSlashable(ValidatorIndex), - /// Deposit Merkle proof did not verify against `eth1_data.deposit_root`. - #[error("deposit merkle proof invalid")] - DepositMerkleInvalid, - /// Deposit request did not pass per-request validation. - #[error("deposit request invalid")] - DepositRequestInvalid, - /// Withdrawal request did not pass per-request validation. - #[error("withdrawal request invalid")] - WithdrawalRequestInvalid, - /// Consolidation request did not pass per-request validation. - #[error("consolidation request invalid")] - ConsolidationRequestInvalid, /// Payload attestation slot does not match the parent slot. #[error("payload attestation slot mismatch")] PayloadAttestationSlotMismatch, diff --git a/moonglass/src/error/primitive.rs b/moonglass-core/src/error/primitive.rs similarity index 100% rename from moonglass/src/error/primitive.rs rename to moonglass-core/src/error/primitive.rs diff --git a/moonglass/src/error/registry.rs b/moonglass-core/src/error/registry.rs similarity index 100% rename from moonglass/src/error/registry.rs rename to moonglass-core/src/error/registry.rs diff --git a/moonglass/src/error/signature.rs b/moonglass-core/src/error/signature.rs similarity index 100% rename from moonglass/src/error/signature.rs rename to moonglass-core/src/error/signature.rs diff --git a/moonglass/src/error/slot.rs b/moonglass-core/src/error/slot.rs similarity index 79% rename from moonglass/src/error/slot.rs rename to moonglass-core/src/error/slot.rs index a004b87..0aa105b 100644 --- a/moonglass/src/error/slot.rs +++ b/moonglass-core/src/error/slot.rs @@ -16,4 +16,8 @@ pub enum SlotError { /// Requested target slot. target: Slot, }, + + /// Advancing this slot to its next value overflowed. + #[error("slot {0} has no representable next slot")] + NextSlotOverflow(Slot), } diff --git a/moonglass-core/src/error/transition.rs b/moonglass-core/src/error/transition.rs new file mode 100644 index 0000000..7e5f5b9 --- /dev/null +++ b/moonglass-core/src/error/transition.rs @@ -0,0 +1,84 @@ +//! Shared state-transition failures used by more than one submodule. + +use thiserror::Error; + +use crate::primitives::{BuilderIndex, ValidatorIndex}; + +/// Arithmetic operation whose overflow makes a state transition invalid. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum TransitionArithmetic { + /// Slot arithmetic. + Slot, + /// Epoch arithmetic. + Epoch, + /// Churn budget arithmetic. + Churn, + /// Vote or reward weight arithmetic. + Weight, + /// Withdrawal index arithmetic. + WithdrawalIndex, + /// Balance aggregation arithmetic. + BalanceSum, + /// Bounded-list length arithmetic. + BoundedListLength, +} + +/// Bounded consensus lists that can reject an append at capacity. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BoundedList { + /// Validator registry. + Validators, + /// Validator balances. + Balances, + /// Previous-epoch participation flags. + PreviousEpochParticipation, + /// Current-epoch participation flags. + CurrentEpochParticipation, + /// Validator inactivity scores. + InactivityScores, + /// Historical summaries. + HistoricalSummaries, + /// Builder registry. + Builders, + /// Indexed beacon-attestation participants. + IndexedAttestationIndices, + /// Indexed payload-attestation participants. + IndexedPayloadAttestationIndices, + /// Pending deposits queue. + PendingDeposits, + /// Pending partial withdrawals queue. + PendingPartialWithdrawals, + /// Pending consolidations queue. + PendingConsolidations, + /// Builder pending withdrawals queue. + BuilderPendingWithdrawals, + /// Expected withdrawals carried by a payload. + PayloadExpectedWithdrawals, +} + +/// State shape that must hold before a transition helper can operate. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +#[non_exhaustive] +pub enum StateTransitionInvariant { + /// A validator index has no matching balance entry. + #[error("validator {0} has no balance entry")] + MissingBalance(ValidatorIndex), + + /// A validator index has no previous-epoch participation entry. + #[error("validator {0} has no previous-epoch participation entry")] + MissingPreviousEpochParticipation(ValidatorIndex), + + /// A validator index has no current-epoch participation entry. + #[error("validator {0} has no current-epoch participation entry")] + MissingCurrentEpochParticipation(ValidatorIndex), + + /// A validator index has no inactivity-score entry. + #[error("validator {0} has no inactivity-score entry")] + MissingInactivityScore(ValidatorIndex), + + /// A builder index has no matching registry entry. + #[error("builder {0} has no registry entry")] + MissingBuilder(BuilderIndex), +} diff --git a/moonglass/src/fork_choice.rs b/moonglass-core/src/fork_choice.rs similarity index 83% rename from moonglass/src/fork_choice.rs rename to moonglass-core/src/fork_choice.rs index 72ca914..768f18f 100644 --- a/moonglass/src/fork_choice.rs +++ b/moonglass-core/src/fork_choice.rs @@ -17,12 +17,11 @@ //! once votes and a recorded envelope decide. Weight, votes, and ancestry all //! range over these nodes, not over bare block roots. Everything else follows: //! -//! - [`Store`] holds this node's local evidence: known blocks and post-states, -//! latest votes, recorded payload envelopes, and PTC vote vectors. -//! - Handlers fold each incoming message into the store. -//! - [`get_head`](Store::get_head) walks the store from the justified root, -//! expands every block into its branch nodes, and keeps the heaviest node at -//! each step. +//! [`Store`] holds this node's local evidence: known blocks and post-states, +//! latest votes, recorded payload envelopes, and PTC vote vectors. Handlers fold +//! each incoming message into the store. [`get_head`](Store::get_head) walks the +//! store from the justified root, expands every block into its branch nodes, and +//! keeps the heaviest node at each step. //! //! # Reading order //! @@ -38,6 +37,8 @@ //! [`on_execution_payload_envelope()`](Store::on_execution_payload_envelope) //! records a delivered payload, [`on_attestation()`](Store::on_attestation) //! records latest votes, +//! [`on_attester_slashing()`](Store::on_attester_slashing) records +//! equivocating validators, //! [`on_payload_attestation_message()`](Store::on_payload_attestation_message) //! records PTC votes, and [`on_tick()`](Store::on_tick) advances the clock. //! 4. Selection reads the store: [`filter`] keeps only viable branches, @@ -53,13 +54,10 @@ //! //! One boundary is worth stating up front. Beyond the consensus-side checks, a //! complete verdict needs a data-availability check and an execution-engine -//! check, but in -//! [`on_execution_payload_envelope()`](Store::on_execution_payload_envelope) both -//! are mocked and always pass (a TODO). So today a recorded payload means only -//! that the consensus-side checks passed (the signature, and that the payload -//! matches the committed bid, slot, parent hash, timestamp, and withdrawals). -//! That is enough for fork choice to open a block's full branch, but it is not -//! yet a complete validity verdict. +//! check. The data-availability result is supplied to the store by the caller, +//! while execution-engine validity is still represented by a local hook. A +//! recorded payload therefore means the data was locally available, the +//! consensus-side checks passed, and the current execution hook accepted it. //! //! # A block's payload, end to end //! @@ -83,11 +81,12 @@ //! 6. [`get_head`](Store::get_head) reads all of this and returns the winning //! block-and-branch node. Later, when a child block builds on `B`'s payload, //! it proves and applies that payload's commitment to its own state through -//! [`accept_parent_payload_commitment`](crate::containers::BeaconState::accept_parent_payload_commitment). +//! [`process_parent_execution_payload`](crate::containers::BeaconState::process_parent_execution_payload). #![allow(clippy::must_use_candidate)] pub mod checkpoints; +pub mod fast_confirmation; pub mod filter; pub mod head; pub mod helpers; diff --git a/moonglass/src/fork_choice/checkpoints.rs b/moonglass-core/src/fork_choice/checkpoints.rs similarity index 100% rename from moonglass/src/fork_choice/checkpoints.rs rename to moonglass-core/src/fork_choice/checkpoints.rs diff --git a/moonglass-core/src/fork_choice/fast_confirmation.rs b/moonglass-core/src/fork_choice/fast_confirmation.rs new file mode 100644 index 0000000..65fc772 --- /dev/null +++ b/moonglass-core/src/fork_choice/fast_confirmation.rs @@ -0,0 +1,44 @@ +//! Helpers shared with the fast-confirmation rule. +//! +//! The full confirmation store tracks epoch snapshots and the latest confirmed +//! root outside ordinary head selection. These helpers keep the fork-choice node +//! shape and safe execution hash rule available without adding that larger state +//! machine here. +//! +//! The rule itself is deferred, so these helpers have no caller yet and their +//! reference-test runner is currently unsupported. They are kept ready for when +//! the confirmation store lands rather than removed. + +use crate::error::ForkChoiceError; +use crate::primitives::{Hash32, Root}; + +use super::store::{ForkChoiceNode, Store}; + +impl Store { + /// Return the fork-choice node corresponding to `block_root`. + /// + /// Fast-confirmation ancestry checks start from the pending branch for a + /// block root, matching the fork-choice tree's unresolved node shape. + pub fn get_node_for_root(&self, block_root: Root) -> ForkChoiceNode { + ForkChoiceNode::pending(block_root) + } + + /// Return the execution block hash considered safe for `confirmed_root`. + /// + /// The confirmed beacon block makes its parent payload safe, so the hash comes + /// from the confirmed block's bid rather than from the delivered payload map. + pub fn get_safe_execution_block_hash( + &self, + confirmed_root: Root, + ) -> Result { + let safe_block = self + .blocks + .get(&confirmed_root) + .ok_or(ForkChoiceError::UnknownBlock(confirmed_root))?; + Ok(safe_block + .body + .signed_execution_payload_bid + .message + .parent_block_hash) + } +} diff --git a/moonglass/src/fork_choice/filter.rs b/moonglass-core/src/fork_choice/filter.rs similarity index 100% rename from moonglass/src/fork_choice/filter.rs rename to moonglass-core/src/fork_choice/filter.rs diff --git a/moonglass/src/fork_choice/head.rs b/moonglass-core/src/fork_choice/head.rs similarity index 100% rename from moonglass/src/fork_choice/head.rs rename to moonglass-core/src/fork_choice/head.rs diff --git a/moonglass/src/fork_choice/helpers.rs b/moonglass-core/src/fork_choice/helpers.rs similarity index 93% rename from moonglass/src/fork_choice/helpers.rs rename to moonglass-core/src/fork_choice/helpers.rs index ee6d213..1bc9f29 100644 --- a/moonglass/src/fork_choice/helpers.rs +++ b/moonglass-core/src/fork_choice/helpers.rs @@ -8,8 +8,11 @@ //! fractions of a slot into millisecond deadlines. Each is tiny on its own. The //! point is that the rest of the module can treat them as settled vocabulary. -use crate::constants::{SLOT_DURATION_MS, SLOTS_PER_EPOCH}; -use crate::containers::BeaconState; +use crate::constants::{ + ATTESTATION_DUE_BPS_GLOAS, BASIS_POINTS, MIN_SEED_LOOKAHEAD, PAYLOAD_ATTESTATION_DUE_BPS, + SLOT_DURATION_MS, SLOTS_PER_EPOCH, +}; +use crate::containers::{BeaconState, Checkpoint}; use crate::error::ForkChoiceError; use crate::primitives::{Epoch, Gwei, Root, Slot}; @@ -178,10 +181,7 @@ impl Store { /// If it is from the current epoch, we use its own post-state's justified /// checkpoint. Fork-choice filtering uses this to drop branches that vote from /// a stale source. - pub fn get_voting_source( - &self, - block_root: Root, - ) -> Result { + pub fn get_voting_source(&self, block_root: Root) -> Result { let block = self .blocks .get(&block_root) @@ -212,8 +212,7 @@ impl Store { /// block, so the zero root is returned. pub fn get_dependent_root(&self, root: Root) -> Result { let epoch = self.get_current_store_epoch(); - let min_seed_lookahead = - u64::try_from(crate::constants::MIN_SEED_LOOKAHEAD).unwrap_or(u64::MAX); + let min_seed_lookahead = u64::try_from(MIN_SEED_LOOKAHEAD).unwrap_or(u64::MAX); if epoch.as_u64() <= min_seed_lookahead { return Ok(Root::ZERO); } @@ -264,10 +263,13 @@ pub fn compute_slots_since_epoch_start(slot: Slot) -> u64 { /// the stake of a single slot's committee, not the whole validator set. This /// divides total active balance by the number of slots in an epoch to get one /// committee's worth, then takes `committee_percent` of that. -pub fn calculate_committee_fraction(state: &BeaconState, committee_percent: u64) -> Gwei { +pub fn calculate_committee_fraction( + state: &BeaconState, + committee_percent: u64, +) -> Result { let slots_per_epoch = u64::try_from(SLOTS_PER_EPOCH).unwrap_or(u64::MAX); - let committee_weight = state.total_active_balance() / slots_per_epoch; - committee_weight * committee_percent / 100 + let committee_weight = state.get_total_active_balance()? / slots_per_epoch; + Ok(committee_weight * committee_percent / 100) } /// Multiply seconds by 1000, saturating at `u64::MAX` instead of overflowing. @@ -281,16 +283,16 @@ pub fn seconds_to_milliseconds(seconds: u64) -> u64 { /// are configured as basis points, where 10000 means the whole slot. This turns /// that fraction into a concrete millisecond offset from the slot's start. pub fn get_slot_component_duration_ms(basis_points: u64) -> u64 { - basis_points * crate::constants::SLOT_DURATION_MS / crate::constants::BASIS_POINTS + basis_points * SLOT_DURATION_MS / BASIS_POINTS } /// How long into a slot an attestation still counts as on time, in milliseconds. pub fn get_attestation_due_ms() -> u64 { - get_slot_component_duration_ms(crate::constants::ATTESTATION_DUE_BPS_GLOAS) + get_slot_component_duration_ms(ATTESTATION_DUE_BPS_GLOAS) } /// How long into a slot a payload attestation still counts as on time, in /// milliseconds. pub fn get_payload_attestation_due_ms() -> u64 { - get_slot_component_duration_ms(crate::constants::PAYLOAD_ATTESTATION_DUE_BPS) + get_slot_component_duration_ms(PAYLOAD_ATTESTATION_DUE_BPS) } diff --git a/moonglass/src/fork_choice/on_attestation.rs b/moonglass-core/src/fork_choice/on_attestation.rs similarity index 100% rename from moonglass/src/fork_choice/on_attestation.rs rename to moonglass-core/src/fork_choice/on_attestation.rs diff --git a/moonglass/src/fork_choice/on_attester_slashing.rs b/moonglass-core/src/fork_choice/on_attester_slashing.rs similarity index 86% rename from moonglass/src/fork_choice/on_attester_slashing.rs rename to moonglass-core/src/fork_choice/on_attester_slashing.rs index 4fbc42e..a2b9b02 100644 --- a/moonglass/src/fork_choice/on_attester_slashing.rs +++ b/moonglass-core/src/fork_choice/on_attester_slashing.rs @@ -7,8 +7,11 @@ //! in its `equivocating_indices` set so that, from then on, that validator's //! votes are ignored when weighing the chain. +use std::collections::BTreeSet; + use crate::containers::AttesterSlashing; use crate::error::{ForkChoiceError, SignatureError}; +use crate::state_transition::is_slashable_attestation_data; use super::store::Store; @@ -28,7 +31,7 @@ impl Store { let a1 = &attester_slashing.attestation_1; let a2 = &attester_slashing.attestation_2; - if !crate::state_transition::is_slashable_attestation_data(&a1.data, &a2.data) { + if !is_slashable_attestation_data(&a1.data, &a2.data) { return Err(ForkChoiceError::InvalidAttesterSlashing); } @@ -43,8 +46,8 @@ impl Store { .validate_indexed_attestation(a2, SignatureError::AttesterSlashingAttestationTwo) .map_err(|_| ForkChoiceError::InvalidAttesterSlashing)?; - let a1_set: std::collections::BTreeSet<_> = a1.attesting_indices.iter().copied().collect(); - let a2_set: std::collections::BTreeSet<_> = a2.attesting_indices.iter().copied().collect(); + let a1_set: BTreeSet<_> = a1.attesting_indices.iter().copied().collect(); + let a2_set: BTreeSet<_> = a2.attesting_indices.iter().copied().collect(); self.equivocating_indices .extend(a1_set.intersection(&a2_set).copied()); Ok(()) diff --git a/moonglass/src/fork_choice/on_block.rs b/moonglass-core/src/fork_choice/on_block.rs similarity index 96% rename from moonglass/src/fork_choice/on_block.rs rename to moonglass-core/src/fork_choice/on_block.rs index a235c6d..629a9f1 100644 --- a/moonglass/src/fork_choice/on_block.rs +++ b/moonglass-core/src/fork_choice/on_block.rs @@ -84,9 +84,8 @@ impl Store { // Phase 2: run the state transition to derive the block's post-state. let mut state = parent_state.clone(); - let mut block_clone = block.clone(); - let block_root = block_clone.tree_root(MerkleError::BeaconBlock)?; - state.apply_signed_block(signed_block)?; + let block_root = block.tree_root(MerkleError::BeaconBlock)?; + state.state_transition(signed_block)?; // The head as of before this block is imported, used by proposer-boost // selection in phase 5. @@ -165,7 +164,7 @@ impl Store { // signature the block already verified, and records under the aggregate's // target root, not the containing block root. for attestation in payload_attestations { - let indexed = state.indexed_payload_attestation(attestation.data.slot, attestation)?; + let indexed = state.get_indexed_payload_attestation(attestation)?; for validator_index in indexed.attesting_indices.iter() { let message = PayloadAttestationMessage { validator_index: *validator_index, diff --git a/moonglass-core/src/fork_choice/on_execution_payload_envelope.rs b/moonglass-core/src/fork_choice/on_execution_payload_envelope.rs new file mode 100644 index 0000000..1474279 --- /dev/null +++ b/moonglass-core/src/fork_choice/on_execution_payload_envelope.rs @@ -0,0 +1,433 @@ +//! Taking in a delivered execution payload. +//! +//! When a [block's](crate::glossary#beacon-block) payload finally arrives, +//! carried in a signed envelope, this handler checks it and records it. +//! Recording it is what lets the block's *full* branch appear in the fork-choice +//! tree. Three kinds of check run: the consensus-side ones (the signature, and +//! that the payload matches the bid, [slot](crate::glossary#slot), parent hash, +//! timestamp, and withdrawals), a data-availability check, and an +//! execution-engine check. Data availability is derived from recorded column +//! sidecars. Execution-engine validity is supplied by the caller through the +//! [`ExecutionPayloadVerifier`] seam. +use std::sync::OnceLock; + +use crate::constants::{MAX_BLOB_COMMITMENTS_PER_BLOCK, NUMBER_OF_COLUMNS}; +use crate::containers::{ + DataColumnSidecar, SignedExecutionPayloadEnvelope, verify_data_column_sidecar, + verify_data_column_sidecar_kzg_proofs, +}; +use crate::crypto::kzg::{EthereumKzgSetup, SetupFileError}; +use crate::error::ForkChoiceError; +use crate::primitives::{KZGCommitment, Root}; +use crate::ssz::List; + +use super::store::Store; + +/// Execution payload verdict supplied by the caller. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExecutionPayloadValidity { + /// The payload is accepted. + Valid, + /// The payload is rejected. + Invalid, +} + +/// External execution verifier used by the core fork-choice path. +pub trait ExecutionPayloadVerifier { + /// Return the execution verdict for a delivered payload envelope. + fn verify_and_notify_new_payload( + &self, + signed_envelope: &SignedExecutionPayloadEnvelope, + ) -> ExecutionPayloadValidity; +} + +/// Verifier used by reference-test paths that do not model execution. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct AcceptAllExecutionPayloadVerifier; + +impl ExecutionPayloadVerifier for AcceptAllExecutionPayloadVerifier { + fn verify_and_notify_new_payload( + &self, + _signed_envelope: &SignedExecutionPayloadEnvelope, + ) -> ExecutionPayloadValidity { + ExecutionPayloadValidity::Valid + } +} + +impl Store { + /// Check a delivered payload against its block and record it in the store. + /// + /// In order: confirm the block is known, that the envelope verifies against + /// the block's state + /// ([`verify_execution_payload_envelope`](crate::containers::BeaconState::verify_execution_payload_envelope), + /// run on a throwaway copy), and that the execution engine accepts it + /// ([`ExecutionPayloadVerifier::verify_and_notify_new_payload`]). If data is + /// not yet available, the + /// verified envelope is queued. Once [`Self::is_data_available`] succeeds, + /// the envelope is filed in [`Store::payloads`](super::store::Store::payloads). + /// A payload's lasting effects are applied later, when a child block builds + /// on it. + pub fn on_execution_payload_envelope( + &mut self, + signed_envelope: &SignedExecutionPayloadEnvelope, + ) -> Result<(), ForkChoiceError> { + self.on_execution_payload_envelope_with_verifier( + signed_envelope, + &AcceptAllExecutionPayloadVerifier, + ) + } + + /// Check and record a delivered payload using `verifier`. + pub fn on_execution_payload_envelope_with_verifier( + &mut self, + signed_envelope: &SignedExecutionPayloadEnvelope, + verifier: &V, + ) -> Result<(), ForkChoiceError> + where + V: ExecutionPayloadVerifier, + { + let envelope = &signed_envelope.message; + let beacon_block_root = envelope.beacon_block_root; + if !self.block_states.contains_key(&beacon_block_root) { + return Err(ForkChoiceError::PayloadEnvelopeForUnknownBlock( + beacon_block_root, + )); + } + self.verify_execution_payload_envelope_consensus(signed_envelope)?; + if !self.is_data_available(beacon_block_root)? { + self.queued_payload_envelopes + .insert(beacon_block_root, signed_envelope.clone()); + return Ok(()); + } + self.verify_execution_payload_envelope_execution(signed_envelope, verifier)?; + self.record_verified_execution_payload_envelope(signed_envelope); + Ok(()) + } + + /// Verify an available payload envelope and record its payload. + pub fn verify_and_record_execution_payload_envelope( + &mut self, + signed_envelope: &SignedExecutionPayloadEnvelope, + ) -> Result<(), ForkChoiceError> { + self.verify_and_record_execution_payload_envelope_with_verifier( + signed_envelope, + &AcceptAllExecutionPayloadVerifier, + ) + } + + /// Verify an available payload envelope with `verifier` and record it. + pub fn verify_and_record_execution_payload_envelope_with_verifier( + &mut self, + signed_envelope: &SignedExecutionPayloadEnvelope, + verifier: &V, + ) -> Result<(), ForkChoiceError> + where + V: ExecutionPayloadVerifier, + { + self.verify_execution_payload_envelope_with_verifier(signed_envelope, verifier)?; + self.record_verified_execution_payload_envelope(signed_envelope); + Ok(()) + } + + /// Verify consensus and execution checks for `signed_envelope`. + pub fn verify_execution_payload_envelope_with_verifier( + &self, + signed_envelope: &SignedExecutionPayloadEnvelope, + verifier: &V, + ) -> Result<(), ForkChoiceError> + where + V: ExecutionPayloadVerifier, + { + self.verify_execution_payload_envelope_consensus(signed_envelope)?; + self.verify_execution_payload_envelope_execution(signed_envelope, verifier) + } + + /// Verify the consensus-side envelope checks. + pub fn verify_execution_payload_envelope_consensus( + &self, + signed_envelope: &SignedExecutionPayloadEnvelope, + ) -> Result<(), ForkChoiceError> { + let envelope = &signed_envelope.message; + let beacon_block_root = envelope.beacon_block_root; + let block_state = self.block_states.get(&beacon_block_root).ok_or( + ForkChoiceError::PayloadEnvelopeForUnknownBlock(beacon_block_root), + )?; + block_state.verify_execution_payload_envelope(signed_envelope)?; + Ok(()) + } + + /// Verify the execution engine verdict for an envelope. + pub fn verify_execution_payload_envelope_execution( + &self, + signed_envelope: &SignedExecutionPayloadEnvelope, + verifier: &V, + ) -> Result<(), ForkChoiceError> + where + V: ExecutionPayloadVerifier, + { + let beacon_block_root = signed_envelope.message.beacon_block_root; + if verifier.verify_and_notify_new_payload(signed_envelope) + == ExecutionPayloadValidity::Invalid + { + return Err(ForkChoiceError::PayloadExecutionInvalid(beacon_block_root)); + } + Ok(()) + } + + /// Record an envelope whose consensus and execution checks already passed. + pub fn record_verified_execution_payload_envelope( + &mut self, + signed_envelope: &SignedExecutionPayloadEnvelope, + ) { + let envelope = &signed_envelope.message; + let beacon_block_root = envelope.beacon_block_root; + self.payloads.insert(beacon_block_root, envelope.clone()); + self.queued_payload_envelopes.remove(&beacon_block_root); + } + + /// Process a queued payload envelope once data is available. + pub fn process_queued_payload_envelope( + &mut self, + beacon_block_root: Root, + ) -> Result { + let setup = default_kzg_setup()?; + self.process_queued_payload_envelope_with_setup(beacon_block_root, setup) + } + + /// Process a queued payload envelope once data is available. + pub fn process_queued_payload_envelope_with_verifier( + &mut self, + beacon_block_root: Root, + verifier: &V, + ) -> Result + where + V: ExecutionPayloadVerifier, + { + let setup = default_kzg_setup()?; + self.process_queued_payload_envelope_with_setup_and_verifier( + beacon_block_root, + setup, + verifier, + ) + } + + /// Process a queued payload envelope with the supplied KZG setup. + pub fn process_queued_payload_envelope_with_setup( + &mut self, + beacon_block_root: Root, + setup: &EthereumKzgSetup, + ) -> Result { + self.process_queued_payload_envelope_with_setup_and_verifier( + beacon_block_root, + setup, + &AcceptAllExecutionPayloadVerifier, + ) + } + + /// Process a queued payload envelope with supplied setup and verifier. + pub fn process_queued_payload_envelope_with_setup_and_verifier( + &mut self, + beacon_block_root: Root, + setup: &EthereumKzgSetup, + verifier: &V, + ) -> Result + where + V: ExecutionPayloadVerifier, + { + if !self + .queued_payload_envelopes + .contains_key(&beacon_block_root) + { + return Ok(false); + } + if !self.is_data_available_with_setup(beacon_block_root, setup) { + return Ok(false); + } + let Some(signed_envelope) = self + .queued_payload_envelopes + .get(&beacon_block_root) + .cloned() + else { + return Ok(false); + }; + self.verify_and_record_execution_payload_envelope_with_verifier( + &signed_envelope, + verifier, + )?; + Ok(true) + } + + /// Record a data column sidecar for later availability checks. + pub fn record_data_column_sidecar( + &mut self, + sidecar: DataColumnSidecar, + ) -> Result<(), ForkChoiceError> { + let setup = default_kzg_setup()?; + self.record_data_column_sidecar_with_setup(sidecar, setup) + } + + /// Record a data column sidecar and process any released payload. + pub fn record_data_column_sidecar_with_verifier( + &mut self, + sidecar: DataColumnSidecar, + verifier: &V, + ) -> Result<(), ForkChoiceError> + where + V: ExecutionPayloadVerifier, + { + let setup = default_kzg_setup()?; + self.record_data_column_sidecar_with_setup_and_verifier(sidecar, setup, verifier) + } + + /// Record a data column sidecar after validating it with the supplied setup. + pub fn record_data_column_sidecar_with_setup( + &mut self, + sidecar: DataColumnSidecar, + setup: &EthereumKzgSetup, + ) -> Result<(), ForkChoiceError> { + self.record_data_column_sidecar_with_setup_and_verifier( + sidecar, + setup, + &AcceptAllExecutionPayloadVerifier, + ) + } + + /// Record a data column sidecar with supplied setup and verifier. + pub fn record_data_column_sidecar_with_setup_and_verifier( + &mut self, + sidecar: DataColumnSidecar, + setup: &EthereumKzgSetup, + verifier: &V, + ) -> Result<(), ForkChoiceError> + where + V: ExecutionPayloadVerifier, + { + let beacon_block_root = sidecar.beacon_block_root; + let block = self + .blocks + .get(&beacon_block_root) + .ok_or(ForkChoiceError::UnknownBlock(beacon_block_root))?; + if sidecar.slot != block.slot { + return Err(ForkChoiceError::DataColumnSidecarSlotMismatch { + sidecar_slot: sidecar.slot, + block_slot: block.slot, + }); + } + + let kzg_commitments = &block + .body + .signed_execution_payload_bid + .message + .blob_kzg_commitments; + validate_data_column_sidecar(&sidecar, kzg_commitments, setup)?; + + let column_index = sidecar.index; + let sidecars = self + .data_column_sidecars + .entry(beacon_block_root) + .or_default(); + if let Some(stored) = sidecars + .iter_mut() + .find(|stored| stored.index == column_index) + { + *stored = sidecar; + } else { + sidecars.push(sidecar); + } + sidecars.sort_by_key(|stored| stored.index.as_u64()); + // Completing the column set may release a queued envelope. The queued + // path already checks data availability, so the guard is left to it + // rather than verifying every column's proof a second time here. + self.process_queued_payload_envelope_with_setup_and_verifier( + beacon_block_root, + setup, + verifier, + )?; + Ok(()) + } + + /// Whether the block's payload data is available under the default setup. + pub fn is_data_available(&self, beacon_block_root: Root) -> Result { + let setup = default_kzg_setup()?; + Ok(self.is_data_available_with_setup(beacon_block_root, setup)) + } + + /// Whether the block's payload data is available under a supplied setup. + pub fn is_data_available_with_setup( + &self, + beacon_block_root: Root, + setup: &EthereumKzgSetup, + ) -> bool { + let Some(block) = self.blocks.get(&beacon_block_root) else { + return false; + }; + let kzg_commitments = &block + .body + .signed_execution_payload_bid + .message + .blob_kzg_commitments; + if kzg_commitments.is_empty() { + return true; + } + let Some(column_sidecars) = self.data_column_sidecars.get(&beacon_block_root) else { + return false; + }; + if !has_complete_column_set(column_sidecars) { + return false; + } + column_sidecars.iter().all(|column_sidecar| { + column_sidecar.beacon_block_root == beacon_block_root + && column_sidecar.slot == block.slot + && verify_data_column_sidecar(column_sidecar, kzg_commitments) + && verify_data_column_sidecar_kzg_proofs(column_sidecar, kzg_commitments, setup) + .unwrap_or(false) + }) + } +} + +/// Return the cached default Ethereum KZG setup. +pub fn default_kzg_setup() -> Result<&'static EthereumKzgSetup, SetupFileError> { + static SETUP: OnceLock> = OnceLock::new(); + SETUP + .get_or_init(EthereumKzgSetup::mainnet) + .as_ref() + .map_err(Clone::clone) +} + +/// Validate a data-column sidecar against block commitments and KZG setup. +pub fn validate_data_column_sidecar( + sidecar: &DataColumnSidecar, + kzg_commitments: &List, + setup: &EthereumKzgSetup, +) -> Result<(), ForkChoiceError> { + let block = sidecar.beacon_block_root; + let column = sidecar.index; + if !verify_data_column_sidecar(sidecar, kzg_commitments) { + return Err(ForkChoiceError::DataColumnSidecarInvalid { block, column }); + } + match verify_data_column_sidecar_kzg_proofs(sidecar, kzg_commitments, setup) { + Ok(true) => Ok(()), + Ok(false) => Err(ForkChoiceError::DataColumnSidecarProofInvalid { block, column }), + Err(source) => Err(ForkChoiceError::DataColumnSidecarProofError { + block, + column, + source, + }), + } +} + +/// Return whether sidecars contain every column index exactly once. +pub fn has_complete_column_set(sidecars: &[DataColumnSidecar]) -> bool { + if sidecars.len() != NUMBER_OF_COLUMNS { + return false; + } + let mut seen = [false; NUMBER_OF_COLUMNS]; + for sidecar in sidecars { + let index = sidecar.index.as_usize(); + if index >= NUMBER_OF_COLUMNS || seen[index] { + return false; + } + seen[index] = true; + } + true +} diff --git a/moonglass/src/fork_choice/on_payload_attestation_message.rs b/moonglass-core/src/fork_choice/on_payload_attestation_message.rs similarity index 94% rename from moonglass/src/fork_choice/on_payload_attestation_message.rs rename to moonglass-core/src/fork_choice/on_payload_attestation_message.rs index c13dca6..a02d278 100644 --- a/moonglass/src/fork_choice/on_payload_attestation_message.rs +++ b/moonglass-core/src/fork_choice/on_payload_attestation_message.rs @@ -14,6 +14,7 @@ use crate::constants::PTC_SIZE; use crate::containers::{BeaconState, IndexedPayloadAttestation, PayloadAttestationMessage}; use crate::error::ForkChoiceError; use crate::primitives::{Root, Slot, ValidatorIndex}; +use crate::ssz::List; use super::store::Store; @@ -75,7 +76,7 @@ impl Store { if data.slot != self.get_current_slot() { return Err(ForkChoiceError::PayloadAttestationWrongSlot); } - let indexed = build_single_indexed(ptc_message); + let indexed = build_single_indexed(ptc_message)?; state.validate_indexed_payload_attestation(&indexed)?; } @@ -182,12 +183,16 @@ pub fn write_vote_positions( /// /// Only the gossip path needs this. A vote that arrived inside a block was /// already verified as part of the block. -pub fn build_single_indexed(msg: &PayloadAttestationMessage) -> IndexedPayloadAttestation { - let mut indices = ssz_rs::List::::default(); - indices.push(msg.validator_index); - IndexedPayloadAttestation { +pub fn build_single_indexed( + msg: &PayloadAttestationMessage, +) -> Result { + let mut indices = List::::default(); + indices + .push(msg.validator_index) + .map_err(|_| ForkChoiceError::PayloadAttestationParticipantsFull)?; + Ok(IndexedPayloadAttestation { attesting_indices: indices, data: msg.data, signature: msg.signature, - } + }) } diff --git a/moonglass/src/fork_choice/on_tick.rs b/moonglass-core/src/fork_choice/on_tick.rs similarity index 100% rename from moonglass/src/fork_choice/on_tick.rs rename to moonglass-core/src/fork_choice/on_tick.rs diff --git a/moonglass/src/fork_choice/payload_status.rs b/moonglass-core/src/fork_choice/payload_status.rs similarity index 90% rename from moonglass/src/fork_choice/payload_status.rs rename to moonglass-core/src/fork_choice/payload_status.rs index 099cf1d..848a6a9 100644 --- a/moonglass/src/fork_choice/payload_status.rs +++ b/moonglass-core/src/fork_choice/payload_status.rs @@ -10,13 +10,12 @@ //! *envelope*. Because that payload can be late or never arrive, the same block //! can end up in one of three shapes, which we call its payload status: //! -//! - [`Empty`](PayloadStatus::Empty): the branch that carries the chain forward -//! without this block's payload, as if this [slot](crate::glossary#slot) -//! carried no transactions. -//! - [`Full`](PayloadStatus::Full): the branch that carries the chain forward on -//! top of this block's payload, once that payload has been delivered. -//! - [`Pending`](PayloadStatus::Pending): not decided yet, because the votes -//! that settle empty versus full have not arrived. +//! [`Empty`](PayloadStatus::Empty) is the branch that carries the chain forward +//! without this block's payload, as if this [slot](crate::glossary#slot) carried +//! no transactions. [`Full`](PayloadStatus::Full) is the branch that carries the +//! chain forward on top of this block's payload, once that payload has been +//! delivered. [`Pending`](PayloadStatus::Pending) is not decided yet, because the +//! votes that settle empty versus full have not arrived. //! //! Fork choice has to keep these outcomes apart, so a position in the //! fork-choice tree is a block root paired with one of these statuses (a @@ -27,10 +26,13 @@ //! its data available. //! [`Store::get_head`] and [`weight`](super::weight) build on every one of them. //! -//! Keep one boundary in mind throughout: recording a payload here means only the -//! consensus-side checks passed. A data-availability check and an execution-engine -//! check are mocked for now (a TODO), so a recorded payload is not yet a complete -//! validity verdict. +//! Keep one boundary in mind throughout: recording a payload here means the +//! consensus-side checks passed, required data-column sidecars were verified, +//! and the configured execution verifier accepted it. The default verifier used +//! by reference-test paths accepts every payload, so production callers should +//! supply an execution-engine adapter before treating the full branch as final +//! execution validity. +use crate::constants::{DATA_AVAILABILITY_TIMELY_THRESHOLD, PAYLOAD_TIMELY_THRESHOLD}; use crate::containers::BeaconBlock; use crate::error::ForkChoiceError; use crate::primitives::Root; @@ -105,10 +107,10 @@ impl Store { /// between a block that *might* become full and one that is now eligible to be /// treated as full. /// - /// "Verified" here means the consensus-side checks passed. The - /// data-availability and execution-engine checks are mocked in this - /// implementation and always pass, so it is not yet a full execution validity - /// verdict. + /// "Verified" here means the consensus-side checks passed, required + /// data-column sidecars were verified, and the configured execution verifier + /// accepted the payload. The default verifier accepts every payload for + /// reference-test paths. pub fn is_payload_verified(&self, root: Root) -> bool { self.payloads.contains_key(&root) } @@ -147,7 +149,7 @@ impl Store { /// it cannot be timely, so the only answer we support is the not-timely one. /// Once the payload is recorded, we count the committee votes matching the /// question and report whether they clear - /// [`PAYLOAD_TIMELY_THRESHOLD`](crate::constants::PAYLOAD_TIMELY_THRESHOLD), a + /// [`PAYLOAD_TIMELY_THRESHOLD`], a /// majority of the committee. The block must be known, otherwise /// [`ForkChoiceError::UnknownBlock`] is returned. pub fn payload_timeliness(&self, root: Root, timely: bool) -> Result { @@ -155,12 +157,7 @@ impl Store { .payload_timeliness_vote .get(&root) .ok_or(ForkChoiceError::UnknownBlock(root))?; - Ok(self.payload_vote_verdict( - root, - votes, - timely, - crate::constants::PAYLOAD_TIMELY_THRESHOLD, - )) + Ok(self.payload_vote_verdict(root, votes, timely, PAYLOAD_TIMELY_THRESHOLD)) } /// Do enough committee votes agree that this block's payload *data was @@ -176,7 +173,7 @@ impl Store { /// recorded the payload, the only supported answer is "not available". /// Otherwise we count the committee votes matching `available` and compare /// against - /// [`DATA_AVAILABILITY_TIMELY_THRESHOLD`](crate::constants::DATA_AVAILABILITY_TIMELY_THRESHOLD). + /// [`DATA_AVAILABILITY_TIMELY_THRESHOLD`]. /// This is a tally of votes, not a real proof that the data exists. pub fn payload_data_availability( &self, @@ -187,12 +184,7 @@ impl Store { .payload_data_availability_vote .get(&root) .ok_or(ForkChoiceError::UnknownBlock(root))?; - Ok(self.payload_vote_verdict( - root, - votes, - available, - crate::constants::DATA_AVAILABILITY_TIMELY_THRESHOLD, - )) + Ok(self.payload_vote_verdict(root, votes, available, DATA_AVAILABILITY_TIMELY_THRESHOLD)) } /// Is `node` an empty-or-full decision about the block from the slot just @@ -238,9 +230,8 @@ impl Store { /// direct child of `root` that chose `root`'s empty branch. Equivalently, we /// keep the full branch whenever any of these holds: /// - /// - no block is boosted this slot, - /// - the boosted block builds on something other than `root`, or - /// - that boosted child built on `root`'s full branch. + /// no block is boosted this slot, the boosted block builds on something other + /// than `root`, or that boosted child built on `root`'s full branch. /// /// `root` must be the previous slot's block, otherwise /// [`ForkChoiceError::NotPreviousSlot`] is returned (or @@ -325,7 +316,7 @@ impl Store { /// *full* payload branch rather than its *empty* one. /// /// `head` must be a resolved node, [`Empty`](PayloadStatus::Empty) or - /// [`Full`](PayloadStatus::Full); a still-[`Pending`](PayloadStatus::Pending) + /// [`Full`](PayloadStatus::Full). A still-[`Pending`](PayloadStatus::Pending) /// head has no branch to choose and is rejected. For a head from earlier than /// the previous slot the choice just follows its branch: build full exactly /// when the head is already full. For the previous slot's block the committee's diff --git a/moonglass/src/fork_choice/proposer_head.rs b/moonglass-core/src/fork_choice/proposer_head.rs similarity index 91% rename from moonglass/src/fork_choice/proposer_head.rs rename to moonglass-core/src/fork_choice/proposer_head.rs index 52577b6..0674858 100644 --- a/moonglass/src/fork_choice/proposer_head.rs +++ b/moonglass-core/src/fork_choice/proposer_head.rs @@ -27,12 +27,8 @@ pub fn get_proposer_reorg_cutoff_ms() -> u64 { get_slot_component_duration_ms(PROPOSER_REORG_CUTOFF_BPS) } -/// Is `slot` *not* the first slot of its epoch? -/// -/// Returns true for every slot except an epoch's first. Re-orging across an epoch -/// boundary is forbidden, since it could disturb finality, so -/// [`Store::get_proposer_head`] requires this to hold. -pub fn is_epoch_boundary(slot: Slot) -> bool { +/// Is `slot` after the first slot of its epoch? +pub fn is_after_epoch_boundary(slot: Slot) -> bool { let slots_per_epoch = u64::try_from(SLOTS_PER_EPOCH).unwrap_or(u64::MAX); !slot.as_u64().is_multiple_of(slots_per_epoch) } @@ -61,7 +57,7 @@ impl Store { pub fn is_parent_strong(&self, root: Root) -> Result { let justified_state = self.get_justified_state()?; let parent_threshold = - calculate_committee_fraction(justified_state, REORG_PARENT_WEIGHT_THRESHOLD); + calculate_committee_fraction(justified_state, REORG_PARENT_WEIGHT_THRESHOLD)?; let parent_root = self .blocks .get(&root) @@ -146,13 +142,6 @@ impl Store { /// and the epoch-boundary, FFG, and finalization checks all pass. The second /// is narrower: a weak head from the previous slot whose proposer equivocated. /// If neither route fires, the head is returned unchanged. - /// - /// # Errors - /// - /// Returns [`ForkChoiceError::ProposerBoostStillActive`] if proposer boost - /// still points at the head, since the boost must have worn off before this - /// decision, and propagates any missing block, state, or justification - /// lookups. pub fn get_proposer_head( &self, head_node: ForkChoiceNode, @@ -187,9 +176,7 @@ impl Store { let head_weak = self.is_head_weak(head_node.root)?; let parent_strong = self.is_parent_strong(head_node.root)?; let proposer_equivocation = self.is_proposer_equivocation(head_node.root)?; - // `is_epoch_boundary` is true when `slot` is NOT an epoch's first slot, so - // it doubles as "the re-org stays inside the epoch". - let reorg_stays_inside_epoch = is_epoch_boundary(slot); + let reorg_stays_inside_epoch = is_after_epoch_boundary(slot); // Group the primary-re-org guards by the reason each speaks to. let timing_allows_primary_reorg = diff --git a/moonglass/src/fork_choice/store.rs b/moonglass-core/src/fork_choice/store.rs similarity index 85% rename from moonglass/src/fork_choice/store.rs rename to moonglass-core/src/fork_choice/store.rs index 9915458..2d16e7a 100644 --- a/moonglass/src/fork_choice/store.rs +++ b/moonglass-core/src/fork_choice/store.rs @@ -11,30 +11,40 @@ //! //! # Where each field is written //! -//! - [`get_forkchoice_store`] seeds clock fields, -//! [checkpoints](crate::glossary#checkpoint), anchor block, anchor post-state, -//! checkpoint states, unrealized -//! [justification](crate::glossary#justification-and-finalization), and empty -//! vote/envelope maps. -//! - [`on_tick()`](Store::on_tick) advances `time`, resets -//! `proposer_boost_root` at [slot](crate::glossary#slot) boundaries, and -//! realizes pulled-up checkpoints at [epoch](crate::glossary#epoch) boundaries. -//! - [`on_block()`](Store::on_block) inserts `blocks`, `block_states`, block -//! timeliness, PTC vote vectors, proposer boost, realized checkpoints, -//! `unrealized_justifications`, and pulled-up checkpoint updates. -//! - [`on_attestation()`](Store::on_attestation) updates `checkpoint_states` -//! and `latest_messages`. -//! - [`on_attester_slashing()`](Store::on_attester_slashing) updates -//! `equivocating_indices`. -//! - [`on_execution_payload_envelope()`](Store::on_execution_payload_envelope) -//! inserts into `payloads` after the current consensus-side envelope checks -//! pass. -//! - [`on_payload_attestation_message()`](Store::on_payload_attestation_message) -//! updates PTC timeliness and data-availability vote vectors. +//! [`get_forkchoice_store`] seeds clock fields, +//! [checkpoints](crate::glossary#checkpoint), anchor block, anchor post-state, +//! checkpoint states, unrealized +//! [justification](crate::glossary#justification-and-finalization), and empty +//! vote/envelope maps. +//! +//! [`on_tick()`](Store::on_tick) advances `time`, resets `proposer_boost_root` at +//! [slot](crate::glossary#slot) boundaries, and realizes pulled-up checkpoints at +//! [epoch](crate::glossary#epoch) boundaries. +//! +//! [`on_block()`](Store::on_block) inserts `blocks`, `block_states`, block +//! timeliness, PTC vote vectors, proposer boost, realized checkpoints, +//! `unrealized_justifications`, and pulled-up checkpoint updates. +//! +//! [`on_attestation()`](Store::on_attestation) updates `checkpoint_states` and +//! `latest_messages`. +//! +//! [`on_attester_slashing()`](Store::on_attester_slashing) updates +//! `equivocating_indices`. +//! +//! [`on_execution_payload_envelope()`](Store::on_execution_payload_envelope) +//! queues unavailable envelopes or inserts into `payloads` after the current +//! consensus-side envelope checks pass. +//! +//! [`on_payload_attestation_message()`](Store::on_payload_attestation_message) +//! updates PTC timeliness and data-availability vote vectors. use std::collections::{BTreeSet, HashMap}; -use crate::containers::{BeaconBlock, BeaconState, Checkpoint, ExecutionPayloadEnvelope}; +use crate::constants::{PTC_SIZE, SLOT_DURATION_MS}; +use crate::containers::{ + BeaconBlock, BeaconState, Checkpoint, DataColumnSidecar, ExecutionPayloadEnvelope, + SignedExecutionPayloadEnvelope, +}; use crate::error::{ForkChoiceError, StoreInvariant}; use crate::primitives::{Root, Slot, ValidatorIndex}; @@ -67,17 +77,15 @@ impl LatestMessage { /// `payload_present`, otherwise empty. A vote about the block's own slot has /// not settled the branch, so it supports the pending node. pub fn supported_payload_status(&self, block_slot: Slot) -> PayloadStatus { - let supported_payload_status; if block_slot < self.slot { if self.payload_present { - supported_payload_status = PayloadStatus::Full; + PayloadStatus::Full } else { - supported_payload_status = PayloadStatus::Empty; + PayloadStatus::Empty } } else { - supported_payload_status = PayloadStatus::Pending; + PayloadStatus::Pending } - supported_payload_status } } @@ -222,6 +230,10 @@ pub struct Store { /// [`on_execution_payload_envelope()`](Store::on_execution_payload_envelope) /// after the current envelope checks. pub payloads: HashMap, + /// Signed payload envelopes waiting for data-column sidecars. + pub queued_payload_envelopes: HashMap, + /// Data-column sidecars recorded for each block root. + pub data_column_sidecars: HashMap>, /// Payload timeliness votes per block root, indexed by committee position. pub payload_timeliness_vote: HashMap, /// Payload data-availability votes per block root, indexed by committee position. @@ -238,10 +250,9 @@ pub struct Store { /// [`ForkChoiceError::AnchorStateRootMismatch`] if the block and state do not /// agree. pub fn get_forkchoice_store( - mut anchor_state: BeaconState, + anchor_state: &BeaconState, anchor_block: &BeaconBlock, ) -> Result { - use crate::constants::SLOT_DURATION_MS; use crate::error::MerkleError; use crate::state_transition::TreeRootExt as _; @@ -253,8 +264,7 @@ pub fn get_forkchoice_store( }); } - let mut anchor_block_clone = anchor_block.clone(); - let anchor_root = anchor_block_clone.tree_root(MerkleError::BeaconBlock)?; + let anchor_root = anchor_block.tree_root(MerkleError::BeaconBlock)?; let anchor_epoch = anchor_state.slot.epoch(); let justified = Checkpoint { epoch: anchor_epoch, @@ -305,6 +315,8 @@ pub fn get_forkchoice_store( latest_messages: HashMap::new(), unrealized_justifications, payloads: HashMap::new(), + queued_payload_envelopes: HashMap::new(), + data_column_sidecars: HashMap::new(), payload_timeliness_vote: HashMap::new(), payload_data_availability_vote: HashMap::new(), }) @@ -327,12 +339,12 @@ impl Store { } } for (root, votes) in &self.payload_timeliness_vote { - if votes.len() != crate::constants::PTC_SIZE { + if votes.len() != PTC_SIZE { return Err(StoreInvariant::TimelinessVotesNotPtcSize(*root)); } } for (root, votes) in &self.payload_data_availability_vote { - if votes.len() != crate::constants::PTC_SIZE { + if votes.len() != PTC_SIZE { return Err(StoreInvariant::DataAvailabilityVotesNotPtcSize(*root)); } } @@ -342,7 +354,7 @@ impl Store { .get(&block.parent_root) .is_some_and(|parent| parent.slot >= block.slot); if parent_out_of_order { - return Err(StoreInvariant::NonDecreasingParentSlot(*root)); + return Err(StoreInvariant::BlockNotAfterParent(*root)); } } Ok(()) diff --git a/moonglass/src/fork_choice/timeliness.rs b/moonglass-core/src/fork_choice/timeliness.rs similarity index 100% rename from moonglass/src/fork_choice/timeliness.rs rename to moonglass-core/src/fork_choice/timeliness.rs diff --git a/moonglass/src/fork_choice/weight.rs b/moonglass-core/src/fork_choice/weight.rs similarity index 98% rename from moonglass/src/fork_choice/weight.rs rename to moonglass-core/src/fork_choice/weight.rs index dd8159a..823b753 100644 --- a/moonglass/src/fork_choice/weight.rs +++ b/moonglass-core/src/fork_choice/weight.rs @@ -67,7 +67,7 @@ impl Store { /// against. pub fn get_proposer_score(&self) -> Result { let state = self.get_justified_state()?; - Ok(compute_proposer_score(state)) + compute_proposer_score(state) } /// Should the current proposer boost actually be counted right now? @@ -136,7 +136,7 @@ impl Store { pub fn is_head_weak(&self, head_root: Root) -> Result { let justified_state = self.get_justified_state()?; let reorg_threshold = - calculate_committee_fraction(justified_state, REORG_HEAD_WEIGHT_THRESHOLD); + calculate_committee_fraction(justified_state, REORG_HEAD_WEIGHT_THRESHOLD)?; let head_state = self .block_states @@ -217,7 +217,7 @@ impl Store { /// one slot committee's stake scaled by a fixed percentage. This computes that /// amount from a given state, and [`Store::get_head`] adds it only to nodes the /// boosted block descends from. -pub fn compute_proposer_score(state: &BeaconState) -> Gwei { +pub fn compute_proposer_score(state: &BeaconState) -> Result { calculate_committee_fraction(state, PROPOSER_SCORE_BOOST) } diff --git a/moonglass/src/glossary.rs b/moonglass-core/src/glossary.rs similarity index 87% rename from moonglass/src/glossary.rs rename to moonglass-core/src/glossary.rs index 472ddd3..a4f58af 100644 --- a/moonglass/src/glossary.rs +++ b/moonglass-core/src/glossary.rs @@ -40,6 +40,14 @@ //! identified by a [`BuilderIndex`](crate::primitives::BuilderIndex). A builder //! need not be the proposer. //! +//! ## Builder payment window +//! +//! The fixed state window that tracks accepted builder bids until +//! payload-timeliness votes and epoch processing settle payment. It is +//! represented by +//! [`builder_pending_payments`](crate::containers::BeaconState::builder_pending_payments) +//! entries of [`BuilderPendingPayment`](crate::containers::BuilderPendingPayment). +//! //! ## Checkpoint //! //! The chain landmark used for finality, a @@ -47,6 +55,13 @@ //! slot, or, when that slot is empty, the latest ancestor block before it. Votes //! name checkpoints as their finality targets. //! +//! ## Churn budget +//! +//! The amount of validator balance that may enter, exit, or consolidate in one +//! epoch. State transition helpers compute it from active balance, then consume +//! it when assigning activation, exit, and consolidation epochs. See +//! [`get_balance_churn_limit`](crate::containers::BeaconState::get_balance_churn_limit). +//! //! ## Committee //! //! The subset of validators assigned to vote in a particular slot, identified by @@ -64,8 +79,8 @@ //! //! Whether the data needed to use and check a payload can actually be downloaded //! from the network. Fork choice gates a payload on it through -//! [`is_data_available`](crate::fork_choice::on_execution_payload_envelope::is_data_available), -//! which is currently mocked. +//! [`Store::is_data_available`](crate::fork_choice::Store::is_data_available), +//! supplied by the caller after local sidecar checks. //! //! ## Dependent root //! @@ -100,9 +115,9 @@ //! ## Execution engine //! //! The execution-layer component that runs a payload's transactions and reports -//! whether it is valid. Fork choice consults it through -//! [`verify_and_notify_new_payload`](crate::fork_choice::on_execution_payload_envelope::verify_and_notify_new_payload), -//! which is currently mocked. +//! whether it is valid. Fork choice consults it through the +//! [`ExecutionPayloadVerifier`](crate::fork_choice::on_execution_payload_envelope::ExecutionPayloadVerifier) +//! seam, which is currently mocked. //! //! ## Execution payload //! @@ -190,6 +205,13 @@ //! vote, then from the justified root walks to the child branch with the most //! supporting stake, over and over, until it reaches a head. //! +//! ## Parent payload handoff +//! +//! The block-level step that accepts execution requests carried by the parent +//! payload and checks they match the root committed in the parent bid. It is the +//! bridge between a delivered payload and the next block that consumes its +//! requests. +//! //! ## Payload attestation //! //! A committee member's vote about a payload's timeliness and data availability, @@ -235,6 +257,13 @@ //! Not the same as the builder: the proposer publishes the block, the builder //! supplies the payload. //! +//! ## Proposer lookahead +//! +//! The state vector of upcoming beacon proposers, stored as +//! [`proposer_lookahead`](crate::containers::BeaconState::proposer_lookahead). +//! Committee helpers read it to know who should propose a slot, and epoch +//! processing shifts and refills it as time moves forward. +//! //! ## Proposer boost //! //! Temporary extra weight given to a block proposed on time in the current slot, @@ -306,3 +335,10 @@ //! [`get_voting_source`](crate::fork_choice::Store::get_voting_source). A block //! competes for head only while its voting source is recent enough, part of what //! makes it a [viable block](crate::glossary#viable-block). +//! +//! ## Withdrawal sweep +//! +//! The bounded scan over validators that selects expected withdrawals for the +//! next execution payload, recorded as +//! [`Withdrawal`](crate::containers::Withdrawal) values. The sweep resumes from +//! the next validator index carried in state. diff --git a/moonglass-core/src/gossip.rs b/moonglass-core/src/gossip.rs new file mode 100644 index 0000000..9b09beb --- /dev/null +++ b/moonglass-core/src/gossip.rs @@ -0,0 +1,147 @@ +//! Pure gossip and validator-duty predicates. + +use crate::constants::{ + ALTAIR_FORK_EPOCH, ALTAIR_FORK_VERSION, BELLATRIX_FORK_EPOCH, BELLATRIX_FORK_VERSION, + CAPELLA_FORK_EPOCH, CAPELLA_FORK_VERSION, DENEB_FORK_EPOCH, DENEB_FORK_VERSION, + ELECTRA_FORK_EPOCH, ELECTRA_FORK_VERSION, FULU_FORK_EPOCH, FULU_FORK_VERSION, + GENESIS_FORK_VERSION, GLOAS_FORK_EPOCH, GLOAS_FORK_VERSION, MIN_SEED_LOOKAHEAD, + SLOTS_PER_EPOCH, +}; +use crate::containers::{BeaconState, ProposerPreferences}; +use crate::error::{OperationError, TransitionArithmetic, TransitionError}; +use crate::primitives::{Epoch, Root, Slot, ValidatorIndex, Version}; + +/// Return the configured fork version for an epoch. +pub fn compute_fork_version(epoch: Epoch) -> Version { + if epoch >= GLOAS_FORK_EPOCH { + return GLOAS_FORK_VERSION; + } + if epoch >= FULU_FORK_EPOCH { + return FULU_FORK_VERSION; + } + if epoch >= ELECTRA_FORK_EPOCH { + return ELECTRA_FORK_VERSION; + } + if epoch >= DENEB_FORK_EPOCH { + return DENEB_FORK_VERSION; + } + if epoch >= CAPELLA_FORK_EPOCH { + return CAPELLA_FORK_VERSION; + } + if epoch >= BELLATRIX_FORK_EPOCH { + return BELLATRIX_FORK_VERSION; + } + if epoch >= ALTAIR_FORK_EPOCH { + return ALTAIR_FORK_VERSION; + } + GENESIS_FORK_VERSION +} + +/// Check whether a gas limit follows the parent limit toward the target. +pub fn is_gas_limit_target_compatible( + parent_gas_limit: u64, + gas_limit: u64, + target_gas_limit: u64, +) -> bool { + let max_gas_limit_difference = (parent_gas_limit / 1024).max(1) - 1; + let min_gas_limit = parent_gas_limit.saturating_sub(max_gas_limit_difference); + let max_gas_limit = parent_gas_limit.saturating_add(max_gas_limit_difference); + + if (min_gas_limit..=max_gas_limit).contains(&target_gas_limit) { + return gas_limit == target_gas_limit; + } + if target_gas_limit > max_gas_limit { + return gas_limit == max_gas_limit; + } + gas_limit == min_gas_limit +} + +impl BeaconState { + /// Return the slot in `epoch` where `validator_index` is assigned to the PTC. + pub fn get_ptc_assignment( + &self, + epoch: Epoch, + validator_index: ValidatorIndex, + ) -> Result, TransitionError> { + let max_epoch = self + .get_current_epoch() + .as_u64() + .checked_add(MIN_SEED_LOOKAHEAD as u64) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Epoch, + ))?; + if epoch.as_u64() > max_epoch { + return Err(OperationError::PayloadAttestationSlotMismatch.into()); + } + + let start_slot = epoch.start_slot().as_u64(); + let end_slot = start_slot.checked_add(SLOTS_PER_EPOCH as u64).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Slot), + )?; + for slot in start_slot..end_slot { + let slot = Slot::new(slot); + let committee = self.get_ptc(slot)?; + if committee.iter().any(|index| *index == validator_index) { + return Ok(Some(slot)); + } + } + Ok(None) + } + + /// Return future proposal slots in the proposer lookahead. + pub fn get_upcoming_proposal_slots(&self, validator_index: ValidatorIndex) -> Vec { + let current_epoch_start_slot = self.get_current_epoch().start_slot(); + self.proposer_lookahead + .iter() + .copied() + .enumerate() + .filter_map(|(offset, proposer_index)| { + let slot = current_epoch_start_slot.saturating_add(offset as u64); + (slot > self.slot && proposer_index == validator_index).then_some(slot) + }) + .collect() + } + + /// Check whether preferences name the expected proposer for their slot. + pub fn is_valid_proposal_slot(&self, preferences: &ProposerPreferences) -> bool { + if preferences.proposal_slot <= self.slot { + return false; + } + let current_epoch = self.get_current_epoch(); + let proposal_epoch = preferences.proposal_slot.epoch(); + if proposal_epoch < current_epoch { + return false; + } + if proposal_epoch.as_u64() + > current_epoch + .as_u64() + .saturating_add(MIN_SEED_LOOKAHEAD as u64) + { + return false; + } + + let epoch_offset = proposal_epoch + .as_u64() + .saturating_sub(current_epoch.as_u64()); + let slot_offset = preferences.proposal_slot.as_u64() % SLOTS_PER_EPOCH as u64; + let index = epoch_offset + .saturating_mul(SLOTS_PER_EPOCH as u64) + .saturating_add(slot_offset); + let Ok(index) = usize::try_from(index) else { + return false; + }; + self.proposer_lookahead + .get(index) + .is_some_and(|proposer| *proposer == preferences.validator_index) + } + + /// Return the dependent block root for the proposer lookahead at `epoch`. + pub fn get_proposer_dependent_root(&self, epoch: Epoch) -> Root { + if epoch.as_u64() <= MIN_SEED_LOOKAHEAD as u64 { + return self.block_root_at_slot(Slot::new(0)); + } + let lookahead_epoch = epoch.saturating_sub(MIN_SEED_LOOKAHEAD as u64); + let slot = lookahead_epoch.start_slot().saturating_sub(1); + self.block_root_at_slot(slot) + } +} diff --git a/moonglass-core/src/lib.rs b/moonglass-core/src/lib.rs new file mode 100644 index 0000000..ed7b629 --- /dev/null +++ b/moonglass-core/src/lib.rs @@ -0,0 +1,161 @@ +#![allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + +//! A behavior-first guide to the Ethereum consensus specs. +//! +//! Ethereum consensus is the rulebook validators use to agree on chain state. +//! This crate models the data validators agree on, the signed blocks that +//! propose changes to it, and the transition rules that decide whether those +//! changes are valid. +//! +//! This crate is a readable execution map, not a production client architecture. +//! Its data structures and main function shapes stay close to the consensus +//! specs where that helps orientation, and helpers may use clearer Rust shapes when +//! that makes the protocol behavior easier to follow. +//! The covered surface is the currently wired consensus-specs reference-test +//! surface. `mainnet` and `minimal` are spec presets, not claims that every +//! documented branch is live-network behavior or fixture-covered. +//! +//! Start with a protocol object, not with a file tree. Ask which handler owns +//! the object, which fields it reads, which fields it writes, whether the write +//! is durable consensus state or local fork-choice view, where the current +//! implementation deliberately stops, and which fixture family exercises that +//! path. The core route is object -> owning handler -> reads -> writes -> +//! decision -> boundary -> fixture. +//! +//! # Start by object +//! +//! Follow one route at a time. Each route names the object to start from, the +//! handler that owns it, the state/store distinction to watch, and the fixture +//! family that closes the loop. +//! +//! Block acceptance starts at [`containers::SignedBeaconBlock`], then inspects +//! [`containers::BeaconState::apply_signed_block`] and +//! [`fork_choice::Store::on_block()`]. Watch the state transition mutate a cloned +//! [`BeaconState`](containers::BeaconState) while fork choice caches the +//! post-state in [`fork_choice::Store`]. Fixtures: `sanity/blocks`, +//! `fork_choice/on_block`. +//! +//! Bid commitment starts at [`containers::ExecutionPayloadBid`], then inspects +//! [`containers::BeaconState::process_execution_payload_bid`]. A bid commits +//! consensus state to payload fields and opens any builder-payment obligation. It +//! is not delivered payload evidence. Fixture: `operations/execution_payload_bid`. +//! +//! Delivered payload evidence starts at +//! [`containers::SignedExecutionPayloadEnvelope`], then inspects +//! [`fork_choice::Store::on_execution_payload_envelope()`] and +//! [`fork_choice::Store::payloads`]. The envelope passed the implemented +//! consensus-side checks, not full execution-engine or blob-availability +//! verification. Fixture: `fork_choice/on_execution_payload_envelope`. +//! +//! Parent-payload settlement starts at [`containers::ExecutionPayloadBid`] and +//! [`containers::ExecutionRequests`], then inspects +//! [`containers::BeaconState::process_parent_execution_payload`]. The child proves +//! its parent payload handoff, applies the parent payload's requests, and releases +//! the parent builder payment. Fixture: `operations/parent_execution_payload`. +//! +//! PTC votes start at [`containers::PayloadAttestation`] and +//! [`containers::PayloadAttestationMessage`], then inspect +//! [`containers::BeaconState::process_payload_attestation`] and +//! [`fork_choice::Store::on_payload_attestation_message()`]. Block aggregates and +//! gossip messages are admitted differently, but the store records local PTC vote +//! evidence by position. Fixtures: `operations/payload_attestation`, +//! `fork_choice/on_payload_attestation_message`. +//! +//! Beacon attestation branch choice starts at [`containers::Attestation`], then +//! inspects [`containers::BeaconState::process_attestation`] and +//! [`fork_choice::Store::on_attestation()`]. Votes for a block at `data.slot` +//! stay pending. Votes for an older head use `AttestationData::index` as the +//! payload empty/full selector. Fixtures: `operations/attestation`, +//! `fork_choice/on_attestation`. +//! +//! Head selection starts at [`fork_choice::ForkChoiceNode`], then inspects +//! [`fork_choice::Store::get_head`]. Fork choice returns both a block root and a +//! [`fork_choice::PayloadStatus`]. Fixture: `fork_choice/get_head`. +//! +//! Use [`state_transition`] to follow a block through the rulebook, then use +//! [`containers`] for the data being moved, [`primitives`] and [`constants`] for +//! vocabulary and parameters, [`error`] for the difference between an invalid +//! transition and behavior not yet covered, and [`fork_choice`] for +//! the head-selection rule that reads accepted blocks and attestations to decide +//! which leaf the next block should build on. +//! +//! Build docs with private items when reading this crate as an executable spec: +//! most of the useful phase maps live in private modules because they mirror +//! consensus sub-phases rather than form a public API surface. +//! +//! Coverage boundaries are part of the reading surface. When a consensus branch +//! can be exercised without yet implementing every external verifier, the +//! relevant module docs should name that boundary explicitly. +//! +//! # Hold these distinctions before reading +//! +//! A few distinctions decide whether the rest of the code reads correctly. +//! Hold them before following any route. +//! +//! [`BeaconState`](containers::BeaconState) is durable consensus state, the +//! snapshot validators agree on and carry forward. The fork-choice +//! [`Store`](fork_choice::Store) is one node's local view, the accepted blocks, +//! attestations, and clock that node has seen. The store is bookkeeping for head +//! selection, not consensus state, and two honest nodes can hold different stores. +//! +//! A builder's bid is a commitment, not an accepted payload. Recording the bid +//! promises a payload at a hash, but the payload effects settle only when a child +//! block proves and applies the parent payload commitment. +//! +//! A recorded payload envelope has passed only the consensus-side checks: beacon +//! block roots, required envelope signature, bid-matched payload fields, payload +//! slot, parent execution hash, timestamp, requests root, and withdrawals. +//! Recording it is not an execution-engine validity verdict and not a +//! data-availability verdict. +//! +//! Payload-timeliness votes are indexed by committee position. A gossip message +//! names one validator and expands to the committee positions that validator +//! holds, so the same vote reads as a validator on the wire and as a set of +//! positions in the aggregate. +//! +//! A beacon attestation's `index` is a payload-branch selector only when the voted +//! block is older than `attestation.data.slot`. If the voted block is at +//! `data.slot`, the vote must use the empty/pending form. The two rulebooks then +//! read that selector differently. In state transition, a historical vote's +//! `index` is matched against the `BeaconState` payload-availability bit to +//! decide whether the vote earns its head flag, not whether the vote is accepted. +//! In fork choice, an older full-branch vote is admitted only once the local +//! [`fork_choice::Store::payloads`] map holds the recorded envelope. One shapes a +//! head-flag reward from durable consensus state, the other is a node-local +//! admission gate. +//! +//! A child block applies the parent payload's effects before its own bid. It +//! settles the parent block's promised payload first, then records its own +//! commitment for a later child to prove. +//! +//! # Where this implementation stops +//! +//! The implementation runs the consensus-side rules and stops at the external +//! services a production client would also wire in. Execution-engine payload +//! validity is not checked, so a recorded payload is consensus-checked, not +//! engine-confirmed. Data-availability helpers verify sidecar and cell proof +//! shapes, but there is no sampling scheduler, peer scorer, or custody backfill +//! service. Networking exposes wire constants and topic helpers, but no libp2p +//! host or gossip-validation dispatcher is included. +//! Within those boundaries the code can still exercise the covered consensus +//! branches, including the payload-status branches in fork choice, which is why +//! each affected module names its own boundary in its docs. +#[cfg(not(any(feature = "mainnet", feature = "minimal")))] +compile_error!("crate must be built with exactly one of the `mainnet` or `minimal` features"); + +#[cfg(all(feature = "mainnet", feature = "minimal"))] +compile_error!( + "crate cannot be built with both `mainnet` and `minimal` features (cargo features are additive)" +); + +pub mod constants; +pub mod containers; +pub mod crypto; +pub mod error; +pub mod fork_choice; +pub mod glossary; +pub mod gossip; +pub mod networking; +pub mod primitives; +pub mod ssz; +pub mod state_transition; diff --git a/moonglass-core/src/networking.rs b/moonglass-core/src/networking.rs new file mode 100644 index 0000000..7d68ead --- /dev/null +++ b/moonglass-core/src/networking.rs @@ -0,0 +1,140 @@ +//! Wire-facing helpers for gossip topics and req/resp protocol IDs. + +use sha2::{Digest, Sha256}; + +use crate::constants::FULU_FORK_EPOCH; +use crate::containers::get_blob_parameters; +use crate::error::TransitionError; +use crate::gossip::compute_fork_version; +use crate::primitives::{Epoch, ForkDigest, Root, SubnetId}; +use crate::state_transition::compute_fork_data_root; + +/// Req/Resp protocol for block range requests. +pub const BEACON_BLOCKS_BY_RANGE_V2_PROTOCOL_ID: &str = + "/eth2/beacon_chain/req/beacon_blocks_by_range/2/ssz_snappy"; + +/// Req/Resp protocol for block root requests. +pub const BEACON_BLOCKS_BY_ROOT_V2_PROTOCOL_ID: &str = + "/eth2/beacon_chain/req/beacon_blocks_by_root/2/ssz_snappy"; + +/// Req/Resp protocol for execution payload envelope range requests. +pub const EXECUTION_PAYLOAD_ENVELOPES_BY_RANGE_V1_PROTOCOL_ID: &str = + "/eth2/beacon_chain/req/execution_payload_envelopes_by_range/1/ssz_snappy"; + +/// Req/Resp protocol for execution payload envelope root requests. +pub const EXECUTION_PAYLOAD_ENVELOPES_BY_ROOT_V1_PROTOCOL_ID: &str = + "/eth2/beacon_chain/req/execution_payload_envelopes_by_root/1/ssz_snappy"; + +/// Gossip encoding suffix used by consensus topics. +pub const GOSSIP_ENCODING_SSZ_SNAPPY: &str = "ssz_snappy"; + +/// Gossip topic name for beacon blocks. +pub const BEACON_BLOCK_TOPIC: &str = "beacon_block"; + +/// Gossip topic name for execution payload bids. +pub const EXECUTION_PAYLOAD_BID_TOPIC: &str = "execution_payload_bid"; + +/// Gossip topic name for execution payload envelopes. +pub const EXECUTION_PAYLOAD_TOPIC: &str = "execution_payload"; + +/// Gossip topic name for payload attestation messages. +pub const PAYLOAD_ATTESTATION_MESSAGE_TOPIC: &str = "payload_attestation_message"; + +/// Gossip topic name for proposer preferences. +pub const PROPOSER_PREFERENCES_TOPIC: &str = "proposer_preferences"; + +/// Gossip topic name for aggregate attestations and proofs. +pub const BEACON_AGGREGATE_AND_PROOF_TOPIC: &str = "beacon_aggregate_and_proof"; + +/// Gossip topic name stem for column sidecars, suffixed with the subnet id. +pub const DATA_COLUMN_SIDECAR_TOPIC: &str = "data_column_sidecar"; + +/// Return the fork digest for `genesis_validators_root` at `epoch`. +pub fn compute_fork_digest( + genesis_validators_root: Root, + epoch: Epoch, +) -> Result { + let fork_version = compute_fork_version(epoch); + let base_digest = compute_fork_data_root(fork_version, genesis_validators_root)?; + + if epoch < FULU_FORK_EPOCH { + return Ok(ForkDigest(first_four_bytes(base_digest.0))); + } + + let blob_parameters = get_blob_parameters(epoch); + let mut input = [0_u8; 16]; + input[..8].copy_from_slice(&blob_parameters.epoch.as_u64().to_le_bytes()); + input[8..].copy_from_slice(&blob_parameters.max_blobs_per_block.to_le_bytes()); + let parameter_digest: [u8; 32] = Sha256::digest(input).into(); + let mut digest = [0_u8; 32]; + for (out, (base, parameter)) in digest + .iter_mut() + .zip(base_digest.0.into_iter().zip(parameter_digest)) + { + *out = base ^ parameter; + } + + Ok(ForkDigest(first_four_bytes(digest))) +} + +/// Build a gossip topic path for `name` and `fork_digest`. +pub fn gossip_topic(fork_digest: ForkDigest, name: &str) -> String { + format!( + "/eth2/{}/{name}/{GOSSIP_ENCODING_SSZ_SNAPPY}", + fork_digest_hex(fork_digest) + ) +} + +/// Lowercase hex string for a `ForkDigest`. +pub fn fork_digest_hex(fork_digest: ForkDigest) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(8); + for byte in fork_digest.0 { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + +/// Topic path for beacon blocks under `fork_digest`. +pub fn beacon_block_topic(fork_digest: ForkDigest) -> String { + gossip_topic(fork_digest, BEACON_BLOCK_TOPIC) +} + +/// Topic path for aggregate attestations and proofs under `fork_digest`. +pub fn beacon_aggregate_and_proof_topic(fork_digest: ForkDigest) -> String { + gossip_topic(fork_digest, BEACON_AGGREGATE_AND_PROOF_TOPIC) +} + +/// Topic path for execution payload bids under `fork_digest`. +pub fn execution_payload_bid_topic(fork_digest: ForkDigest) -> String { + gossip_topic(fork_digest, EXECUTION_PAYLOAD_BID_TOPIC) +} + +/// Topic path for execution payload envelopes under `fork_digest`. +pub fn execution_payload_topic(fork_digest: ForkDigest) -> String { + gossip_topic(fork_digest, EXECUTION_PAYLOAD_TOPIC) +} + +/// Topic path for payload attestation messages under `fork_digest`. +pub fn payload_attestation_message_topic(fork_digest: ForkDigest) -> String { + gossip_topic(fork_digest, PAYLOAD_ATTESTATION_MESSAGE_TOPIC) +} + +/// Topic path for proposer preferences under `fork_digest`. +pub fn proposer_preferences_topic(fork_digest: ForkDigest) -> String { + gossip_topic(fork_digest, PROPOSER_PREFERENCES_TOPIC) +} + +/// Topic path for column sidecars on `subnet_id` under `fork_digest`. +pub fn data_column_sidecar_topic(fork_digest: ForkDigest, subnet_id: SubnetId) -> String { + gossip_topic( + fork_digest, + &format!("{DATA_COLUMN_SIDECAR_TOPIC}_{}", subnet_id.as_u64()), + ) +} + +/// First four bytes of a digest. +pub const fn first_four_bytes(bytes: [u8; 32]) -> [u8; 4] { + [bytes[0], bytes[1], bytes[2], bytes[3]] +} diff --git a/moonglass/src/primitives.rs b/moonglass-core/src/primitives.rs similarity index 78% rename from moonglass/src/primitives.rs rename to moonglass-core/src/primitives.rs index 346acad..35b3fb7 100644 --- a/moonglass/src/primitives.rs +++ b/moonglass-core/src/primitives.rs @@ -4,10 +4,10 @@ //! SSZ encoding, but they are not interchangeable in the transition rules. This //! keeps those protocol meanings explicit. -mod arithmetic; -mod byte_arrays; -mod numeric; -mod numeric_ssz; +pub mod arithmetic; +pub mod byte_arrays; +pub mod numeric; +pub mod numeric_ssz; pub use byte_arrays::*; pub use numeric::*; diff --git a/moonglass/src/primitives/arithmetic.rs b/moonglass-core/src/primitives/arithmetic.rs similarity index 85% rename from moonglass/src/primitives/arithmetic.rs rename to moonglass-core/src/primitives/arithmetic.rs index 43894b0..00c52d4 100644 --- a/moonglass/src/primitives/arithmetic.rs +++ b/moonglass-core/src/primitives/arithmetic.rs @@ -1,5 +1,6 @@ //! Arithmetic operator impls on the primitive newtypes. +use core::fmt::{Display, Formatter, Result as FmtResult}; use core::ops::{Add, AddAssign, Div, Mul, Rem, Sub, SubAssign}; use super::{BuilderIndex, CommitteeIndex, Epoch, Gwei, Slot, ValidatorIndex}; @@ -163,32 +164,32 @@ impl Div for Gwei { } } -impl core::fmt::Display for Slot { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +impl Display for Slot { + fn fmt(&self, f: &mut Formatter) -> FmtResult { self.0.fmt(f) } } -impl core::fmt::Display for Epoch { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +impl Display for Epoch { + fn fmt(&self, f: &mut Formatter) -> FmtResult { self.0.fmt(f) } } -impl core::fmt::Display for ValidatorIndex { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +impl Display for ValidatorIndex { + fn fmt(&self, f: &mut Formatter) -> FmtResult { self.0.fmt(f) } } -impl core::fmt::Display for BuilderIndex { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +impl Display for BuilderIndex { + fn fmt(&self, f: &mut Formatter) -> FmtResult { self.0.fmt(f) } } -impl core::fmt::Display for CommitteeIndex { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +impl Display for CommitteeIndex { + fn fmt(&self, f: &mut Formatter) -> FmtResult { self.0.fmt(f) } } diff --git a/moonglass-core/src/primitives/byte_arrays.rs b/moonglass-core/src/primitives/byte_arrays.rs new file mode 100644 index 0000000..11973c4 --- /dev/null +++ b/moonglass-core/src/primitives/byte_arrays.rs @@ -0,0 +1,747 @@ +//! Fixed-size byte newtypes (roots, hashes, BLS pubkeys, signatures, KZG commitments). + +use crate::constants::BYTES_PER_CELL; +use crate::ssz::prelude::*; + +/// SSZ chunk length, in bytes. +pub const SSZ_CHUNK_BYTES: usize = 32; +/// Compressed BLS12-381 G1 point length, in bytes. +pub const BLS_G1_COMPRESSED_BYTES: usize = 48; +/// Compressed BLS12-381 G2 point length, in bytes. +pub const BLS_G2_COMPRESSED_BYTES: usize = 96; +/// KZG commitment length (compressed BLS12-381 G1 point), in bytes. +pub const KZG_COMMITMENT_BYTES: usize = 48; +/// KZG proof length (compressed BLS12-381 G1 point), in bytes. +pub const KZG_PROOF_BYTES: usize = 48; +/// 256-bit unsigned integer length, in bytes. +pub const UINT256_BYTES: usize = 32; + +/// Public node identifier encoded as a little-endian `uint256`. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct NodeId(pub [u8; UINT256_BYTES]); + +impl NodeId { + /// All-zero node identifier. + pub const ZERO: Self = Self([0; UINT256_BYTES]); + + /// Construct from little-endian `uint256` bytes. + #[inline] + pub const fn from_le_bytes(bytes: [u8; UINT256_BYTES]) -> Self { + Self(bytes) + } + + /// Return little-endian `uint256` bytes. + #[inline] + pub const fn to_le_bytes(self) -> [u8; UINT256_BYTES] { + self.0 + } +} + +/// 32-byte SSZ hash-tree-root. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct Root(pub [u8; 32]); + +impl Root { + /// All-zero root used by the spec as an unset placeholder in block headers. + pub const ZERO: Self = Self([0; 32]); +} + +/// 32-byte execution-layer hash. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct Hash32(pub [u8; 32]); + +/// Versioned blob hash (KZG commitment digest with version prefix). +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct VersionedHash(pub [u8; 32]); + +/// 20-byte execution-layer address. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct ExecutionAddress(pub [u8; 20]); + +/// 4-byte signing version. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct Version(pub [u8; 4]); + +/// 4-byte digest of the active signing version. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct ForkDigest(pub [u8; 4]); + +/// 32-byte SSZ signing domain. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct Domain(pub [u8; 32]); + +/// 4-byte domain-type tag (left half of a [`Domain`]). +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct DomainType(pub [u8; 4]); + +/// Compressed BLS public key as SSZ-decoded bytes. +/// +/// Curve validity is checked by the BLS verifier. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct BLSPubkey(pub [u8; BLS_G1_COMPRESSED_BYTES]); + +impl Default for BLSPubkey { + fn default() -> Self { + Self([0; BLS_G1_COMPRESSED_BYTES]) + } +} + +impl SszSized for BLSPubkey { + fn is_variable_size() -> bool { + false + } + fn size_hint() -> usize { + BLS_G1_COMPRESSED_BYTES + } +} + +impl Serialize for BLSPubkey { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(&self.0); + Ok(BLS_G1_COMPRESSED_BYTES) + } +} + +impl Deserialize for BLSPubkey { + fn deserialize(encoding: &[u8]) -> Result { + match encoding.len() { + n if n == BLS_G1_COMPRESSED_BYTES => { + let mut out = [0u8; BLS_G1_COMPRESSED_BYTES]; + out.copy_from_slice(encoding); + Ok(Self(out)) + } + n if n < BLS_G1_COMPRESSED_BYTES => Err(DeserializeError::ExpectedFurtherInput { + provided: n, + expected: BLS_G1_COMPRESSED_BYTES, + }), + n => Err(DeserializeError::AdditionalInput { + provided: n, + expected: BLS_G1_COMPRESSED_BYTES, + }), + } + } +} + +impl Merkleized for BLSPubkey { + fn hash_tree_root(&self) -> Result { + Ok(merkleize_byte_sequence(&self.0)) + } +} + +impl SimpleSerialize for BLSPubkey { + fn is_composite_type() -> bool { + true + } +} + +/// Compressed BLS signature as SSZ-decoded bytes. +/// +/// Curve validity is checked by the BLS verifier. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct BLSSignature(pub [u8; BLS_G2_COMPRESSED_BYTES]); + +impl Default for BLSSignature { + fn default() -> Self { + Self([0; BLS_G2_COMPRESSED_BYTES]) + } +} + +impl BLSSignature { + /// Serialized BLS12-381 G2 point at infinity used for empty aggregates and + /// self-build placeholders. + pub const G2_POINT_AT_INFINITY: Self = { + let mut bytes = [0; BLS_G2_COMPRESSED_BYTES]; + bytes[0] = 0xC0; + Self(bytes) + }; + + /// True when this signature is the serialized G2 point at infinity. + pub const fn is_g2_point_at_infinity(&self) -> bool { + let expected = Self::G2_POINT_AT_INFINITY.0; + let mut i = 0; + while i < BLS_G2_COMPRESSED_BYTES { + if self.0[i] != expected[i] { + return false; + } + i += 1; + } + true + } +} + +impl SszSized for BLSSignature { + fn is_variable_size() -> bool { + false + } + fn size_hint() -> usize { + BLS_G2_COMPRESSED_BYTES + } +} + +impl Serialize for BLSSignature { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(&self.0); + Ok(BLS_G2_COMPRESSED_BYTES) + } +} + +impl Deserialize for BLSSignature { + fn deserialize(encoding: &[u8]) -> Result { + match encoding.len() { + n if n == BLS_G2_COMPRESSED_BYTES => { + let mut out = [0u8; BLS_G2_COMPRESSED_BYTES]; + out.copy_from_slice(encoding); + Ok(Self(out)) + } + n if n < BLS_G2_COMPRESSED_BYTES => Err(DeserializeError::ExpectedFurtherInput { + provided: n, + expected: BLS_G2_COMPRESSED_BYTES, + }), + n => Err(DeserializeError::AdditionalInput { + provided: n, + expected: BLS_G2_COMPRESSED_BYTES, + }), + } + } +} + +impl Merkleized for BLSSignature { + fn hash_tree_root(&self) -> Result { + Ok(merkleize_byte_sequence(&self.0)) + } +} + +impl SimpleSerialize for BLSSignature { + fn is_composite_type() -> bool { + true + } +} + +/// KZG commitment as SSZ-decoded bytes. +/// +/// Curve and subgroup validity are checked by the KZG verifier. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct KZGCommitment(pub [u8; KZG_COMMITMENT_BYTES]); + +impl Default for KZGCommitment { + fn default() -> Self { + Self([0; KZG_COMMITMENT_BYTES]) + } +} + +impl SszSized for KZGCommitment { + fn is_variable_size() -> bool { + false + } + fn size_hint() -> usize { + KZG_COMMITMENT_BYTES + } +} + +impl Serialize for KZGCommitment { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(&self.0); + Ok(KZG_COMMITMENT_BYTES) + } +} + +impl Deserialize for KZGCommitment { + fn deserialize(encoding: &[u8]) -> Result { + match encoding.len() { + n if n == KZG_COMMITMENT_BYTES => { + let mut out = [0u8; KZG_COMMITMENT_BYTES]; + out.copy_from_slice(encoding); + Ok(Self(out)) + } + n if n < KZG_COMMITMENT_BYTES => Err(DeserializeError::ExpectedFurtherInput { + provided: n, + expected: KZG_COMMITMENT_BYTES, + }), + n => Err(DeserializeError::AdditionalInput { + provided: n, + expected: KZG_COMMITMENT_BYTES, + }), + } + } +} + +impl Merkleized for KZGCommitment { + fn hash_tree_root(&self) -> Result { + Ok(merkleize_byte_sequence(&self.0)) + } +} + +impl SimpleSerialize for KZGCommitment { + fn is_composite_type() -> bool { + true + } +} + +/// KZG proof as SSZ-decoded bytes. +/// +/// Curve and subgroup validity are checked by the KZG verifier. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct KZGProof(pub [u8; KZG_PROOF_BYTES]); + +impl Default for KZGProof { + fn default() -> Self { + Self([0; KZG_PROOF_BYTES]) + } +} + +impl SszSized for KZGProof { + fn is_variable_size() -> bool { + false + } + fn size_hint() -> usize { + KZG_PROOF_BYTES + } +} + +impl Serialize for KZGProof { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(&self.0); + Ok(KZG_PROOF_BYTES) + } +} + +impl Deserialize for KZGProof { + fn deserialize(encoding: &[u8]) -> Result { + match encoding.len() { + n if n == KZG_PROOF_BYTES => { + let mut out = [0u8; KZG_PROOF_BYTES]; + out.copy_from_slice(encoding); + Ok(Self(out)) + } + n if n < KZG_PROOF_BYTES => Err(DeserializeError::ExpectedFurtherInput { + provided: n, + expected: KZG_PROOF_BYTES, + }), + n => Err(DeserializeError::AdditionalInput { + provided: n, + expected: KZG_PROOF_BYTES, + }), + } + } +} + +impl Merkleized for KZGProof { + fn hash_tree_root(&self) -> Result { + Ok(merkleize_byte_sequence(&self.0)) + } +} + +impl SimpleSerialize for KZGProof { + fn is_composite_type() -> bool { + true + } +} + +/// Serialized evaluations for one data-availability cell. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct Cell(pub [u8; BYTES_PER_CELL]); + +impl Default for Cell { + fn default() -> Self { + Self([0; BYTES_PER_CELL]) + } +} + +impl SszSized for Cell { + fn is_variable_size() -> bool { + false + } + fn size_hint() -> usize { + BYTES_PER_CELL + } +} + +impl Serialize for Cell { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(&self.0); + Ok(BYTES_PER_CELL) + } +} + +impl Deserialize for Cell { + fn deserialize(encoding: &[u8]) -> Result { + match encoding.len() { + n if n == BYTES_PER_CELL => { + let mut out = [0u8; BYTES_PER_CELL]; + out.copy_from_slice(encoding); + Ok(Self(out)) + } + n if n < BYTES_PER_CELL => Err(DeserializeError::ExpectedFurtherInput { + provided: n, + expected: BYTES_PER_CELL, + }), + n => Err(DeserializeError::AdditionalInput { + provided: n, + expected: BYTES_PER_CELL, + }), + } + } +} + +impl Merkleized for Cell { + fn hash_tree_root(&self) -> Result { + Ok(merkleize_byte_sequence(&self.0)) + } +} + +impl SimpleSerialize for Cell { + fn is_composite_type() -> bool { + true + } +} + +/// 256-bit unsigned integer stored as 32 little-endian bytes per SSZ. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct Uint256(pub [u8; UINT256_BYTES]); + +impl SszSized for Uint256 { + fn is_variable_size() -> bool { + false + } + fn size_hint() -> usize { + UINT256_BYTES + } +} + +impl Serialize for Uint256 { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(&self.0); + Ok(UINT256_BYTES) + } +} + +impl Deserialize for Uint256 { + fn deserialize(encoding: &[u8]) -> Result { + match encoding.len() { + n if n == UINT256_BYTES => { + let mut out = [0u8; UINT256_BYTES]; + out.copy_from_slice(encoding); + Ok(Self(out)) + } + n if n < UINT256_BYTES => Err(DeserializeError::ExpectedFurtherInput { + provided: n, + expected: UINT256_BYTES, + }), + n => Err(DeserializeError::AdditionalInput { + provided: n, + expected: UINT256_BYTES, + }), + } + } +} + +impl Merkleized for Uint256 { + fn hash_tree_root(&self) -> Result { + // Basic uintN values pack into a single chunk. For uint256 the chunk is the value itself. + Ok(Node(self.0)) + } +} + +impl SimpleSerialize for Uint256 { + fn is_composite_type() -> bool { + false + } +} + +/// 32-byte opaque payload. +pub type Bytes32 = [u8; 32]; + +impl From for Root { + fn from(node: Node) -> Self { + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(node.as_ref()); + Self(bytes) + } +} + +impl SszSized for Root { + fn is_variable_size() -> bool { + <[u8; 32] as SszSized>::is_variable_size() + } + + fn size_hint() -> usize { + <[u8; 32] as SszSized>::size_hint() + } +} + +impl Serialize for Root { + fn serialize(&self, buffer: &mut Vec) -> Result { + Serialize::serialize(&self.0, buffer) + } +} + +impl Deserialize for Root { + fn deserialize(encoding: &[u8]) -> Result { + <[u8; 32] as Deserialize>::deserialize(encoding).map(Self) + } +} + +impl Merkleized for Root { + fn hash_tree_root(&self) -> Result { + Merkleized::hash_tree_root(&self.0) + } +} + +impl SimpleSerialize for Root { + fn is_composite_type() -> bool { + <[u8; 32] as SimpleSerialize>::is_composite_type() + } +} + +impl SszSized for Hash32 { + fn is_variable_size() -> bool { + <[u8; 32] as SszSized>::is_variable_size() + } + + fn size_hint() -> usize { + <[u8; 32] as SszSized>::size_hint() + } +} + +impl Serialize for Hash32 { + fn serialize(&self, buffer: &mut Vec) -> Result { + Serialize::serialize(&self.0, buffer) + } +} + +impl Deserialize for Hash32 { + fn deserialize(encoding: &[u8]) -> Result { + <[u8; 32] as Deserialize>::deserialize(encoding).map(Self) + } +} + +impl Merkleized for Hash32 { + fn hash_tree_root(&self) -> Result { + Merkleized::hash_tree_root(&self.0) + } +} + +impl SimpleSerialize for Hash32 { + fn is_composite_type() -> bool { + <[u8; 32] as SimpleSerialize>::is_composite_type() + } +} + +impl SszSized for VersionedHash { + fn is_variable_size() -> bool { + <[u8; 32] as SszSized>::is_variable_size() + } + + fn size_hint() -> usize { + <[u8; 32] as SszSized>::size_hint() + } +} + +impl Serialize for VersionedHash { + fn serialize(&self, buffer: &mut Vec) -> Result { + Serialize::serialize(&self.0, buffer) + } +} + +impl Deserialize for VersionedHash { + fn deserialize(encoding: &[u8]) -> Result { + <[u8; 32] as Deserialize>::deserialize(encoding).map(Self) + } +} + +impl Merkleized for VersionedHash { + fn hash_tree_root(&self) -> Result { + Merkleized::hash_tree_root(&self.0) + } +} + +impl SimpleSerialize for VersionedHash { + fn is_composite_type() -> bool { + <[u8; 32] as SimpleSerialize>::is_composite_type() + } +} + +impl SszSized for ExecutionAddress { + fn is_variable_size() -> bool { + <[u8; 20] as SszSized>::is_variable_size() + } + + fn size_hint() -> usize { + <[u8; 20] as SszSized>::size_hint() + } +} + +impl Serialize for ExecutionAddress { + fn serialize(&self, buffer: &mut Vec) -> Result { + Serialize::serialize(&self.0, buffer) + } +} + +impl Deserialize for ExecutionAddress { + fn deserialize(encoding: &[u8]) -> Result { + <[u8; 20] as Deserialize>::deserialize(encoding).map(Self) + } +} + +impl Merkleized for ExecutionAddress { + fn hash_tree_root(&self) -> Result { + Merkleized::hash_tree_root(&self.0) + } +} + +impl SimpleSerialize for ExecutionAddress { + fn is_composite_type() -> bool { + <[u8; 20] as SimpleSerialize>::is_composite_type() + } +} + +impl SszSized for Version { + fn is_variable_size() -> bool { + <[u8; 4] as SszSized>::is_variable_size() + } + + fn size_hint() -> usize { + <[u8; 4] as SszSized>::size_hint() + } +} + +impl Serialize for Version { + fn serialize(&self, buffer: &mut Vec) -> Result { + Serialize::serialize(&self.0, buffer) + } +} + +impl Deserialize for Version { + fn deserialize(encoding: &[u8]) -> Result { + <[u8; 4] as Deserialize>::deserialize(encoding).map(Self) + } +} + +impl Merkleized for Version { + fn hash_tree_root(&self) -> Result { + Merkleized::hash_tree_root(&self.0) + } +} + +impl SimpleSerialize for Version { + fn is_composite_type() -> bool { + <[u8; 4] as SimpleSerialize>::is_composite_type() + } +} + +impl SszSized for ForkDigest { + fn is_variable_size() -> bool { + <[u8; 4] as SszSized>::is_variable_size() + } + + fn size_hint() -> usize { + <[u8; 4] as SszSized>::size_hint() + } +} + +impl Serialize for ForkDigest { + fn serialize(&self, buffer: &mut Vec) -> Result { + Serialize::serialize(&self.0, buffer) + } +} + +impl Deserialize for ForkDigest { + fn deserialize(encoding: &[u8]) -> Result { + <[u8; 4] as Deserialize>::deserialize(encoding).map(Self) + } +} + +impl Merkleized for ForkDigest { + fn hash_tree_root(&self) -> Result { + Merkleized::hash_tree_root(&self.0) + } +} + +impl SimpleSerialize for ForkDigest { + fn is_composite_type() -> bool { + <[u8; 4] as SimpleSerialize>::is_composite_type() + } +} + +impl SszSized for Domain { + fn is_variable_size() -> bool { + <[u8; 32] as SszSized>::is_variable_size() + } + + fn size_hint() -> usize { + <[u8; 32] as SszSized>::size_hint() + } +} + +impl Serialize for Domain { + fn serialize(&self, buffer: &mut Vec) -> Result { + Serialize::serialize(&self.0, buffer) + } +} + +impl Deserialize for Domain { + fn deserialize(encoding: &[u8]) -> Result { + <[u8; 32] as Deserialize>::deserialize(encoding).map(Self) + } +} + +impl Merkleized for Domain { + fn hash_tree_root(&self) -> Result { + Merkleized::hash_tree_root(&self.0) + } +} + +impl SimpleSerialize for Domain { + fn is_composite_type() -> bool { + <[u8; 32] as SimpleSerialize>::is_composite_type() + } +} + +impl SszSized for DomainType { + fn is_variable_size() -> bool { + <[u8; 4] as SszSized>::is_variable_size() + } + + fn size_hint() -> usize { + <[u8; 4] as SszSized>::size_hint() + } +} + +impl Serialize for DomainType { + fn serialize(&self, buffer: &mut Vec) -> Result { + Serialize::serialize(&self.0, buffer) + } +} + +impl Deserialize for DomainType { + fn deserialize(encoding: &[u8]) -> Result { + <[u8; 4] as Deserialize>::deserialize(encoding).map(Self) + } +} + +impl Merkleized for DomainType { + fn hash_tree_root(&self) -> Result { + Merkleized::hash_tree_root(&self.0) + } +} + +impl SimpleSerialize for DomainType { + fn is_composite_type() -> bool { + <[u8; 4] as SimpleSerialize>::is_composite_type() + } +} diff --git a/moonglass/src/primitives/numeric.rs b/moonglass-core/src/primitives/numeric.rs similarity index 71% rename from moonglass/src/primitives/numeric.rs rename to moonglass-core/src/primitives/numeric.rs index 037d8a2..7d778eb 100644 --- a/moonglass/src/primitives/numeric.rs +++ b/moonglass-core/src/primitives/numeric.rs @@ -7,7 +7,11 @@ use crate::error::PrimitivesError; /// /// This checks only that the protocol value fits the host pointer width. /// Collection bounds remain the caller's responsibility. -fn u64_to_usize(value: u64) -> usize { +/// +/// # Panics +/// +/// Panics if `value` does not fit in `usize` on this host. +pub fn u64_to_usize(value: u64) -> usize { usize::try_from(value).expect("protocol index fits host usize") } @@ -22,42 +26,36 @@ pub struct Slot(pub u64); impl Slot { /// Construct from a raw slot number. #[inline] - #[must_use] pub const fn new(value: u64) -> Self { Self(value) } /// Return the raw slot number. #[inline] - #[must_use] pub const fn as_u64(self) -> u64 { self.0 } /// Converts to `usize` for ring-buffer indexing. #[inline] - #[must_use] pub fn as_usize(self) -> usize { u64_to_usize(self.0) } /// The epoch this slot belongs to. #[inline] - #[must_use] pub const fn epoch(self) -> Epoch { Epoch(self.0 / SLOTS_PER_EPOCH as u64) } /// Saturating addition, clamped to `Slot(u64::MAX)`. #[inline] - #[must_use] pub const fn saturating_add(self, rhs: u64) -> Self { Self(self.0.saturating_add(rhs)) } /// Saturating subtraction, clamped to `Slot(0)`. #[inline] - #[must_use] pub const fn saturating_sub(self, rhs: u64) -> Self { Self(self.0.saturating_sub(rhs)) } @@ -71,42 +69,36 @@ pub struct Epoch(pub u64); impl Epoch { /// Construct from a raw epoch number. #[inline] - #[must_use] pub const fn new(value: u64) -> Self { Self(value) } /// Return the raw epoch number. #[inline] - #[must_use] pub const fn as_u64(self) -> u64 { self.0 } /// Converts to `usize` for collection indexing. #[inline] - #[must_use] pub fn as_usize(self) -> usize { u64_to_usize(self.0) } /// First slot in this epoch, saturating for sentinel epochs. #[inline] - #[must_use] pub const fn start_slot(self) -> Slot { Slot(self.0.saturating_mul(SLOTS_PER_EPOCH as u64)) } /// Saturating addition, clamped to `Epoch(u64::MAX)`. #[inline] - #[must_use] pub const fn saturating_add(self, rhs: u64) -> Self { Self(self.0.saturating_add(rhs)) } /// Saturating subtraction, clamped to `Epoch(0)`. #[inline] - #[must_use] pub const fn saturating_sub(self, rhs: u64) -> Self { Self(self.0.saturating_sub(rhs)) } @@ -120,28 +112,24 @@ pub struct ValidatorIndex(pub u64); impl ValidatorIndex { /// Construct from a raw validator-registry position. #[inline] - #[must_use] pub const fn new(value: u64) -> Self { Self(value) } /// Return the raw validator-registry position. #[inline] - #[must_use] pub const fn as_u64(self) -> u64 { self.0 } /// Converts to `usize` for collection indexing. #[inline] - #[must_use] pub fn as_usize(self) -> usize { u64_to_usize(self.0) } /// True if this index encodes a [`BuilderIndex`]. #[inline] - #[must_use] pub const fn is_builder_index(self) -> bool { self.0 & BUILDER_INDEX_FLAG != 0 } @@ -170,21 +158,18 @@ pub struct BuilderIndex(pub u64); impl BuilderIndex { /// Construct from a raw builder-registry position. #[inline] - #[must_use] pub const fn new(value: u64) -> Self { Self(value) } /// Return the raw builder-registry position. #[inline] - #[must_use] pub const fn as_u64(self) -> u64 { self.0 } /// Converts to `usize` for collection indexing. #[inline] - #[must_use] pub fn as_usize(self) -> usize { u64_to_usize(self.0) } @@ -210,21 +195,168 @@ pub struct CommitteeIndex(pub u64); impl CommitteeIndex { /// Construct from a raw committee index. #[inline] - #[must_use] pub const fn new(value: u64) -> Self { Self(value) } /// Return the raw committee index. #[inline] - #[must_use] pub const fn as_u64(self) -> u64 { self.0 } /// Converts to `usize` for collection indexing. #[inline] - #[must_use] + pub fn as_usize(self) -> usize { + u64_to_usize(self.0) + } +} + +/// Data-column index used by availability sampling. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct ColumnIndex(pub u64); + +impl ColumnIndex { + /// Construct from a raw column index. + #[inline] + pub const fn new(value: u64) -> Self { + Self(value) + } + + /// Return the raw column index. + #[inline] + pub const fn as_u64(self) -> u64 { + self.0 + } + + /// Converts to `usize` for collection indexing. + #[inline] + pub fn as_usize(self) -> usize { + u64_to_usize(self.0) + } +} + +/// Matrix-row index used by data availability recovery. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct RowIndex(pub u64); + +impl RowIndex { + /// Construct from a raw row index. + #[inline] + pub const fn new(value: u64) -> Self { + Self(value) + } + + /// Return the raw row index. + #[inline] + pub const fn as_u64(self) -> u64 { + self.0 + } + + /// Converts to `usize` for collection indexing. + #[inline] + pub fn as_usize(self) -> usize { + u64_to_usize(self.0) + } +} + +/// Custody group index used by data availability sampling. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct CustodyIndex(pub u64); + +impl CustodyIndex { + /// Construct from a raw custody group index. + #[inline] + pub const fn new(value: u64) -> Self { + Self(value) + } + + /// Return the raw custody group index. + #[inline] + pub const fn as_u64(self) -> u64 { + self.0 + } + + /// Converts to `usize` for collection indexing. + #[inline] + pub fn as_usize(self) -> usize { + u64_to_usize(self.0) + } +} + +/// Gossip subnet index. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct SubnetId(pub u64); + +impl SubnetId { + /// Construct from a raw subnet index. + #[inline] + pub const fn new(value: u64) -> Self { + Self(value) + } + + /// Return the raw subnet index. + #[inline] + pub const fn as_u64(self) -> u64 { + self.0 + } + + /// Converts to `usize` for collection indexing. + #[inline] + pub fn as_usize(self) -> usize { + u64_to_usize(self.0) + } +} + +/// Cell index within the extended blob domain. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct CellIndex(pub u64); + +impl CellIndex { + /// Construct from a raw cell index. + #[inline] + pub const fn new(value: u64) -> Self { + Self(value) + } + + /// Return the raw cell index. + #[inline] + pub const fn as_u64(self) -> u64 { + self.0 + } + + /// Converts to `usize` for collection indexing. + #[inline] + pub fn as_usize(self) -> usize { + u64_to_usize(self.0) + } +} + +/// Index into a deduplicated KZG commitment list. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct CommitmentIndex(pub u64); + +impl CommitmentIndex { + /// Construct from a raw commitment index. + #[inline] + pub const fn new(value: u64) -> Self { + Self(value) + } + + /// Return the raw commitment index. + #[inline] + pub const fn as_u64(self) -> u64 { + self.0 + } + + /// Converts to `usize` for collection indexing. + #[inline] pub fn as_usize(self) -> usize { u64_to_usize(self.0) } @@ -238,21 +370,18 @@ pub struct WithdrawalIndex(pub u64); impl WithdrawalIndex { /// Construct from a raw withdrawal sequence index. #[inline] - #[must_use] pub const fn new(value: u64) -> Self { Self(value) } /// Return the raw withdrawal sequence index. #[inline] - #[must_use] pub const fn as_u64(self) -> u64 { self.0 } /// Converts to `usize` for collection indexing. #[inline] - #[must_use] pub fn as_usize(self) -> usize { u64_to_usize(self.0) } @@ -271,35 +400,30 @@ impl Gwei { /// Construct from a raw gwei amount. #[inline] - #[must_use] pub const fn new(value: u64) -> Self { Self(value) } /// Return the raw gwei amount. #[inline] - #[must_use] pub const fn as_u64(self) -> u64 { self.0 } /// Saturating addition, clamped to `u64::MAX`. #[inline] - #[must_use] pub const fn saturating_add(self, rhs: Self) -> Self { Self(self.0.saturating_add(rhs.0)) } /// Saturating subtraction, clamped to `0`. Required by penalty math. #[inline] - #[must_use] pub const fn saturating_sub(self, rhs: Self) -> Self { Self(self.0.saturating_sub(rhs.0)) } /// Checked addition, returning `None` on overflow. #[inline] - #[must_use] pub const fn checked_add(self, rhs: Self) -> Option { match self.0.checked_add(rhs.0) { Some(v) => Some(Self(v)), @@ -309,7 +433,6 @@ impl Gwei { /// Checked subtraction, returning `None` on underflow. #[inline] - #[must_use] pub const fn checked_sub(self, rhs: Self) -> Option { match self.0.checked_sub(rhs.0) { Some(v) => Some(Self(v)), @@ -331,14 +454,12 @@ impl ParticipationFlags { /// Construct from raw flag bits. #[inline] - #[must_use] pub const fn new(value: u8) -> Self { Self(value) } /// Return the raw flag bits. #[inline] - #[must_use] pub const fn as_u8(self) -> u8 { self.0 } diff --git a/moonglass-core/src/primitives/numeric_ssz.rs b/moonglass-core/src/primitives/numeric_ssz.rs new file mode 100644 index 0000000..bbb5fcf --- /dev/null +++ b/moonglass-core/src/primitives/numeric_ssz.rs @@ -0,0 +1,417 @@ +//! SSZ `Serialize`/`Deserialize`/`Merkleized` impls for the numeric newtypes. + +use crate::ssz::prelude::*; + +use super::{ + BuilderIndex, CellIndex, ColumnIndex, CommitmentIndex, CommitteeIndex, CustodyIndex, Epoch, + Gwei, ParticipationFlags, RowIndex, Slot, SubnetId, ValidatorIndex, WithdrawalIndex, +}; + +// SSZ impls for uint-wrapping newtypes keep them basic, so lists and vectors +// pack their bytes before merkleization instead of rooting each element. + +impl SszSized for Slot { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for Slot { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for Slot { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for Slot { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for Slot { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for Epoch { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for Epoch { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for Epoch { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for Epoch { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for Epoch { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for ValidatorIndex { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for ValidatorIndex { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for ValidatorIndex { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for ValidatorIndex { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for ValidatorIndex { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for BuilderIndex { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for BuilderIndex { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for BuilderIndex { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for BuilderIndex { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for BuilderIndex { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for CommitteeIndex { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for CommitteeIndex { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for CommitteeIndex { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for CommitteeIndex { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for CommitteeIndex { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for ColumnIndex { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for ColumnIndex { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for ColumnIndex { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for ColumnIndex { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for ColumnIndex { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for RowIndex { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for RowIndex { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for RowIndex { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for RowIndex { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for RowIndex { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for CustodyIndex { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for CustodyIndex { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for CustodyIndex { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for CustodyIndex { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for CustodyIndex { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for SubnetId { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for SubnetId { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for SubnetId { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for SubnetId { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for SubnetId { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for CellIndex { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for CellIndex { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for CellIndex { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for CellIndex { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for CellIndex { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for CommitmentIndex { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for CommitmentIndex { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for CommitmentIndex { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for CommitmentIndex { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for CommitmentIndex { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for WithdrawalIndex { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for WithdrawalIndex { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for WithdrawalIndex { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for WithdrawalIndex { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for WithdrawalIndex { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for Gwei { + fn is_variable_size() -> bool { + u64::is_variable_size() + } + fn size_hint() -> usize { + u64::size_hint() + } +} +impl Serialize for Gwei { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for Gwei { + fn deserialize(encoding: &[u8]) -> Result { + u64::deserialize(encoding).map(Self) + } +} +impl Merkleized for Gwei { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for Gwei { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for ParticipationFlags { + fn is_variable_size() -> bool { + u8::is_variable_size() + } + fn size_hint() -> usize { + u8::size_hint() + } +} +impl Serialize for ParticipationFlags { + fn serialize(&self, buffer: &mut Vec) -> Result { + self.0.serialize(buffer) + } +} +impl Deserialize for ParticipationFlags { + fn deserialize(encoding: &[u8]) -> Result { + u8::deserialize(encoding).map(Self) + } +} +impl Merkleized for ParticipationFlags { + fn hash_tree_root(&self) -> Result { + self.0.hash_tree_root() + } +} +impl SimpleSerialize for ParticipationFlags { + fn is_composite_type() -> bool { + false + } +} diff --git a/moonglass-core/src/ssz.rs b/moonglass-core/src/ssz.rs new file mode 100644 index 0000000..f333110 --- /dev/null +++ b/moonglass-core/src/ssz.rs @@ -0,0 +1,39 @@ +//! In-house SSZ encoding, decoding, and hash-tree-root support for Moonglass. + +/// Number of bytes in a merkle chunk. +pub const BYTES_PER_CHUNK: usize = 32; +/// Number of bytes in an SSZ variable-offset word. +pub const BYTES_PER_LENGTH_OFFSET: usize = 4; + +pub mod basic; +pub mod bitfield; +pub mod codec; +pub mod collection; +pub mod container; +pub mod error; +pub mod list; +pub mod merkle; +pub mod node; +pub mod traits; +pub mod vector; + +pub use bitfield::*; +pub use codec::*; +pub use collection::*; +pub use container::*; +pub use error::*; +pub use list::*; +pub use merkle::*; +pub use node::*; +pub use traits::*; +pub use vector::*; + +/// Public prelude for SSZ callers. +pub mod prelude { + pub use super::{ + Bitlist, Bitvector, ContainerDecoder, ContainerEncoder, Deserialize, DeserializeError, + List, MerkleizationError, Merkleized, Node, Serialize, SerializeError, SimpleSerialize, + SszSized, Vector, container_is_variable_size, container_size_hint, field_layout, + merkleize_byte_sequence, merkleize_roots, + }; +} diff --git a/moonglass-core/src/ssz/basic.rs b/moonglass-core/src/ssz/basic.rs new file mode 100644 index 0000000..51bfff2 --- /dev/null +++ b/moonglass-core/src/ssz/basic.rs @@ -0,0 +1,271 @@ +//! SSZ impls for basic Rust values. + +use super::{ + Deserialize, DeserializeError, MerkleizationError, Merkleized, Node, Serialize, SerializeError, + SimpleSerialize, SszSized, basic_root, deserialize_fixed_bytes, + merkleize_byte_sequence_with_limit, +}; + +impl SszSized for u8 { + fn is_variable_size() -> bool { + false + } + + fn size_hint() -> usize { + 1 + } +} + +impl Serialize for u8 { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(&self.to_le_bytes()); + Ok(1) + } +} + +impl Deserialize for u8 { + fn deserialize(encoding: &[u8]) -> Result { + Ok(Self::from_le_bytes(deserialize_fixed_bytes::<1>(encoding)?)) + } +} + +impl Merkleized for u8 { + fn hash_tree_root(&self) -> Result { + basic_root(&self.to_le_bytes()) + } +} + +impl SimpleSerialize for u8 { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for u16 { + fn is_variable_size() -> bool { + false + } + + fn size_hint() -> usize { + 2 + } +} + +impl Serialize for u16 { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(&self.to_le_bytes()); + Ok(2) + } +} + +impl Deserialize for u16 { + fn deserialize(encoding: &[u8]) -> Result { + Ok(Self::from_le_bytes(deserialize_fixed_bytes::<2>(encoding)?)) + } +} + +impl Merkleized for u16 { + fn hash_tree_root(&self) -> Result { + basic_root(&self.to_le_bytes()) + } +} + +impl SimpleSerialize for u16 { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for u32 { + fn is_variable_size() -> bool { + false + } + + fn size_hint() -> usize { + 4 + } +} + +impl Serialize for u32 { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(&self.to_le_bytes()); + Ok(4) + } +} + +impl Deserialize for u32 { + fn deserialize(encoding: &[u8]) -> Result { + Ok(Self::from_le_bytes(deserialize_fixed_bytes::<4>(encoding)?)) + } +} + +impl Merkleized for u32 { + fn hash_tree_root(&self) -> Result { + basic_root(&self.to_le_bytes()) + } +} + +impl SimpleSerialize for u32 { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for u64 { + fn is_variable_size() -> bool { + false + } + + fn size_hint() -> usize { + 8 + } +} + +impl Serialize for u64 { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(&self.to_le_bytes()); + Ok(8) + } +} + +impl Deserialize for u64 { + fn deserialize(encoding: &[u8]) -> Result { + Ok(Self::from_le_bytes(deserialize_fixed_bytes::<8>(encoding)?)) + } +} + +impl Merkleized for u64 { + fn hash_tree_root(&self) -> Result { + basic_root(&self.to_le_bytes()) + } +} + +impl SimpleSerialize for u64 { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for u128 { + fn is_variable_size() -> bool { + false + } + + fn size_hint() -> usize { + 16 + } +} + +impl Serialize for u128 { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(&self.to_le_bytes()); + Ok(16) + } +} + +impl Deserialize for u128 { + fn deserialize(encoding: &[u8]) -> Result { + Ok(Self::from_le_bytes(deserialize_fixed_bytes::<16>( + encoding, + )?)) + } +} + +impl Merkleized for u128 { + fn hash_tree_root(&self) -> Result { + basic_root(&self.to_le_bytes()) + } +} + +impl SimpleSerialize for u128 { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for bool { + fn is_variable_size() -> bool { + false + } + fn size_hint() -> usize { + 1 + } +} + +impl Serialize for bool { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.push(u8::from(*self)); + Ok(1) + } +} + +impl Deserialize for bool { + fn deserialize(encoding: &[u8]) -> Result { + let byte = u8::deserialize(encoding)?; + match byte { + 0 => Ok(false), + 1 => Ok(true), + other => Err(DeserializeError::InvalidBool(other)), + } + } +} + +impl Merkleized for bool { + fn hash_tree_root(&self) -> Result { + u8::from(*self).hash_tree_root() + } +} + +impl SimpleSerialize for bool { + fn is_composite_type() -> bool { + false + } +} + +impl SszSized for [u8; N] { + fn is_variable_size() -> bool { + false + } + fn size_hint() -> usize { + N + } +} + +impl Serialize for [u8; N] { + fn serialize(&self, buffer: &mut Vec) -> Result { + buffer.extend_from_slice(self); + Ok(N) + } +} + +impl Deserialize for [u8; N] { + fn deserialize(encoding: &[u8]) -> Result { + if encoding.len() < N { + return Err(DeserializeError::ExpectedFurtherInput { + provided: encoding.len(), + expected: N, + }); + } + if encoding.len() > N { + return Err(DeserializeError::AdditionalInput { + provided: encoding.len(), + expected: N, + }); + } + let mut out = [0u8; N]; + out.copy_from_slice(encoding); + Ok(out) + } +} + +impl Merkleized for [u8; N] { + fn hash_tree_root(&self) -> Result { + merkleize_byte_sequence_with_limit(self, N) + } +} + +impl SimpleSerialize for [u8; N] { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/ssz/bitfield.rs b/moonglass-core/src/ssz/bitfield.rs new file mode 100644 index 0000000..fc57ca0 --- /dev/null +++ b/moonglass-core/src/ssz/bitfield.rs @@ -0,0 +1,447 @@ +//! SSZ bitvector and bitlist support. + +use std::array::IntoIter as ArrayIntoIter; +use std::fmt; +use std::ops::{Index, IndexMut}; +use std::vec::IntoIter as VecIntoIter; + +use super::{ + BYTES_PER_LENGTH_OFFSET, CollectionError, Deserialize, DeserializeError, MerkleizationError, + Merkleized, Node, Serialize, SerializeError, SimpleSerialize, SszSized, + merkleize_byte_sequence_with_limit, mix_in_length, +}; + +/// Fixed-length bitvector. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Bitvector { + /// Backing bits. + bits: [bool; N], +} + +impl Bitvector { + /// Number of bits. + pub const fn len(&self) -> usize { + N + } + + /// Whether this bitvector has zero length. + pub const fn is_empty(&self) -> bool { + N == 0 + } + + /// Get a bit. + pub fn get(&self, index: usize) -> Option { + self.bits.get(index).copied() + } + + /// Set a bit if the index is in range. + pub fn set(&mut self, index: usize, value: bool) { + if let Some(bit) = self.bits.get_mut(index) { + *bit = value; + } + } + + /// Iterate over bits. + pub fn iter(&self) -> impl Iterator { + self.bits.iter() + } + + /// Iterate over mutable bits. + pub fn iter_mut(&mut self) -> impl Iterator { + self.bits.iter_mut() + } + + /// Borrow all bits. + pub const fn as_slice(&self) -> &[bool] { + &self.bits + } + + /// Mutably borrow all bits. + pub fn as_mut_slice(&mut self) -> &mut [bool] { + &mut self.bits + } + + /// Count set bits. + pub fn count_ones(&self) -> usize { + self.bits.iter().filter(|bit| **bit).count() + } +} + +impl Default for Bitvector { + fn default() -> Self { + Self { bits: [false; N] } + } +} + +impl fmt::Debug for Bitvector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.bits.fmt(f) + } +} + +impl Index for Bitvector { + type Output = bool; + + fn index(&self, index: usize) -> &Self::Output { + &self.bits[index] + } +} + +impl IndexMut for Bitvector { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.bits[index] + } +} + +impl IntoIterator for Bitvector { + type IntoIter = ArrayIntoIter; + type Item = bool; + + fn into_iter(self) -> Self::IntoIter { + self.bits.into_iter() + } +} + +impl AsRef<[bool]> for Bitvector { + fn as_ref(&self) -> &[bool] { + &self.bits + } +} + +impl AsMut<[bool]> for Bitvector { + fn as_mut(&mut self) -> &mut [bool] { + &mut self.bits + } +} + +impl From<[bool; N]> for Bitvector { + fn from(bits: [bool; N]) -> Self { + Self { bits } + } +} + +impl TryFrom> for Bitvector { + type Error = CollectionError; + + fn try_from(value: Vec) -> Result { + if value.len() != N { + return Err(CollectionError::WrongLength { + len: value.len(), + expected: N, + }); + } + let mut bits = [false; N]; + bits.copy_from_slice(&value); + Ok(Self { bits }) + } +} + +impl SszSized for Bitvector { + fn is_variable_size() -> bool { + false + } + fn size_hint() -> usize { + bitfield_byte_len(N) + } +} + +impl Serialize for Bitvector { + fn serialize(&self, buffer: &mut Vec) -> Result { + let bytes = pack_bits(&self.bits); + let len = bytes.len(); + buffer.extend_from_slice(&bytes); + Ok(len) + } +} + +impl Deserialize for Bitvector { + fn deserialize(encoding: &[u8]) -> Result { + let expected = bitfield_byte_len(N); + if encoding.len() < expected { + return Err(DeserializeError::ExpectedFurtherInput { + provided: encoding.len(), + expected, + }); + } + if encoding.len() > expected { + return Err(DeserializeError::AdditionalInput { + provided: encoding.len(), + expected, + }); + } + validate_padding_bits(encoding, N)?; + let mut bits = [false; N]; + for (index, bit) in bits.iter_mut().enumerate() { + *bit = bit_at(encoding, index); + } + Ok(Self { bits }) + } +} + +impl Merkleized for Bitvector { + fn hash_tree_root(&self) -> Result { + let bytes = pack_bits(&self.bits); + merkleize_byte_sequence_with_limit(&bytes, bitfield_byte_len(N)) + } +} + +impl SimpleSerialize for Bitvector { + fn is_composite_type() -> bool { + true + } +} + +/// Variable-length bitlist. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Bitlist { + /// Backing bits. + bits: Vec, +} + +impl Bitlist { + /// Number of stored bits. + pub fn len(&self) -> usize { + self.bits.len() + } + + /// Whether there are no bits. + pub fn is_empty(&self) -> bool { + self.bits.is_empty() + } + + /// Append a bit when capacity permits. + pub fn try_push(&mut self, value: bool) -> Result<(), CollectionError> { + if self.bits.len() >= N { + return Err(CollectionError::TooLong { + len: self.bits.len().saturating_add(1), + limit: N, + }); + } + + self.bits.push(value); + Ok(()) + } + + /// Append a bit. + pub fn push(&mut self, value: bool) -> Result<(), CollectionError> { + self.try_push(value) + } + + /// Get a bit. + pub fn get(&self, index: usize) -> Option { + self.bits.get(index).copied() + } + + /// Set a bit if the index is in range. + pub fn set(&mut self, index: usize, value: bool) { + if let Some(bit) = self.bits.get_mut(index) { + *bit = value; + } + } + + /// Borrow all bits. + pub fn as_slice(&self) -> &[bool] { + &self.bits + } + + /// Mutably borrow all bits. + pub fn as_mut_slice(&mut self) -> &mut [bool] { + &mut self.bits + } + + /// Iterate over bits. + pub fn iter(&self) -> impl Iterator { + self.bits.iter() + } + + /// Iterate over mutable bits. + pub fn iter_mut(&mut self) -> impl Iterator { + self.bits.iter_mut() + } + + /// Count set bits. + pub fn count_ones(&self) -> usize { + self.bits.iter().filter(|bit| **bit).count() + } +} + +impl Default for Bitlist { + fn default() -> Self { + Self { bits: Vec::new() } + } +} + +impl fmt::Debug for Bitlist { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.bits.fmt(f) + } +} + +impl Index for Bitlist { + type Output = bool; + + fn index(&self, index: usize) -> &Self::Output { + &self.bits[index] + } +} + +impl IndexMut for Bitlist { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.bits[index] + } +} + +impl IntoIterator for Bitlist { + type IntoIter = VecIntoIter; + type Item = bool; + + fn into_iter(self) -> Self::IntoIter { + self.bits.into_iter() + } +} + +impl AsRef<[bool]> for Bitlist { + fn as_ref(&self) -> &[bool] { + &self.bits + } +} + +impl AsMut<[bool]> for Bitlist { + fn as_mut(&mut self) -> &mut [bool] { + &mut self.bits + } +} + +impl From> for Vec { + fn from(value: Bitlist) -> Self { + value.bits + } +} + +impl TryFrom> for Bitlist { + type Error = CollectionError; + + fn try_from(value: Vec) -> Result { + if value.len() > N { + return Err(CollectionError::TooLong { + len: value.len(), + limit: N, + }); + } + Ok(Self { bits: value }) + } +} + +impl SszSized for Bitlist { + fn is_variable_size() -> bool { + true + } + fn size_hint() -> usize { + BYTES_PER_LENGTH_OFFSET + } +} + +impl Serialize for Bitlist { + fn serialize(&self, buffer: &mut Vec) -> Result { + if self.bits.len() > N { + return Err(SerializeError::ListTooLong { + len: self.bits.len(), + limit: N, + }); + } + let mut bytes = pack_bits(&self.bits); + let delimiter_index = self.bits.len() % 8; + if delimiter_index == 0 { + bytes.push(1); + } else if let Some(last) = bytes.last_mut() { + *last |= 1u8 << delimiter_index; + } + let len = bytes.len(); + buffer.extend_from_slice(&bytes); + Ok(len) + } +} + +impl Deserialize for Bitlist { + fn deserialize(encoding: &[u8]) -> Result { + if encoding.is_empty() { + return Err(DeserializeError::MissingBitlistDelimiter); + } + let Some(last) = encoding.last().copied() else { + return Err(DeserializeError::MissingBitlistDelimiter); + }; + if last == 0 { + return Err(DeserializeError::MissingBitlistDelimiter); + } + let delimiter_index = 7usize.saturating_sub(last.leading_zeros() as usize); + let bit_len = (encoding.len() - 1) * 8 + delimiter_index; + if bit_len > N { + return Err(DeserializeError::ListTooLong { + len: bit_len, + limit: N, + }); + } + let mut payload = encoding.to_vec(); + let last_index = payload.len() - 1; + payload[last_index] &= !(1u8 << delimiter_index); + let mut bits = Vec::with_capacity(bit_len); + for i in 0..bit_len { + bits.push(bit_at(&payload, i)); + } + Ok(Self { bits }) + } +} + +impl Merkleized for Bitlist { + fn hash_tree_root(&self) -> Result { + if self.bits.len() > N { + return Err(MerkleizationError::ListTooLong { + len: self.bits.len(), + limit: N, + }); + } + let bytes = pack_bits(&self.bits); + let root = merkleize_byte_sequence_with_limit(&bytes, bitfield_byte_len(N))?; + Ok(mix_in_length(root, self.bits.len())) + } +} + +impl SimpleSerialize for Bitlist { + fn is_composite_type() -> bool { + true + } +} + +/// Number of bytes needed to carry `bits` bitfield bits. +pub const fn bitfield_byte_len(bits: usize) -> usize { + bits.div_ceil(8) +} + +/// Pack booleans into SSZ little-endian bit order. +pub fn pack_bits(bits: &[bool]) -> Vec { + let mut bytes = vec![0u8; bitfield_byte_len(bits.len())]; + for (index, bit) in bits.iter().copied().enumerate() { + if bit { + bytes[index / 8] |= 1u8 << (index % 8); + } + } + bytes +} + +/// Read one little-endian bit from packed bytes. +pub fn bit_at(bytes: &[u8], index: usize) -> bool { + bytes[index / 8] & (1u8 << (index % 8)) != 0 +} + +/// Reject set padding bits after the logical bitfield length. +pub fn validate_padding_bits(bytes: &[u8], bits: usize) -> Result<(), DeserializeError> { + if bits == 0 || bits.is_multiple_of(8) { + return Ok(()); + } + let used_bits = bits % 8; + let mask = !((1u8 << used_bits) - 1); + if bytes.last().copied().unwrap_or_default() & mask != 0 { + return Err(DeserializeError::NonZeroPaddingBits); + } + Ok(()) +} diff --git a/moonglass-core/src/ssz/codec.rs b/moonglass-core/src/ssz/codec.rs new file mode 100644 index 0000000..863f6e3 --- /dev/null +++ b/moonglass-core/src/ssz/codec.rs @@ -0,0 +1,233 @@ +//! SSZ offset, sequence, and byte encoding helpers. + +use super::{ + BYTES_PER_LENGTH_OFFSET, Deserialize, DeserializeError, Serialize, SerializeError, SszSized, +}; + +/// Write a `uint32` SSZ offset. +pub fn write_offset(offset: usize, buffer: &mut Vec) -> Result<(), SerializeError> { + let offset = u32::try_from(offset).map_err(|_| SerializeError::OffsetOverflow)?; + buffer.extend_from_slice(&offset.to_le_bytes()); + Ok(()) +} + +/// Read a `uint32` SSZ offset at `cursor`. +pub fn read_offset_at(encoding: &[u8], cursor: usize) -> Result { + let end = cursor + .checked_add(BYTES_PER_LENGTH_OFFSET) + .ok_or(DeserializeError::OffsetOverflow)?; + if end > encoding.len() { + return Err(DeserializeError::ExpectedFurtherInput { + provided: encoding.len(), + expected: end, + }); + } + let mut bytes = [0u8; BYTES_PER_LENGTH_OFFSET]; + bytes.copy_from_slice(&encoding[cursor..end]); + Ok(u32::from_le_bytes(bytes) as usize) +} + +/// Validate variable offsets and return bounds with input length appended. +pub fn validate_variable_offsets( + offsets: &[usize], + fixed_end: usize, + input_len: usize, +) -> Result, DeserializeError> { + if offsets.is_empty() { + if fixed_end != input_len { + return Err(DeserializeError::AdditionalInput { + provided: input_len, + expected: fixed_end, + }); + } + return Ok(vec![input_len]); + } + let first = offsets[0]; + if first != fixed_end || first > input_len { + return Err(DeserializeError::InvalidOffset { + offset: first, + len: input_len, + }); + } + let mut bounds = Vec::with_capacity(offsets.len() + 1); + let mut previous = first; + bounds.push(first); + for offset in offsets.iter().copied().skip(1) { + if offset < previous { + return Err(DeserializeError::NonMonotonicOffset { previous, offset }); + } + if offset > input_len { + return Err(DeserializeError::InvalidOffset { + offset, + len: input_len, + }); + } + bounds.push(offset); + previous = offset; + } + bounds.push(input_len); + Ok(bounds) +} + +/// Serialize a homogeneous SSZ sequence. +pub fn serialize_sequence(values: &[T], buffer: &mut Vec) -> Result +where + T: SszSized + Serialize, +{ + if T::is_variable_size() { + let fixed_size = values + .len() + .checked_mul(BYTES_PER_LENGTH_OFFSET) + .ok_or(SerializeError::OffsetOverflow)?; + let mut fixed = Vec::with_capacity(fixed_size); + let mut variable = Vec::new(); + let mut offset = fixed_size; + for value in values { + write_offset(offset, &mut fixed)?; + offset = offset + .checked_add(value.serialize(&mut variable)?) + .ok_or(SerializeError::OffsetOverflow)?; + } + let written = fixed + .len() + .checked_add(variable.len()) + .ok_or(SerializeError::OffsetOverflow)?; + buffer.extend_from_slice(&fixed); + buffer.extend_from_slice(&variable); + Ok(written) + } else { + let start = buffer.len(); + for value in values { + value.serialize(buffer)?; + } + Ok(buffer.len() - start) + } +} + +/// Decode a bounded SSZ list into a vector. +pub fn deserialize_list_items( + encoding: &[u8], +) -> Result, DeserializeError> +where + T: SszSized + Deserialize, +{ + if T::is_variable_size() { + if encoding.is_empty() { + return Ok(Vec::new()); + } + let first_offset = read_offset_at(encoding, 0)?; + if first_offset == 0 { + return Err(DeserializeError::InvalidOffset { + offset: first_offset, + len: encoding.len(), + }); + } + if first_offset % BYTES_PER_LENGTH_OFFSET != 0 { + return Err(DeserializeError::InvalidOffset { + offset: first_offset, + len: encoding.len(), + }); + } + let count = first_offset / BYTES_PER_LENGTH_OFFSET; + if count > N { + return Err(DeserializeError::ListTooLong { + len: count, + limit: N, + }); + } + deserialize_variable_items::(encoding, count) + } else { + let item_size = T::size_hint(); + if item_size == 0 { + return Err(DeserializeError::InvalidOffset { + offset: 0, + len: encoding.len(), + }); + } + if !encoding.len().is_multiple_of(item_size) { + return Err(DeserializeError::AdditionalInput { + provided: encoding.len(), + expected: encoding.len() / item_size * item_size, + }); + } + let count = encoding.len() / item_size; + if count > N { + return Err(DeserializeError::ListTooLong { + len: count, + limit: N, + }); + } + encoding + .chunks_exact(item_size) + .map(T::deserialize) + .collect() + } +} + +/// Decode a fixed-length SSZ vector into a vector. +pub fn deserialize_vector_items( + encoding: &[u8], +) -> Result, DeserializeError> +where + T: SszSized + Deserialize, +{ + if T::is_variable_size() { + if N == 0 { + if encoding.is_empty() { + return Ok(Vec::new()); + } + return Err(DeserializeError::AdditionalInput { + provided: encoding.len(), + expected: 0, + }); + } + deserialize_variable_items::(encoding, N) + } else { + let expected = N + .checked_mul(T::size_hint()) + .ok_or(DeserializeError::OffsetOverflow)?; + if encoding.len() < expected { + return Err(DeserializeError::ExpectedFurtherInput { + provided: encoding.len(), + expected, + }); + } + if encoding.len() > expected { + return Err(DeserializeError::AdditionalInput { + provided: encoding.len(), + expected, + }); + } + encoding + .chunks_exact(T::size_hint()) + .map(T::deserialize) + .collect() + } +} + +/// Decode variable-size SSZ sequence items. +pub fn deserialize_variable_items( + encoding: &[u8], + count: usize, +) -> Result, DeserializeError> +where + T: SszSized + Deserialize, +{ + let fixed_end = count + .checked_mul(BYTES_PER_LENGTH_OFFSET) + .ok_or(DeserializeError::OffsetOverflow)?; + if fixed_end > encoding.len() { + return Err(DeserializeError::ExpectedFurtherInput { + provided: encoding.len(), + expected: fixed_end, + }); + } + let mut offsets = Vec::with_capacity(count); + for i in 0..count { + offsets.push(read_offset_at(encoding, i * BYTES_PER_LENGTH_OFFSET)?); + } + let bounds = validate_variable_offsets(&offsets, fixed_end, encoding.len())?; + (0..count) + .map(|i| T::deserialize(&encoding[bounds[i]..bounds[i + 1]])) + .collect() +} diff --git a/moonglass-core/src/ssz/collection.rs b/moonglass-core/src/ssz/collection.rs new file mode 100644 index 0000000..5ed6992 --- /dev/null +++ b/moonglass-core/src/ssz/collection.rs @@ -0,0 +1,38 @@ +//! SSZ homogeneous collection merkleization. + +use super::{ + MerkleizationError, Merkleized, Node, Serialize, SimpleSerialize, SszSized, + merkleize_byte_sequence_with_limit, merkleize_roots_with_limit, +}; + +/// Compute the collection root for a homogeneous SSZ sequence. +pub fn collection_root(values: &[T], limit: usize) -> Result +where + T: SszSized + Serialize + Merkleized + SimpleSerialize, +{ + if values.len() > limit { + return Err(MerkleizationError::ListTooLong { + len: values.len(), + limit, + }); + } + + if T::is_composite_type() { + let roots = values + .iter() + .map(Merkleized::hash_tree_root) + .collect::, _>>()?; + merkleize_roots_with_limit(&roots, limit) + } else { + let mut bytes = Vec::new(); + for value in values { + value + .serialize(&mut bytes) + .map_err(|_| MerkleizationError::LengthOverflow)?; + } + let byte_limit = limit + .checked_mul(T::size_hint()) + .ok_or(MerkleizationError::LengthOverflow)?; + merkleize_byte_sequence_with_limit(&bytes, byte_limit) + } +} diff --git a/moonglass-core/src/ssz/container.rs b/moonglass-core/src/ssz/container.rs new file mode 100644 index 0000000..b57a70c --- /dev/null +++ b/moonglass-core/src/ssz/container.rs @@ -0,0 +1,192 @@ +//! Helpers for explicit SSZ container implementations. + +use std::ops::Range; + +use super::{ + BYTES_PER_LENGTH_OFFSET, Deserialize, DeserializeError, Serialize, SerializeError, SszSized, + read_offset_at, validate_variable_offsets, write_offset, +}; + +/// Static SSZ layout for one container field. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FieldLayout { + /// Whether the field is variable-size SSZ. + pub variable_size: bool, + /// Fixed field size, or the fixed-section offset word for variable fields. + pub size_hint: usize, +} + +/// Return the layout contribution for an SSZ field type. +pub fn field_layout() -> FieldLayout { + FieldLayout { + variable_size: T::is_variable_size(), + size_hint: if T::is_variable_size() { + BYTES_PER_LENGTH_OFFSET + } else { + T::size_hint() + }, + } +} + +/// Return true when any container field has variable-size SSZ encoding. +pub fn container_is_variable_size(fields: &[FieldLayout]) -> bool { + fields.iter().any(|field| field.variable_size) +} + +/// Return the fixed-section size for an SSZ container layout. +pub fn container_size_hint(fields: &[FieldLayout]) -> usize { + fields.iter().map(|field| field.size_hint).sum() +} + +/// Incrementally serialize container fields in spec order. +pub struct ContainerEncoder { + /// Fixed section, including offset words for variable fields. + fixed: Vec, + /// Variable field payloads in field order. + variable: Vec, + /// Declared fixed-section length for this container. + fixed_size: usize, + /// Next variable payload offset from the start of the container. + variable_offset: usize, +} + +impl ContainerEncoder { + /// Start a container encoder with the fixed-section size for its type. + pub fn new(fixed_size: usize) -> Self { + Self { + fixed: Vec::with_capacity(fixed_size), + variable: Vec::new(), + fixed_size, + variable_offset: fixed_size, + } + } + + /// Start a container encoder for `T`. + pub fn for_type() -> Self + where + T: SszSized, + { + Self::new(T::size_hint()) + } + + /// Write one field in spec order. + pub fn write_field(&mut self, value: &T) -> Result<(), SerializeError> + where + T: SszSized + Serialize, + { + if T::is_variable_size() { + write_offset(self.variable_offset, &mut self.fixed)?; + self.variable_offset = self + .variable_offset + .checked_add(value.serialize(&mut self.variable)?) + .ok_or(SerializeError::OffsetOverflow)?; + } else { + value.serialize(&mut self.fixed)?; + } + Ok(()) + } + + /// Append the completed container encoding to `buffer`. + pub fn finish(self, buffer: &mut Vec) -> Result { + if self.fixed.len() != self.fixed_size { + return Err(SerializeError::ContainerFixedSize { + got: self.fixed.len(), + expected: self.fixed_size, + }); + } + let written = self + .fixed + .len() + .checked_add(self.variable.len()) + .ok_or(SerializeError::OffsetOverflow)?; + buffer.extend_from_slice(&self.fixed); + buffer.extend_from_slice(&self.variable); + Ok(written) + } +} + +/// Decode an SSZ container one field at a time in spec order. +pub struct ContainerDecoder { + /// Complete container encoding, owned so the decoder carries no lifetime. + /// + /// The owned copy is intentional. Borrowing the input would add a lifetime + /// parameter to this public type, which the project trades away in favor of + /// a lifetime-free public shape. + encoding: Vec, + /// Byte ranges for each field, in field order. + ranges: Vec>, + /// Next field to decode. + next: usize, +} + +impl ContainerDecoder { + /// Build a decoder from the container encoding and its field layouts. + pub fn new(encoding: &[u8], layouts: &[FieldLayout]) -> Result { + let ranges = container_field_ranges(encoding, layouts)?; + Ok(Self { + encoding: encoding.to_vec(), + ranges, + next: 0, + }) + } + + /// Decode the next field as `T`. + pub fn deserialize_next(&mut self) -> Result { + let Some(range) = self.ranges.get(self.next).cloned() else { + return Err(DeserializeError::MissingField("past end of layout")); + }; + self.next += 1; + T::deserialize(&self.encoding[range]) + } +} + +/// Resolve field byte ranges for a container encoding. +pub fn container_field_ranges( + encoding: &[u8], + layouts: &[FieldLayout], +) -> Result>, DeserializeError> { + let mut cursor = 0usize; + let mut ranges = vec![0..0; layouts.len()]; + let mut variable_fields = Vec::new(); + let mut variable_offsets = Vec::new(); + + for (field_index, layout) in layouts.iter().copied().enumerate() { + if layout.variable_size { + let offset = read_offset_at(encoding, cursor)?; + cursor = cursor + .checked_add(BYTES_PER_LENGTH_OFFSET) + .ok_or(DeserializeError::OffsetOverflow)?; + variable_fields.push(field_index); + variable_offsets.push(offset); + } else { + let end = cursor + .checked_add(layout.size_hint) + .ok_or(DeserializeError::OffsetOverflow)?; + if end > encoding.len() { + return Err(DeserializeError::ExpectedFurtherInput { + provided: encoding.len(), + expected: end, + }); + } + ranges[field_index] = cursor..end; + cursor = end; + } + } + + if variable_offsets.is_empty() { + if cursor != encoding.len() { + return Err(DeserializeError::AdditionalInput { + provided: encoding.len(), + expected: cursor, + }); + } + return Ok(ranges); + } + + let variable_bounds = validate_variable_offsets(&variable_offsets, cursor, encoding.len())?; + for (variable_index, field_index) in variable_fields.into_iter().enumerate() { + ranges[field_index] = variable_bounds[variable_index]..variable_bounds[variable_index + 1]; + } + + Ok(ranges) +} diff --git a/moonglass-core/src/ssz/error.rs b/moonglass-core/src/ssz/error.rs new file mode 100644 index 0000000..b233a3a --- /dev/null +++ b/moonglass-core/src/ssz/error.rs @@ -0,0 +1,156 @@ +//! SSZ error types. + +use thiserror::Error; + +/// Error returned by SSZ serialization. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum SerializeError { + /// A byte offset did not fit in `uint32`. + #[error("SSZ offset overflow")] + OffsetOverflow, + /// A container fixed section did not match its declared size. + #[error("container fixed section length {got} did not match expected {expected}")] + ContainerFixedSize { + /// Actual fixed-section length. + got: usize, + /// Expected fixed-section length. + expected: usize, + }, + /// A bounded list exceeded its limit. + #[error("bounded list length {len} exceeds limit {limit}")] + ListTooLong { + /// Actual list length. + len: usize, + /// Maximum list length. + limit: usize, + }, + /// A vector did not have its declared length. + #[error("vector length mismatch: expected {expected}, got {len}")] + VectorLength { + /// Actual vector length. + len: usize, + /// Expected vector length. + expected: usize, + }, +} + +/// Error returned by SSZ decoding. +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum DeserializeError { + /// The input ended before the expected number of bytes were available. + #[error("expected further input: provided {provided}, expected {expected}")] + ExpectedFurtherInput { + /// Bytes provided. + provided: usize, + /// Bytes expected. + expected: usize, + }, + /// The input contained extra bytes. + #[error("additional input: provided {provided}, expected {expected}")] + AdditionalInput { + /// Bytes provided. + provided: usize, + /// Bytes expected. + expected: usize, + }, + /// A variable offset was malformed. + #[error("invalid variable offset {offset} for input length {len}")] + InvalidOffset { + /// Offset value. + offset: usize, + /// Input length. + len: usize, + }, + /// A variable offset was smaller than a preceding bound. + #[error("non-monotonic variable offset {offset} after {previous}")] + NonMonotonicOffset { + /// Previous offset. + previous: usize, + /// Current offset. + offset: usize, + }, + /// A byte offset overflowed host arithmetic. + #[error("SSZ offset overflow")] + OffsetOverflow, + /// Decoded list exceeds its declared limit. + #[error("bounded list length {len} exceeds limit {limit}")] + ListTooLong { + /// Actual list length. + len: usize, + /// Maximum list length. + limit: usize, + }, + /// Decoded vector length differs from its declared length. + #[error("vector length mismatch: expected {expected}, got {got}")] + VectorLength { + /// Expected vector length. + expected: usize, + /// Actual vector length. + got: usize, + }, + /// Bitlist data did not include a delimiter bit. + #[error("bitlist missing delimiter")] + MissingBitlistDelimiter, + /// A fixed-size bitfield carried set padding bits. + #[error("bitfield has non-zero padding bits")] + NonZeroPaddingBits, + /// Boolean SSZ value was not `0` or `1`. + #[error("invalid boolean byte {0}")] + InvalidBool(u8), + /// Derived container failed to assign a field. + #[error("container field {0} was not decoded")] + MissingField(&'static str), +} + +/// Error returned by SSZ merkleization. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum MerkleizationError { + /// A length or limit value could not be represented. + #[error("SSZ merkleization length overflow")] + LengthOverflow, + /// A bounded list exceeded its limit. + #[error("bounded list length {len} exceeds limit {limit}")] + ListTooLong { + /// Actual list length. + len: usize, + /// Maximum list length. + limit: usize, + }, + /// A vector did not have its declared length. + #[error("vector length mismatch: expected {expected}, got {len}")] + VectorLength { + /// Actual vector length. + len: usize, + /// Expected vector length. + expected: usize, + }, + /// A basic-value root received too many bytes. + #[error("basic value length {len} exceeds chunk size {limit}")] + BasicValueTooLong { + /// Actual byte length. + len: usize, + /// Maximum byte length. + limit: usize, + }, +} + +/// Bounded collection construction error. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum CollectionError { + /// Input list exceeded limit. + #[error("collection length {len} exceeds limit {limit}")] + TooLong { + /// Actual length. + len: usize, + /// Maximum length. + limit: usize, + }, + /// Vector length did not match its declared length. + #[error("collection length {len} did not match expected {expected}")] + WrongLength { + /// Actual length. + len: usize, + /// Expected length. + expected: usize, + }, +} diff --git a/moonglass-core/src/ssz/list.rs b/moonglass-core/src/ssz/list.rs new file mode 100644 index 0000000..159332a --- /dev/null +++ b/moonglass-core/src/ssz/list.rs @@ -0,0 +1,221 @@ +//! SSZ bounded list. + +use std::fmt; +use std::ops::{Deref, DerefMut, Index, IndexMut}; +use std::vec::IntoIter; + +use super::{ + BYTES_PER_LENGTH_OFFSET, CollectionError, Deserialize, DeserializeError, MerkleizationError, + Merkleized, Node, Serialize, SerializeError, SimpleSerialize, SszSized, collection_root, + deserialize_list_items, mix_in_length, serialize_sequence, +}; + +/// SSZ bounded variable-length list. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct List { + /// Backing values. + values: Vec, +} + +impl List { + /// Return the number of items. + pub fn len(&self) -> usize { + self.values.len() + } + + /// Return whether the list is empty. + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + /// Append an item when capacity permits. + pub fn push(&mut self, value: T) -> Result<(), CollectionError> { + if self.values.len() >= N { + return Err(CollectionError::TooLong { + len: self.values.len().saturating_add(1), + limit: N, + }); + } + + self.values.push(value); + Ok(()) + } + + /// Borrow an item by index. + pub fn get(&self, index: usize) -> Option<&T> { + self.values.get(index) + } + + /// Borrow all items. + pub fn as_slice(&self) -> &[T] { + &self.values + } + + /// Mutably borrow all items. + pub fn as_mut_slice(&mut self) -> &mut [T] { + &mut self.values + } + + /// Iterate over borrowed items. + pub fn iter(&self) -> impl Iterator { + self.values.iter() + } + + /// Iterate over mutably borrowed items. + pub fn iter_mut(&mut self) -> impl Iterator { + self.values.iter_mut() + } + + /// Remove all values. + pub fn clear(&mut self) { + self.values.clear(); + } +} + +impl Default for List { + fn default() -> Self { + Self { values: Vec::new() } + } +} + +impl fmt::Debug for List { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.values.fmt(f) + } +} + +impl Deref for List { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + &self.values + } +} + +impl DerefMut for List { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.values + } +} + +impl Index for List { + type Output = T; + + fn index(&self, index: usize) -> &Self::Output { + &self.values[index] + } +} + +impl IndexMut for List { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.values[index] + } +} + +impl IntoIterator for List { + type IntoIter = IntoIter; + type Item = T; + + fn into_iter(self) -> Self::IntoIter { + self.values.into_iter() + } +} + +impl AsRef<[T]> for List { + fn as_ref(&self) -> &[T] { + &self.values + } +} + +impl AsMut<[T]> for List { + fn as_mut(&mut self) -> &mut [T] { + &mut self.values + } +} + +impl From> for Vec { + fn from(value: List) -> Self { + value.values + } +} + +impl TryFrom<&[T]> for List { + type Error = CollectionError; + + fn try_from(value: &[T]) -> Result { + Self::try_from(value.to_vec()) + } +} + +impl TryFrom> for List { + type Error = CollectionError; + + fn try_from(value: Vec) -> Result { + if value.len() > N { + return Err(CollectionError::TooLong { + len: value.len(), + limit: N, + }); + } + Ok(Self { values: value }) + } +} + +impl SszSized for List +where + T: SszSized, +{ + fn is_variable_size() -> bool { + true + } + fn size_hint() -> usize { + BYTES_PER_LENGTH_OFFSET + } +} + +impl Serialize for List +where + T: SszSized + Serialize, +{ + fn serialize(&self, buffer: &mut Vec) -> Result { + if self.values.len() > N { + return Err(SerializeError::ListTooLong { + len: self.values.len(), + limit: N, + }); + } + serialize_sequence(&self.values, buffer) + } +} + +impl Deserialize for List +where + T: SszSized + Deserialize, +{ + fn deserialize(encoding: &[u8]) -> Result { + let values = deserialize_list_items::(encoding)?; + Ok(Self { values }) + } +} + +impl Merkleized for List +where + T: SszSized + Serialize + Merkleized + SimpleSerialize, +{ + fn hash_tree_root(&self) -> Result { + if self.values.len() > N { + return Err(MerkleizationError::ListTooLong { + len: self.values.len(), + limit: N, + }); + } + let root = collection_root::(&self.values, N)?; + Ok(mix_in_length(root, self.values.len())) + } +} + +impl SimpleSerialize for List { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass-core/src/ssz/merkle.rs b/moonglass-core/src/ssz/merkle.rs new file mode 100644 index 0000000..fb30503 --- /dev/null +++ b/moonglass-core/src/ssz/merkle.rs @@ -0,0 +1,181 @@ +//! SSZ merkleization helpers. + +use std::sync::LazyLock; + +use sha2::{Digest, Sha256}; + +use super::{BYTES_PER_CHUNK, DeserializeError, MerkleizationError, Node}; + +/// Merkleize roots as a vector of already-rooted chunks. +pub fn merkleize_roots(roots: &[Node]) -> Node { + merkleize_roots_unchecked(roots, roots.len()) +} + +/// Merkleize roots, padding up to `limit`. +pub fn merkleize_roots_with_limit( + roots: &[Node], + limit: usize, +) -> Result { + if roots.len() > limit { + return Err(MerkleizationError::ListTooLong { + len: roots.len(), + limit, + }); + } + Ok(merkleize_roots_unchecked(roots, limit)) +} + +/// Merkleize roots after the caller has checked length against `limit`. +pub fn merkleize_roots_unchecked(roots: &[Node], limit: usize) -> Node { + let height = tree_depth_for_limit(limit.max(1)); + + if roots.is_empty() { + return ZERO_HASHES[height]; + } + + let mut depth = 0usize; + let mut nodes = roots.to_vec(); + while nodes.len() > 1 { + if !nodes.len().is_multiple_of(2) { + nodes.push(ZERO_HASHES[depth]); + } + nodes = nodes + .chunks_exact(2) + .map(|pair| hash_pair(pair[0], pair[1])) + .collect(); + depth += 1; + } + + let mut root = nodes[0]; + while depth < height { + root = hash_pair(root, ZERO_HASHES[depth]); + depth += 1; + } + root +} + +/// Merkleize packed serialized basic bytes. +pub fn merkleize_byte_sequence(bytes: &[u8]) -> Node { + merkleize_byte_sequence_unchecked(bytes, bytes.len()) +} + +/// Merkleize packed serialized basic bytes, padding to a byte limit. +pub fn merkleize_byte_sequence_with_limit( + bytes: &[u8], + byte_limit: usize, +) -> Result { + if bytes.len() > byte_limit { + return Err(MerkleizationError::ListTooLong { + len: bytes.len(), + limit: byte_limit, + }); + } + Ok(merkleize_byte_sequence_unchecked(bytes, byte_limit)) +} + +/// Merkleize packed serialized basic bytes after checking the byte limit. +pub fn merkleize_byte_sequence_unchecked(bytes: &[u8], byte_limit: usize) -> Node { + let chunk_limit = chunk_count(byte_limit).max(1); + merkleize_roots_unchecked(&pack_bytes(bytes), chunk_limit) +} + +/// Mix a list length into a merkle root. +pub fn mix_in_length(root: Node, length: usize) -> Node { + let mut length_chunk = [0u8; BYTES_PER_CHUNK]; + length_chunk[..8].copy_from_slice(&(length as u64).to_le_bytes()); + hash_pair(root, Node(length_chunk)) +} + +/// Number of 32-byte chunks required to cover `bytes`. +pub const fn chunk_count(bytes: usize) -> usize { + bytes.div_ceil(BYTES_PER_CHUNK) +} + +/// Maximum SSZ tree height the cached zero-hash table spans. +/// +/// SSZ list and vector limits stay well under `2**64` elements, so a height of +/// 64 covers every tree depth a merkleization can reach. +pub const MAX_TREE_HEIGHT: usize = 64; + +/// SSZ zero-subtree hashes from leaf depth through [`MAX_TREE_HEIGHT`], derived +/// once. Callers index into this rather than rebuilding the chain with SHA256 on +/// every merkleization. +pub static ZERO_HASHES: LazyLock<[Node; MAX_TREE_HEIGHT + 1]> = LazyLock::new(|| { + let mut table = [Node::default(); MAX_TREE_HEIGHT + 1]; + for depth in 1..=MAX_TREE_HEIGHT { + let node = table[depth - 1]; + table[depth] = hash_pair(node, node); + } + table +}); + +/// Pack bytes into 32-byte SSZ chunks. +pub fn pack_bytes(bytes: &[u8]) -> Vec { + if bytes.is_empty() { + return vec![Node::default()]; + } + bytes + .chunks(BYTES_PER_CHUNK) + .map(|chunk| { + let mut out = [0u8; BYTES_PER_CHUNK]; + out[..chunk.len()].copy_from_slice(chunk); + Node(out) + }) + .collect() +} + +/// Hash two SSZ chunks into their parent node. +pub fn hash_pair(left: Node, right: Node) -> Node { + let mut hasher = Sha256::new(); + hasher.update(left.0); + hasher.update(right.0); + let digest = hasher.finalize(); + let mut bytes = [0u8; BYTES_PER_CHUNK]; + bytes.copy_from_slice(&digest); + Node(bytes) +} + +/// Return the depth needed to cover `limit` leaves. +pub const fn tree_depth_for_limit(limit: usize) -> usize { + if limit <= 1 { + 0 + } else { + (usize::BITS - (limit - 1).leading_zeros()) as usize + } +} + +/// Decode exactly `N` bytes. +pub fn deserialize_fixed_bytes( + encoding: &[u8], +) -> Result<[u8; N], DeserializeError> { + if encoding.len() < N { + return Err(DeserializeError::ExpectedFurtherInput { + provided: encoding.len(), + expected: N, + }); + } + if encoding.len() > N { + return Err(DeserializeError::AdditionalInput { + provided: encoding.len(), + expected: N, + }); + } + + let mut bytes = [0u8; N]; + bytes.copy_from_slice(encoding); + Ok(bytes) +} + +/// Return the hash-tree-root node for an SSZ basic value. +pub fn basic_root(bytes: &[u8]) -> Result { + if bytes.len() > BYTES_PER_CHUNK { + return Err(MerkleizationError::BasicValueTooLong { + len: bytes.len(), + limit: BYTES_PER_CHUNK, + }); + } + + let mut out = [0u8; BYTES_PER_CHUNK]; + out[..bytes.len()].copy_from_slice(bytes); + Ok(Node(out)) +} diff --git a/moonglass-core/src/ssz/node.rs b/moonglass-core/src/ssz/node.rs new file mode 100644 index 0000000..ee98bab --- /dev/null +++ b/moonglass-core/src/ssz/node.rs @@ -0,0 +1,60 @@ +//! SSZ merkle-tree node type. + +use std::fmt; + +use thiserror::Error; + +use super::BYTES_PER_CHUNK; + +/// SSZ merkle tree node. +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct Node(pub [u8; BYTES_PER_CHUNK]); + +impl Node { + /// Borrow the node bytes. + pub const fn as_bytes(&self) -> &[u8; BYTES_PER_CHUNK] { + &self.0 + } +} + +impl fmt::Debug for Node { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("Node").field(&self.0).finish() + } +} + +impl AsRef<[u8]> for Node { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl TryFrom<&[u8]> for Node { + type Error = NodeError; + + fn try_from(value: &[u8]) -> Result { + if value.len() != BYTES_PER_CHUNK { + return Err(NodeError::WrongLength { + expected: BYTES_PER_CHUNK, + got: value.len(), + }); + } + let mut bytes = [0u8; BYTES_PER_CHUNK]; + bytes.copy_from_slice(value); + Ok(Self(bytes)) + } +} + +/// Node conversion error. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum NodeError { + /// Input was not exactly one chunk. + #[error("node length mismatch: expected {expected}, got {got}")] + WrongLength { + /// Expected byte length. + expected: usize, + /// Actual byte length. + got: usize, + }, +} diff --git a/moonglass-core/src/ssz/traits.rs b/moonglass-core/src/ssz/traits.rs new file mode 100644 index 0000000..a252cf8 --- /dev/null +++ b/moonglass-core/src/ssz/traits.rs @@ -0,0 +1,37 @@ +//! Public SSZ traits. + +use std::marker::Sized as StdSized; + +use super::{DeserializeError, MerkleizationError, Node, SerializeError}; + +/// SSZ type sizing metadata. +pub trait SszSized { + /// Whether the type has variable-size SSZ encoding. + fn is_variable_size() -> bool; + /// Fixed-size encoding length, or fixed-section contribution for variable types. + fn size_hint() -> usize; +} + +/// SSZ serialization. +pub trait Serialize { + /// Append the SSZ encoding of `self` to `buffer`. + fn serialize(&self, buffer: &mut Vec) -> Result; +} + +/// SSZ deserialization. +pub trait Deserialize: StdSized { + /// Decode `Self` from a complete SSZ byte slice. + fn deserialize(encoding: &[u8]) -> Result; +} + +/// SSZ hash-tree-root. +pub trait Merkleized { + /// Compute the SSZ hash-tree-root. + fn hash_tree_root(&self) -> Result; +} + +/// Marker trait used to choose basic packing versus per-element roots. +pub trait SimpleSerialize { + /// Whether the type is composite for SSZ collection merkleization. + fn is_composite_type() -> bool; +} diff --git a/moonglass-core/src/ssz/vector.rs b/moonglass-core/src/ssz/vector.rs new file mode 100644 index 0000000..a22d85a --- /dev/null +++ b/moonglass-core/src/ssz/vector.rs @@ -0,0 +1,208 @@ +//! SSZ fixed-length vector. + +use std::fmt; +use std::ops::{Deref, DerefMut, Index, IndexMut}; +use std::vec::IntoIter; + +use super::{ + BYTES_PER_LENGTH_OFFSET, CollectionError, Deserialize, DeserializeError, MerkleizationError, + Merkleized, Node, Serialize, SerializeError, SimpleSerialize, SszSized, collection_root, + deserialize_vector_items, serialize_sequence, +}; + +/// SSZ fixed-length vector. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Vector { + /// Backing values. + values: Vec, +} + +impl Vector { + /// Return the number of items. + pub fn len(&self) -> usize { + self.values.len() + } + + /// Return whether the vector length is zero. + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + /// Borrow an item by index. + pub fn get(&self, index: usize) -> Option<&T> { + self.values.get(index) + } + + /// Borrow all items. + pub fn as_slice(&self) -> &[T] { + &self.values + } + + /// Mutably borrow all items. + pub fn as_mut_slice(&mut self) -> &mut [T] { + &mut self.values + } + + /// Iterate over borrowed items. + pub fn iter(&self) -> impl Iterator { + self.values.iter() + } + + /// Iterate over mutably borrowed items. + pub fn iter_mut(&mut self) -> impl Iterator { + self.values.iter_mut() + } +} + +impl Default for Vector { + fn default() -> Self { + Self { + values: vec![T::default(); N], + } + } +} + +impl fmt::Debug for Vector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.values.fmt(f) + } +} + +impl Deref for Vector { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + &self.values + } +} + +impl DerefMut for Vector { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.values + } +} + +impl Index for Vector { + type Output = T; + + fn index(&self, index: usize) -> &Self::Output { + &self.values[index] + } +} + +impl IndexMut for Vector { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.values[index] + } +} + +impl IntoIterator for Vector { + type IntoIter = IntoIter; + type Item = T; + + fn into_iter(self) -> Self::IntoIter { + self.values.into_iter() + } +} + +impl AsRef<[T]> for Vector { + fn as_ref(&self) -> &[T] { + &self.values + } +} + +impl AsMut<[T]> for Vector { + fn as_mut(&mut self) -> &mut [T] { + &mut self.values + } +} + +impl From> for Vec { + fn from(value: Vector) -> Self { + value.values + } +} + +impl TryFrom<&[T]> for Vector { + type Error = CollectionError; + + fn try_from(value: &[T]) -> Result { + Self::try_from(value.to_vec()) + } +} + +impl TryFrom> for Vector { + type Error = CollectionError; + + fn try_from(value: Vec) -> Result { + if value.len() != N { + return Err(CollectionError::WrongLength { + len: value.len(), + expected: N, + }); + } + Ok(Self { values: value }) + } +} + +impl SszSized for Vector +where + T: SszSized, +{ + fn is_variable_size() -> bool { + T::is_variable_size() + } + fn size_hint() -> usize { + if T::is_variable_size() { + N * BYTES_PER_LENGTH_OFFSET + } else { + N * T::size_hint() + } + } +} + +impl Serialize for Vector +where + T: SszSized + Serialize, +{ + fn serialize(&self, buffer: &mut Vec) -> Result { + if self.values.len() != N { + return Err(SerializeError::VectorLength { + len: self.values.len(), + expected: N, + }); + } + serialize_sequence(&self.values, buffer) + } +} + +impl Deserialize for Vector +where + T: SszSized + Deserialize, +{ + fn deserialize(encoding: &[u8]) -> Result { + let values = deserialize_vector_items::(encoding)?; + Ok(Self { values }) + } +} + +impl Merkleized for Vector +where + T: SszSized + Serialize + Merkleized + SimpleSerialize, +{ + fn hash_tree_root(&self) -> Result { + if self.values.len() != N { + return Err(MerkleizationError::VectorLength { + len: self.values.len(), + expected: N, + }); + } + collection_root::(&self.values, N) + } +} + +impl SimpleSerialize for Vector { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass/src/state_transition.rs b/moonglass-core/src/state_transition.rs similarity index 74% rename from moonglass/src/state_transition.rs rename to moonglass-core/src/state_transition.rs index fb0e7af..a4494fc 100644 --- a/moonglass/src/state_transition.rs +++ b/moonglass-core/src/state_transition.rs @@ -6,39 +6,43 @@ //! post-state root matches the root claimed by the block. //! //! Execution payload envelopes are verified against the bid and expected -//! consensus-side commitments. Execution-engine validity and blob/data -//! availability are not yet implemented. +//! consensus-side commitments. Fork choice handles data availability and the +//! execution verifier before recording a delivered payload. //! //! # Reading routes //! -//! - Block transition: [`BeaconState::apply_signed_block`] clones the pre-state, -//! runs [`BeaconState::process_slots`], verifies the proposer signature, -//! applies [`BeaconState::process_block`], checks the claimed post-state root, -//! then commits the clone back to `self`. -//! - Slot transition: [`BeaconState::process_slots`] repeatedly calls -//! [`BeaconState::process_slot`], which records recent roots, advances the -//! clock, clears the payload-availability bit for the next slot, and runs -//! [`BeaconState::process_epoch`] at epoch boundaries. -//! - Block body transition: [`BeaconState::process_block`] handles the previous -//! slot's parent payload commitment before accepting the current slot's -//! builder bid and operations. -//! - Operation transition: [`BeaconState::process_operations`] runs slashings, -//! beacon attestations, exits, credential changes, and payload -//! attestations in consensus order. +//! Block transition: [`BeaconState::state_transition`] clones the pre-state, +//! runs [`BeaconState::process_slots`], verifies the proposer signature, applies +//! [`BeaconState::process_block`], checks the claimed post-state root, then +//! commits the clone back to `self`. +//! +//! Slot transition: [`BeaconState::process_slots`] repeatedly calls +//! [`BeaconState::process_slot`], which records recent roots, advances the clock, +//! clears the payload-availability bit for the next slot, and runs +//! [`BeaconState::process_epoch`] at epoch boundaries. +//! +//! Block body transition: [`BeaconState::process_block`] handles the previous +//! slot's parent payload commitment before accepting the current slot's builder +//! bid and operations. +//! +//! Operation transition: [`BeaconState::process_operations`] runs slashings, +//! beacon attestations, exits, credential changes, and payload attestations in +//! consensus order. //! //! The main functions keep spec shape. Smaller helpers may be named around the //! state handoff they make visible. -mod balance; -mod block; -mod builder; -mod committee; -mod epoch; -mod operations; -mod signing; -mod slot; -mod validator; -mod withdrawal; +pub mod balance; +pub mod block; +pub mod builder; +pub mod committee; +pub mod epoch; +pub mod genesis; +pub mod operations; +pub mod signing; +pub mod slot; +pub mod validator; +pub mod withdrawal; pub use committee::*; pub use operations::*; @@ -46,26 +50,28 @@ pub use signing::*; pub use validator::*; use crate::constants::{ - DOMAIN_BEACON_BUILDER, DOMAIN_BEACON_PROPOSER, GENESIS_SLOT, SLOT_DURATION_MS, + BUILDER_INDEX_SELF_BUILD, DOMAIN_BEACON_BUILDER, DOMAIN_BEACON_PROPOSER, GENESIS_SLOT, + SLOT_DURATION_MS, }; use crate::containers::{ BeaconState, ExecutionPayloadEnvelope, SignedBeaconBlock, SignedExecutionPayloadEnvelope, }; use crate::error::{BlockError, MerkleError, SignatureError, TransitionError}; -use crate::primitives::{BLSPubkey, Root}; +use crate::primitives::{BLSPubkey, BuilderIndex, Root, Slot}; +use crate::ssz::Merkleized; #[doc(hidden)] -pub(crate) trait TreeRootExt { +pub trait TreeRootExt { /// Compute the SSZ tree root, emitting `on_fail` on merkleization failure. - fn tree_root(&mut self, on_fail: MerkleError) -> Result; + fn tree_root(&self, on_fail: MerkleError) -> Result; } impl TreeRootExt for T where - T: ssz_rs::Merkleized, + T: Merkleized, { - fn tree_root(&mut self, on_fail: MerkleError) -> Result { - ssz_rs::Merkleized::hash_tree_root(self) + fn tree_root(&self, on_fail: MerkleError) -> Result { + Merkleized::hash_tree_root(self) .map(Root::from) .map_err(|_| on_fail.into()) } @@ -83,8 +89,7 @@ impl BeaconState { /// does the clone replace `self`. A root disagreement raises /// [`TransitionError::StateRootMismatch`], so a forged or stale state root /// can never be committed. - /// Spec: `state_transition` - pub fn apply_signed_block( + pub fn state_transition( &mut self, signed_block: &SignedBeaconBlock, ) -> Result<(), TransitionError> { @@ -97,6 +102,14 @@ impl BeaconState { Ok(()) } + /// Compatibility wrapper for callers that name the transition by input. + pub fn apply_signed_block( + &mut self, + signed_block: &SignedBeaconBlock, + ) -> Result<(), TransitionError> { + self.state_transition(signed_block) + } + /// Verify a delivered execution payload envelope for the current block. /// /// The envelope must name the current block through `beacon_block_root` and @@ -111,9 +124,9 @@ impl BeaconState { /// or data availability. Fork choice records the checked envelope in /// [`crate::fork_choice::Store::payloads`], and the committed requests are /// applied later by a child block through - /// [`BeaconState::accept_parent_payload_commitment`]. + /// [`BeaconState::process_parent_execution_payload`]. pub fn verify_execution_payload_envelope( - &mut self, + &self, signed_envelope: &SignedExecutionPayloadEnvelope, ) -> Result<(), TransitionError> { let envelope = &signed_envelope.message; @@ -130,22 +143,22 @@ impl BeaconState { /// Compute the block root represented by the state's latest block header /// after filling in the current state root. - fn current_block_root(&mut self) -> Result { + pub fn current_block_root(&self) -> Result { let state_root = self.tree_root(MerkleError::BeaconState)?; - let mut header = self.latest_block_header.with_state_root(state_root); + let header = self.latest_block_header.with_state_root(state_root); header.tree_root(MerkleError::BeaconBlockHeader) } /// Verify the builder or self-build signature on an execution payload envelope. - fn verify_execution_payload_envelope_signature( + pub fn verify_execution_payload_envelope_signature( &self, signed_envelope: &SignedExecutionPayloadEnvelope, ) -> Result<(), TransitionError> { let envelope = &signed_envelope.message; let signer_pubkey = self.execution_payload_envelope_signer(envelope.builder_index)?; - let mut envelope_msg = envelope.clone(); + let envelope_msg = envelope.clone(); let signing_root = self.signing_root_for( - &mut envelope_msg, + &envelope_msg, DOMAIN_BEACON_BUILDER, self.slot.epoch(), MerkleError::ExecutionPayloadEnvelope, @@ -162,11 +175,11 @@ impl BeaconState { /// /// Self-build envelopes are signed by the beacon proposer. Non-self-build /// envelopes are signed by the registered builder named in the envelope. - fn execution_payload_envelope_signer( + pub fn execution_payload_envelope_signer( &self, - builder_index: crate::primitives::BuilderIndex, + builder_index: BuilderIndex, ) -> Result { - if builder_index == crate::constants::BUILDER_INDEX_SELF_BUILD { + if builder_index == BUILDER_INDEX_SELF_BUILD { let proposer = self.beacon_proposer_index()?; return Ok(self.validator(proposer)?.pubkey); } @@ -178,7 +191,7 @@ impl BeaconState { /// This is the consensus-side boundary: it validates committed fields and /// expected withdrawals, but does not run execution-engine validity or blob /// data-availability verification. - fn validate_execution_payload_envelope( + pub fn validate_execution_payload_envelope( &self, envelope: &ExecutionPayloadEnvelope, ) -> Result<(), TransitionError> { @@ -195,7 +208,7 @@ impl BeaconState { if envelope.payload.block_hash != bid.block_hash { return Err(BlockError::EnvelopePayloadHashMismatch.into()); } - let mut requests = envelope.execution_requests.clone(); + let requests = envelope.execution_requests.clone(); let requests_root = requests.tree_root(MerkleError::ExecutionRequests)?; if requests_root != bid.execution_requests_root { return Err(BlockError::EnvelopeRequestsRootMismatch.into()); @@ -210,7 +223,7 @@ impl BeaconState { } .into()); } - let expected_timestamp = self.expected_execution_payload_timestamp()?; + let expected_timestamp = self.compute_time_at_slot(self.slot)?; if envelope.payload.timestamp != expected_timestamp { return Err(BlockError::EnvelopeTimestampMismatch { got: envelope.payload.timestamp, @@ -224,12 +237,11 @@ impl BeaconState { Ok(()) } - /// Expected execution payload timestamp for the state's current slot. - fn expected_execution_payload_timestamp(&self) -> Result { - // Spec: `compute_time_at_slot`. Multiply slot * SLOT_DURATION_MS first - // and divide by 1000 last so the result is exact when SLOT_DURATION_MS - // is not a multiple of 1000. - let slots_since_genesis = self.slot.as_u64().saturating_sub(GENESIS_SLOT.as_u64()); + /// Return the timestamp at `slot`. + pub fn compute_time_at_slot(&self, slot: Slot) -> Result { + // Multiply slot * SLOT_DURATION_MS first and divide by 1000 last so the + // result is exact when SLOT_DURATION_MS is not a multiple of 1000. + let slots_since_genesis = slot.as_u64().saturating_sub(GENESIS_SLOT.as_u64()); slots_since_genesis .checked_mul(SLOT_DURATION_MS) .map(|ms| ms / 1_000) @@ -251,8 +263,8 @@ impl BeaconState { let proposer_index = signed_block.message.proposer_index; let pubkey = self.validator(proposer_index)?.pubkey; let domain = self.domain_for(DOMAIN_BEACON_PROPOSER, self.slot.epoch())?; - let mut block = signed_block.message.clone(); - let signing_root = compute_signing_root(&mut block, domain, MerkleError::BeaconBlock)?; + let block = signed_block.message.clone(); + let signing_root = compute_signing_root(&block, domain, MerkleError::BeaconBlock)?; verify_signature( &pubkey, signing_root, @@ -262,7 +274,7 @@ impl BeaconState { } /// Compare the computed post-state root against the block's claimed root. - fn expect_post_state_root(&mut self, expected: Root) -> Result<(), TransitionError> { + pub fn expect_post_state_root(&mut self, expected: Root) -> Result<(), TransitionError> { let post_root = self.tree_root(MerkleError::BeaconState)?; if expected != post_root { return Err(TransitionError::StateRootMismatch { diff --git a/moonglass-core/src/state_transition/balance.rs b/moonglass-core/src/state_transition/balance.rs new file mode 100644 index 0000000..acb7322 --- /dev/null +++ b/moonglass-core/src/state_transition/balance.rs @@ -0,0 +1,472 @@ +//! Balance behavior used by rewards and validator exits. +//! +//! Covers balance mutation (overflow-checked increases, saturating decreases), +//! total active balance, base-reward math, per-flag reward and penalty +//! distribution, the inactivity-leak penalty, and the slashing mutation. + +use std::collections::HashSet; + +use crate::constants::{ + BASE_REWARD_FACTOR, EFFECTIVE_BALANCE_INCREMENT, EPOCHS_PER_SLASHINGS_VECTOR, GENESIS_EPOCH, + INACTIVITY_PENALTY_QUOTIENT, INACTIVITY_SCORE_BIAS, MIN_EPOCHS_TO_INACTIVITY_PENALTY, + MIN_SLASHING_PENALTY_QUOTIENT, PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, + TIMELY_HEAD_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, VALIDATOR_REGISTRY_LIMIT, WEIGHT_DENOMINATOR, + WHISTLEBLOWER_REWARD_QUOTIENT, +}; +use crate::containers::BeaconState; +use crate::error::{ + OperationError, PrimitivesError, StateTransitionInvariant, TransitionArithmetic, + TransitionError, +}; +use crate::primitives::{Epoch, Gwei, ValidatorIndex}; +use crate::ssz::List; +use crate::state_transition::BeaconStateLookup; + +/// Per-validator balance changes produced by an epoch accounting phase. +pub struct BalanceDeltas { + /// Rewards accumulated per validator index. + pub rewards: Vec, + /// Penalties accumulated per validator index. + pub penalties: Vec, +} + +impl BalanceDeltas { + /// Construct zeroed reward and penalty vectors for `validator_count` validators. + pub fn zeroed(validator_count: usize) -> Self { + Self { + rewards: vec![Gwei::ZERO; validator_count], + penalties: vec![Gwei::ZERO; validator_count], + } + } + + /// Apply the accumulated rewards, then penalties, to `balances`. + /// + /// A reward addition that overflows `u64` makes the transition invalid and + /// raises [`TransitionError::BalanceOverflow`]. Penalties saturate at zero, + /// matching the spec's underflow protection on `decrease_balance`. + pub fn apply_to( + self, + balances: &mut List, + ) -> Result<(), TransitionError> { + for (i, reward) in self.rewards.iter().enumerate() { + let index = ValidatorIndex(i as u64); + if i >= balances.len() { + return Err(StateTransitionInvariant::MissingBalance(index).into()); + } + balances[i] = balances[i] + .checked_add(*reward) + .ok_or(TransitionError::BalanceOverflow)?; + } + for (i, penalty) in self.penalties.iter().enumerate() { + let index = ValidatorIndex(i as u64); + if i >= balances.len() { + return Err(StateTransitionInvariant::MissingBalance(index).into()); + } + balances[i] = balances[i].saturating_sub(*penalty); + } + Ok(()) + } +} + +impl BeaconState { + /// Add `delta` gwei to `index`'s balance. A `u64` overflow is invalid and + /// raises [`TransitionError::BalanceOverflow`]. + pub fn increase_balance( + &mut self, + index: ValidatorIndex, + delta: Gwei, + ) -> Result<(), TransitionError> { + let slot = self.balance_mut(index)?; + *slot = slot + .checked_add(delta) + .ok_or(TransitionError::BalanceOverflow)?; + Ok(()) + } + + /// Subtract `delta` gwei from `index`'s balance. Saturates at `Gwei(0)`. + pub fn decrease_balance( + &mut self, + index: ValidatorIndex, + delta: Gwei, + ) -> Result<(), TransitionError> { + let slot = self.balance_mut(index)?; + *slot = slot.saturating_sub(delta); + Ok(()) + } + + /// Return the mutable balance cell for an existing validator index. + pub fn balance_mut(&mut self, index: ValidatorIndex) -> Result<&mut Gwei, TransitionError> { + let _ = self.validator(index)?; + if index.as_usize() >= self.balances.len() { + return Err(StateTransitionInvariant::MissingBalance(index).into()); + } + Ok(&mut self.balances[index.as_usize()]) + } + + /// Return the current epoch from the state's slot. + pub fn get_current_epoch(&self) -> Epoch { + self.slot.epoch() + } + + /// Sum of effective balances over `indices`, floored at + /// `EFFECTIVE_BALANCE_INCREMENT`. + pub fn get_total_balance(&self, indices: &[ValidatorIndex]) -> Result { + let mut total = Gwei::ZERO; + for index in indices { + let balance = self.validator(*index)?.effective_balance; + total = total + .checked_add(balance) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))?; + } + Ok(total.max(EFFECTIVE_BALANCE_INCREMENT)) + } + + /// Sum of effective balances over the active validator set. + pub fn get_total_active_balance(&self) -> Result { + let indices = self.active_validator_indices(self.get_current_epoch()); + self.get_total_balance(&indices) + } + + /// Base reward issued per effective-balance increment per epoch. + pub fn get_base_reward_per_increment(&self) -> Result { + let total = self.get_total_active_balance()?.as_u64(); + let numerator = EFFECTIVE_BALANCE_INCREMENT + .as_u64() + .checked_mul(BASE_REWARD_FACTOR) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; + Ok(Gwei(numerator / integer_squareroot(total))) + } + + /// Base reward for the validator at `index`. Errors if `index` is out of range. + pub fn get_base_reward(&self, index: ValidatorIndex) -> Result { + let v = self.validator(index)?; + let increments = v.effective_balance.as_u64() / EFFECTIVE_BALANCE_INCREMENT.as_u64(); + let reward = increments + .checked_mul(self.get_base_reward_per_increment()?.as_u64()) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; + Ok(Gwei(reward)) + } + + /// Base reward for callers not yet migrated to `get_base_reward`. + pub fn base_reward(&self, index: ValidatorIndex) -> Result { + self.get_base_reward(index) + } + + /// Total active-balance, expressed in `EFFECTIVE_BALANCE_INCREMENT` units. + pub fn active_increments(&self) -> Result { + Ok(self.get_total_active_balance()?.as_u64() / EFFECTIVE_BALANCE_INCREMENT.as_u64()) + } + + /// Previous epoch from the state's current slot. + pub fn get_previous_epoch(&self) -> Epoch { + let current = self.get_current_epoch(); + if current == GENESIS_EPOCH { + GENESIS_EPOCH + } else { + Epoch(current.as_u64() - 1) + } + } + + /// Previous epoch for callers not yet migrated to `get_previous_epoch`. + pub fn previous_epoch(&self) -> Epoch { + self.get_previous_epoch() + } + + /// Return how many epochs have elapsed since the finalized checkpoint. + pub fn get_finality_delay(&self) -> Result { + self.get_previous_epoch() + .as_u64() + .checked_sub(self.finalized_checkpoint.epoch.as_u64()) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Epoch, + )) + } + + /// True if finality has fallen far enough behind to trigger the inactivity leak. + pub fn is_in_inactivity_leak(&self) -> Result { + Ok(self.get_finality_delay()? > MIN_EPOCHS_TO_INACTIVITY_PENALTY) + } + + /// Validators eligible for per-epoch rewards or penalties. + pub fn get_eligible_validator_indices(&self) -> Result, TransitionError> { + let previous = self.get_previous_epoch(); + let next = previous.as_u64().checked_add(1).map(Epoch).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Epoch), + )?; + Ok(self + .validators + .iter() + .enumerate() + .filter_map(|(i, v)| { + let active = v.is_active_validator(previous); + let post_slash = v.slashed && next < v.withdrawable_epoch; + (active || post_slash).then_some(ValidatorIndex(i as u64)) + }) + .collect()) + } + + /// Eligible validators for callers not yet migrated to `get_`. + pub fn eligible_validator_indices(&self) -> Result, TransitionError> { + self.get_eligible_validator_indices() + } + + /// Indices of validators that were active in `epoch`, not slashed, and earned + /// the participation flag at `flag_index`. + pub fn get_unslashed_participating_indices( + &self, + flag_index: usize, + epoch: Epoch, + ) -> Result, TransitionError> { + Self::participation_flag_weight(flag_index)?; + let current = self.get_current_epoch(); + let previous = self.get_previous_epoch(); + let (participation, current_participation) = if epoch == current { + (&self.current_epoch_participation, true) + } else if epoch == previous { + (&self.previous_epoch_participation, false) + } else { + return Err(OperationError::AttestationTargetEpochInvalid.into()); + }; + let mut out = Vec::new(); + for (i, v) in self.validators.iter().enumerate() { + if !v.is_active_validator(epoch) { + continue; + } + let index = ValidatorIndex(i as u64); + let flags = participation.get(i).copied().ok_or_else(|| { + let invariant = if current_participation { + StateTransitionInvariant::MissingCurrentEpochParticipation(index) + } else { + StateTransitionInvariant::MissingPreviousEpochParticipation(index) + }; + TransitionError::from(invariant) + })?; + if flags.has_flag(flag_index)? && !v.slashed { + out.push(index); + } + } + Ok(out) + } + + /// Participating indices for callers not yet migrated to `get_`. + pub fn unslashed_participating_indices( + &self, + flag_index: usize, + epoch: Epoch, + ) -> Result, TransitionError> { + self.get_unslashed_participating_indices(flag_index, epoch) + } + + /// Per-validator reward and penalty vectors for participation flag `flag_index`. + pub fn get_flag_index_deltas( + &self, + flag_index: usize, + ) -> Result<(Vec, Vec), TransitionError> { + let len = self.validators.len(); + let mut deltas = BalanceDeltas::zeroed(len); + let previous = self.get_previous_epoch(); + let participating = self.get_unslashed_participating_indices(flag_index, previous)?; + let participating_set: HashSet = participating.iter().copied().collect(); + let weight = Self::participation_flag_weight(flag_index)?; + let participating_increments = + self.get_total_balance(&participating)?.as_u64() / EFFECTIVE_BALANCE_INCREMENT.as_u64(); + let active_increments = + self.get_total_active_balance()?.as_u64() / EFFECTIVE_BALANCE_INCREMENT.as_u64(); + let in_leak = self.get_finality_delay()? > MIN_EPOCHS_TO_INACTIVITY_PENALTY; + for index in self.get_eligible_validator_indices()? { + let base = self.get_base_reward(index)?.as_u64(); + if participating_set.contains(&index) { + if !in_leak { + let numerator = base + .checked_mul(weight) + .and_then(|value| value.checked_mul(participating_increments)) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; + let denominator = active_increments.checked_mul(WEIGHT_DENOMINATOR).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Weight), + )?; + deltas.add_reward(index, Gwei(numerator / denominator))?; + } + } else if flag_index != TIMELY_HEAD_FLAG_INDEX { + let penalty = + base.checked_mul(weight) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))? + / WEIGHT_DENOMINATOR; + deltas.add_penalty(index, Gwei(penalty))?; + } + } + Ok((deltas.rewards, deltas.penalties)) + } + + /// Per-validator reward and penalty deltas for participation flag `flag_index`. + pub fn participation_flag_deltas( + &self, + flag_index: usize, + ) -> Result { + let (rewards, penalties) = self.get_flag_index_deltas(flag_index)?; + Ok(BalanceDeltas { rewards, penalties }) + } + + /// Per-validator inactivity-leak reward and penalty vectors. + pub fn get_inactivity_penalty_deltas(&self) -> Result<(Vec, Vec), TransitionError> { + let len = self.validators.len(); + let mut deltas = BalanceDeltas::zeroed(len); + let previous = self.get_previous_epoch(); + let matching = + self.get_unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, previous)?; + let matching_set: HashSet = matching.iter().copied().collect(); + let denominator = INACTIVITY_SCORE_BIAS + .checked_mul(INACTIVITY_PENALTY_QUOTIENT) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; + for index in self.get_eligible_validator_indices()? { + if matching_set.contains(&index) { + continue; + } + let validator = self.validator(index)?; + let score = self + .inactivity_scores + .get(index.as_usize()) + .copied() + .ok_or(StateTransitionInvariant::MissingInactivityScore(index))?; + let numerator = validator + .effective_balance + .as_u64() + .checked_mul(score) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; + deltas.add_penalty(index, Gwei(numerator / denominator))?; + } + Ok((deltas.rewards, deltas.penalties)) + } + + /// Per-validator inactivity-leak deltas. + pub fn inactivity_penalty_deltas(&self) -> Result { + let (rewards, penalties) = self.get_inactivity_penalty_deltas()?; + Ok(BalanceDeltas { rewards, penalties }) + } + + /// Apply the slashing mutation to `slashed_index`. + pub fn slash_validator( + &mut self, + slashed_index: ValidatorIndex, + whistleblower_index: Option, + ) -> Result<(), TransitionError> { + let current_epoch = self.get_current_epoch(); + let effective_balance = self.validator(slashed_index)?.effective_balance; + let extended = current_epoch + .as_u64() + .checked_add(EPOCHS_PER_SLASHINGS_VECTOR as u64) + .map(Epoch) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Epoch, + ))?; + let slashings_slot = current_epoch % EPOCHS_PER_SLASHINGS_VECTOR; + let updated_bucket = self.slashings[slashings_slot] + .checked_add(effective_balance) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))?; + + self.initiate_validator_exit(slashed_index)?; + + let v = &mut self.validators[slashed_index.as_usize()]; + v.slashed = true; + if extended > v.withdrawable_epoch { + v.withdrawable_epoch = extended; + } + + self.slashings[slashings_slot] = updated_bucket; + + self.decrease_balance( + slashed_index, + Gwei(effective_balance.as_u64() / MIN_SLASHING_PENALTY_QUOTIENT), + )?; + + let proposer_index = self.beacon_proposer_index()?; + let whistleblower = whistleblower_index.unwrap_or(proposer_index); + let whistleblower_reward = Gwei(effective_balance.as_u64() / WHISTLEBLOWER_REWARD_QUOTIENT); + let proposer_reward = + Gwei(whistleblower_reward.as_u64() * PROPOSER_WEIGHT / WEIGHT_DENOMINATOR); + self.increase_balance(proposer_index, proposer_reward)?; + self.increase_balance( + whistleblower, + Gwei(whistleblower_reward.as_u64() - proposer_reward.as_u64()), + )?; + Ok(()) + } + + /// Return the configured reward weight for a participation flag. + pub fn participation_flag_weight(flag_index: usize) -> Result { + PARTICIPATION_FLAG_WEIGHTS + .get(flag_index) + .copied() + .ok_or_else(|| PrimitivesError::FlagIndexOutOfRange(flag_index).into()) + } +} + +impl BalanceDeltas { + /// Add `delta` to the reward vector at `index`. + pub fn add_reward( + &mut self, + index: ValidatorIndex, + delta: Gwei, + ) -> Result<(), TransitionError> { + self.add_delta(index, delta, true) + } + + /// Add `delta` to the penalty vector at `index`. + pub fn add_penalty( + &mut self, + index: ValidatorIndex, + delta: Gwei, + ) -> Result<(), TransitionError> { + self.add_delta(index, delta, false) + } + + /// Add a reward or penalty delta, checking vector shape and overflow. + pub fn add_delta( + &mut self, + index: ValidatorIndex, + delta: Gwei, + reward: bool, + ) -> Result<(), TransitionError> { + let deltas = if reward { + &mut self.rewards + } else { + &mut self.penalties + }; + let slot = deltas + .get_mut(index.as_usize()) + .ok_or(StateTransitionInvariant::MissingBalance(index))?; + *slot = slot + .checked_add(delta) + .ok_or(TransitionError::BalanceOverflow)?; + Ok(()) + } +} + +/// Return the largest integer `x` such that `x * x <= n`. +pub fn integer_squareroot(n: u64) -> u64 { + if n == u64::MAX { + return u64::from(u32::MAX); + } + let mut x = n; + let mut y = x.div_ceil(2); + while y < x { + x = y; + y = x.midpoint(n / x); + } + x +} diff --git a/moonglass/src/state_transition/block.rs b/moonglass-core/src/state_transition/block.rs similarity index 87% rename from moonglass/src/state_transition/block.rs rename to moonglass-core/src/state_transition/block.rs index 5e654d1..91e062c 100644 --- a/moonglass/src/state_transition/block.rs +++ b/moonglass-core/src/state_transition/block.rs @@ -9,8 +9,8 @@ //! block's payload effects before its own current-slot bid is accepted. That //! order is the main payload handoff to watch when tracing state writes. -mod parent_payload; -mod sync_aggregate; +pub mod parent_payload; +pub mod sync_aggregate; use sha2::{Digest, Sha256}; @@ -23,7 +23,7 @@ use crate::state_transition::{BeaconStateLookup, TreeRootExt, verify_signature}; impl BeaconState { /// Apply the per-block sub-phases of `block` in consensus order. /// - /// The first phase, [`BeaconState::accept_parent_payload_commitment`], + /// The first phase, [`BeaconState::process_parent_execution_payload`], /// settles the parent block's delivered payload by applying its execution /// requests, releasing the parent builder payment, and marking the parent /// payload available, all before the current slot's own identity and bid are @@ -33,11 +33,10 @@ impl BeaconState { /// sync-committee participation. When this runs through /// [`BeaconState::apply_signed_block`], a failure in any phase aborts the /// cloned transition before it replaces the caller's state. - /// Spec: `process_block` pub fn process_block(&mut self, block: &BeaconBlock) -> Result<(), TransitionError> { // Previous-slot payload handoff: settle the parent payload before this // slot records its own payload commitment. - self.accept_parent_payload_commitment(block)?; + self.process_parent_execution_payload(block)?; // Current-slot block identity and body processing. self.process_block_header(block)?; @@ -54,13 +53,13 @@ impl BeaconState { /// /// The block's `slot` must equal the state slot and lie strictly after the /// parent header's slot, the `proposer_index` must match the slot's expected - /// proposer, the `parent_root` must hash-match the cached parent header, and - /// that proposer must not already be slashed. When all hold, the block's - /// header is stored as `latest_block_header` with a zero state root, which a - /// later [`BeaconState::process_slot`] backfills. Any mismatch raises a - /// [`BlockError`], rejecting a block that claims the wrong slot, proposer, or - /// parent. - /// Spec: `process_block_header` + /// proposer, and the `parent_root` must hash-match the cached parent + /// header. Once those identity checks hold, the block's header is stored as + /// `latest_block_header` with a zero state root, which a later + /// [`BeaconState::process_slot`] backfills. The proposer is then checked for + /// slashing, matching the inherited helper's direct mutation order while the + /// clone-commit block entry point still discards the scratch state on + /// failure. pub fn process_block_header(&mut self, block: &BeaconBlock) -> Result<(), TransitionError> { if block.slot != self.slot { return Err(BlockError::BlockSlotMismatch { @@ -95,10 +94,10 @@ impl BeaconState { .into()); } let body_root = block.body.clone().tree_root(MerkleError::BeaconBlockBody)?; + self.latest_block_header = block.header(body_root, Root::ZERO); if self.validator(block.proposer_index)?.slashed { return Err(BlockError::ProposerSlashed(block.proposer_index).into()); } - self.latest_block_header = block.header(body_root, Root::ZERO); Ok(()) } @@ -110,14 +109,13 @@ impl BeaconState { /// exclusive-or into the current epoch's slot of `randao_mixes`, advancing /// the chain randomness that later seeds committee, proposer, and /// sync-committee sampling. - /// Spec: `process_randao` pub fn process_randao(&mut self, body: &BeaconBlockBody) -> Result<(), TransitionError> { let epoch = self.slot.epoch(); let proposer_index = self.beacon_proposer_index()?; let pubkey = self.validator(proposer_index)?.pubkey; - let mut epoch_object = epoch; + let epoch_object = epoch; let signing_root = - self.signing_root_for(&mut epoch_object, DOMAIN_RANDAO, epoch, MerkleError::Epoch)?; + self.signing_root_for(&epoch_object, DOMAIN_RANDAO, epoch, MerkleError::Epoch)?; verify_signature( &pubkey, signing_root, @@ -143,12 +141,13 @@ impl BeaconState { /// the same data, it is promoted into `eth1_data` as the deposit source the /// next deposit proofs verify against. The bag itself is cleared only later, /// at the voting-period boundary. - /// Spec: `process_eth1_data` pub fn process_eth1_data(&mut self, body: &BeaconBlockBody) -> Result<(), TransitionError> { if self.eth1_data_votes.len() >= ETH1_DATA_VOTES_LEN { return Err(BlockError::Eth1VotesFull.into()); } - self.eth1_data_votes.push(body.eth1_data); + self.eth1_data_votes + .push(body.eth1_data) + .map_err(|_| BlockError::Eth1VotesFull)?; let votes_for = self .eth1_data_votes .iter() diff --git a/moonglass/src/state_transition/block/parent_payload.rs b/moonglass-core/src/state_transition/block/parent_payload.rs similarity index 53% rename from moonglass/src/state_transition/block/parent_payload.rs rename to moonglass-core/src/state_transition/block/parent_payload.rs index 3845f4f..0cf49da 100644 --- a/moonglass/src/state_transition/block/parent_payload.rs +++ b/moonglass-core/src/state_transition/block/parent_payload.rs @@ -12,68 +12,63 @@ use crate::primitives::{Hash32, Slot}; use crate::state_transition::TreeRootExt; /// Verified parent-payload data that the child block is allowed to settle. -struct ParentPayloadCommitment { +pub struct ParentPayloadCommitment { /// Slot whose payload is being settled. - slot: Slot, + pub slot: Slot, /// Execution block hash promised by the parent bid. - block_hash: Hash32, + pub block_hash: Hash32, /// Builder payment to release or queue when the parent payload is accepted. - payment: BuilderPendingWithdrawal, + pub payment: BuilderPendingWithdrawal, } impl BeaconState { - /// Settle the parent block's delivered payload as the first phase of block processing. + /// Validate and process the parent block's delivered execution payload. /// - /// This is the cross-slot handoff that runs before the current block's own - /// identity and bid are touched. The block's bid must name the parent - /// payload's `block_hash` through `parent_block_hash`, and when it does the - /// carried `parent_execution_requests` must hash-match the request root the - /// parent bid committed to. Once proven, the parent's deposit, withdrawal, - /// and consolidation requests are applied, the parent builder payment is - /// released, the parent bid slot's payload-availability bit is set, and - /// `latest_block_hash` advances to the parent payload's block hash. A bid that - /// extends no parent payload carries no requests and is a no-op, while - /// requests that do not match raise [`BlockError::ParentPayloadRequestsMismatch`]. - /// This runs as a phase of [`BeaconState::process_block`], which itself - /// operates on the clone [`BeaconState::apply_signed_block`] commits only - /// after the whole transition succeeds, so a mid-phase failure is discarded - /// with that clone rather than left in the committed state. - pub fn accept_parent_payload_commitment( + /// The current block's bid must name the previous bid's `block_hash` through + /// `parent_block_hash`. If it does not, the parent was empty and the block + /// must carry no parent execution requests. If it does, those requests must + /// hash-match the root committed by the previous bid before they are applied. + /// This runs as a phase of [`BeaconState::process_block`]. When that entry + /// point is reached through [`BeaconState::apply_signed_block`], the work + /// happens on a cloned state. The clone is committed only after the whole + /// transition succeeds, so a mid-phase failure is discarded rather than left + /// in the caller's state. + pub fn process_parent_execution_payload( &mut self, block: &BeaconBlock, ) -> Result<(), TransitionError> { - let Some(commitment) = self.verify_parent_payload_commitment(block)? else { - return Ok(()); - }; - self.apply_parent_execution_requests(&block.body.parent_execution_requests)?; - self.release_parent_builder_payment(&commitment)?; - self.mark_parent_payload_available(&commitment); - Ok(()) - } - - /// Verify whether `block` extends and proves the parent payload commitment. - fn verify_parent_payload_commitment( - &self, - block: &BeaconBlock, - ) -> Result, TransitionError> { let bid = &block.body.signed_execution_payload_bid.message; let parent_bid = &self.latest_execution_payload_bid; let requests = &block.body.parent_execution_requests; if bid.parent_block_hash != parent_bid.block_hash { if !requests.is_empty() { - return Err(BlockError::ParentPayloadRequestsMismatch.into()); + return Err(BlockError::ParentPayloadUnexpectedRequests.into()); } - return Ok(None); + return Ok(()); } - let mut request_root_source = requests.clone(); + let request_root_source = requests.clone(); let requests_root = request_root_source.tree_root(MerkleError::ExecutionRequests)?; if requests_root != parent_bid.execution_requests_root { return Err(BlockError::ParentPayloadRequestsMismatch.into()); } - Ok(Some(ParentPayloadCommitment { + self.apply_parent_execution_payload(requests) + } + + /// Settle the proven parent payload effects into the child state. + /// + /// Execution requests are processed at the child slot, then the parent bid's + /// builder payment is released from the live payment window or queued + /// directly if that window has aged out. Finally the parent slot is marked + /// payload-available and `latest_block_hash` advances to the parent payload. + pub fn apply_parent_execution_payload( + &mut self, + requests: &ExecutionRequests, + ) -> Result<(), TransitionError> { + let parent_bid = self.latest_execution_payload_bid.clone(); + let commitment = ParentPayloadCommitment { slot: parent_bid.slot, block_hash: parent_bid.block_hash, payment: BuilderPendingWithdrawal { @@ -81,34 +76,8 @@ impl BeaconState { amount: parent_bid.value, builder_index: parent_bid.builder_index, }, - })) - } - - /// Release or queue the builder payment tied to a settled parent payload. - fn release_parent_builder_payment( - &mut self, - commitment: &ParentPayloadCommitment, - ) -> Result<(), TransitionError> { - if let Some(payment_index) = self.builder_payment_index_for_slot(commitment.slot) { - self.settle_builder_payment(payment_index)?; - } else if commitment.payment.amount.as_u64() > 0 { - self.queue_builder_pending_withdrawal(commitment.payment)?; - } - Ok(()) - } - - /// Mark the parent slot's payload as available and advance `latest_block_hash`. - fn mark_parent_payload_available(&mut self, commitment: &ParentPayloadCommitment) { - self.execution_payload_availability - .set(commitment.slot % SLOTS_PER_HISTORICAL_ROOT, true); - self.latest_block_hash = commitment.block_hash; - } + }; - /// Apply execution-layer requests delivered by the proven parent payload. - fn apply_parent_execution_requests( - &mut self, - requests: &ExecutionRequests, - ) -> Result<(), TransitionError> { for d in requests.deposits.iter() { self.process_deposit_request(d)?; } @@ -124,6 +93,16 @@ impl BeaconState { for e in requests.builder_exits.iter() { self.process_builder_exit_request(e)?; } + + if let Some(payment_index) = self.builder_payment_index_for_slot(commitment.slot) { + self.settle_builder_payment(payment_index)?; + } else if commitment.payment.amount.as_u64() > 0 { + self.queue_builder_pending_withdrawal(commitment.payment)?; + } + + self.execution_payload_availability + .set(commitment.slot % SLOTS_PER_HISTORICAL_ROOT, true); + self.latest_block_hash = commitment.block_hash; Ok(()) } } diff --git a/moonglass/src/state_transition/block/sync_aggregate.rs b/moonglass-core/src/state_transition/block/sync_aggregate.rs similarity index 88% rename from moonglass/src/state_transition/block/sync_aggregate.rs rename to moonglass-core/src/state_transition/block/sync_aggregate.rs index b05c8dc..33e7466 100644 --- a/moonglass/src/state_transition/block/sync_aggregate.rs +++ b/moonglass-core/src/state_transition/block/sync_aggregate.rs @@ -16,11 +16,11 @@ use crate::state_transition::{BeaconStateLookup, fast_aggregate_verify}; /// Per-participant and proposer rewards derived for one sync aggregate. #[derive(Clone, Copy)] -struct SyncAggregateRewards { +pub struct SyncAggregateRewards { /// Reward paid to each participating sync committee member. - participant: Gwei, + pub participant: Gwei, /// Reward paid to the block proposer per participant. - proposer: Gwei, + pub proposer: Gwei, } impl BeaconState { @@ -33,7 +33,6 @@ impl BeaconState { /// member's balance is increased by the per-participant reward and the /// block proposer is paid a cut for each, while a non-participating member is /// penalized by the same per-participant amount. - /// Spec: `process_sync_aggregate` pub fn process_sync_aggregate( &mut self, sync_aggregate: &SyncAggregate, @@ -41,13 +40,13 @@ impl BeaconState { let committee_pubkeys = self.sync_committee_pubkeys(); let participant_pubkeys = participating_sync_pubkeys(&committee_pubkeys, sync_aggregate); self.verify_sync_aggregate_signature(sync_aggregate, &participant_pubkeys)?; - let rewards = self.sync_aggregate_rewards(); + let rewards = self.sync_aggregate_rewards()?; self.apply_sync_aggregate_rewards(sync_aggregate, &committee_pubkeys, rewards)?; Ok(()) } /// Public keys of the current sync committee in committee order. - fn sync_committee_pubkeys(&self) -> Vec { + pub fn sync_committee_pubkeys(&self) -> Vec { self.current_sync_committee .pubkeys .iter() @@ -56,7 +55,7 @@ impl BeaconState { } /// Verify the aggregate signature over the previous slot's block root. - fn verify_sync_aggregate_signature( + pub fn verify_sync_aggregate_signature( &self, sync_aggregate: &SyncAggregate, participant_pubkeys: &[BLSPubkey], @@ -74,10 +73,10 @@ impl BeaconState { } /// Compute per-participant and per-proposer sync aggregate rewards. - fn sync_aggregate_rewards(&self) -> SyncAggregateRewards { + pub fn sync_aggregate_rewards(&self) -> Result { let total_active_increments = - self.total_active_balance().as_u64() / EFFECTIVE_BALANCE_INCREMENT.as_u64(); - let base_reward_per_increment = self.base_reward_per_increment().as_u64(); + self.get_total_active_balance()?.as_u64() / EFFECTIVE_BALANCE_INCREMENT.as_u64(); + let base_reward_per_increment = self.get_base_reward_per_increment()?.as_u64(); let total_base_rewards = base_reward_per_increment * total_active_increments; let max_participant_rewards = total_base_rewards * SYNC_REWARD_WEIGHT / WEIGHT_DENOMINATOR / (SLOTS_PER_EPOCH as u64); @@ -85,14 +84,14 @@ impl BeaconState { let proposer_reward = Gwei( participant_reward.as_u64() * PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT), ); - SyncAggregateRewards { + Ok(SyncAggregateRewards { participant: participant_reward, proposer: proposer_reward, - } + }) } /// Apply participant rewards, non-participant penalties, and proposer rewards. - fn apply_sync_aggregate_rewards( + pub fn apply_sync_aggregate_rewards( &mut self, sync_aggregate: &SyncAggregate, committee_pubkeys: &[BLSPubkey], @@ -121,7 +120,7 @@ impl BeaconState { /// /// The bitfield is committee-position based, so this preserves committee order /// before aggregate BLS verification. -fn participating_sync_pubkeys( +pub fn participating_sync_pubkeys( committee_pubkeys: &[BLSPubkey], sync_aggregate: &SyncAggregate, ) -> Vec { diff --git a/moonglass/src/state_transition/builder.rs b/moonglass-core/src/state_transition/builder.rs similarity index 74% rename from moonglass/src/state_transition/builder.rs rename to moonglass-core/src/state_transition/builder.rs index 5c838ae..a696013 100644 --- a/moonglass/src/state_transition/builder.rs +++ b/moonglass-core/src/state_transition/builder.rs @@ -4,7 +4,7 @@ //! payload-timeliness committee voting, builder exits and slashings, and the //! per-slot pending-payment settlement. -mod bids; -mod lifecycle; -mod payload_attestations; -mod payments; +pub mod bids; +pub mod lifecycle; +pub mod payload_attestations; +pub mod payments; diff --git a/moonglass/src/state_transition/builder/bids.rs b/moonglass-core/src/state_transition/builder/bids.rs similarity index 70% rename from moonglass/src/state_transition/builder/bids.rs rename to moonglass-core/src/state_transition/builder/bids.rs index e59cc84..070ff37 100644 --- a/moonglass/src/state_transition/builder/bids.rs +++ b/moonglass-core/src/state_transition/builder/bids.rs @@ -1,42 +1,38 @@ //! Builder bid validation and current-slot bid commitment. //! -//! The bid path answers one narrow question: may this proposer commit the -//! current slot to this builder's promised payload? If yes, the bid is written -//! into [`BeaconState::latest_execution_payload_bid`] and the builder's pending -//! payment obligation is opened. The payload itself is not accepted here. The -//! envelope path later checks the delivered payload against this commitment, and -//! a child block settles the parent payload's execution requests when it proves -//! the handoff. +//! The [bid](crate::glossary#execution-payload-bid) path answers one narrow +//! question: may this proposer commit the current slot to this +//! [builder](crate::glossary#builder)'s promised payload? If yes, the bid is +//! written into [`BeaconState::latest_execution_payload_bid`] and the builder's +//! pending payment obligation is opened. The payload itself is not accepted +//! here. The [envelope](crate::glossary#execution-payload-envelope) path later +//! checks the delivered payload against this commitment, and a child block +//! settles the parent payload's execution requests when it proves the +//! [handoff](crate::glossary#parent-payload-handoff). use crate::constants::{ - BLOB_SCHEDULE, BUILDER_INDEX_SELF_BUILD, DOMAIN_BEACON_BUILDER, GENESIS_SLOT, - MAX_BLOBS_PER_BLOCK, MIN_DEPOSIT_AMOUNT, PAYLOAD_BUILDER_VERSION, SLOTS_PER_EPOCH, + BUILDER_INDEX_SELF_BUILD, DOMAIN_BEACON_BUILDER, GENESIS_SLOT, MIN_DEPOSIT_AMOUNT, + PAYLOAD_BUILDER_VERSION, SLOTS_PER_EPOCH, }; use crate::containers::{ BeaconState, BuilderPendingPayment, BuilderPendingWithdrawal, ExecutionPayloadBid, - SignedExecutionPayloadBid, + SignedExecutionPayloadBid, get_blob_parameters, +}; +use crate::error::{ + MerkleError, OperationError, SignatureError, TransitionArithmetic, TransitionError, }; -use crate::error::{MerkleError, OperationError, SignatureError, TransitionError}; use crate::primitives::{BuilderIndex, Epoch, Gwei, Slot, ValidatorIndex}; use crate::state_transition::{BeaconStateLookup, verify_signature}; impl BeaconState { /// Resolve the blob-commitment limit a bid must respect at `epoch`. /// - /// The most recent `BLOB_SCHEDULE` entry whose epoch has been reached gives - /// the active limit, falling back to `MAX_BLOBS_PER_BLOCK` when none applies. + /// The active blob-parameter tuple at `epoch` gives the block limit. /// This is the cap [`BeaconState::process_execution_payload_bid`] compares a /// bid's `blob_kzg_commitments` length against. - /// # Panics - /// Panics only if the active blob limit cannot fit in a host `usize`. - #[must_use] pub fn max_blobs_per_block_at(epoch: Epoch) -> usize { - let active = BLOB_SCHEDULE - .iter() - .rev() - .find_map(|(entry_epoch, limit)| (epoch >= *entry_epoch).then_some(*limit)) - .unwrap_or(MAX_BLOBS_PER_BLOCK); - usize::try_from(active).expect("blob limit fits host usize") + let active = get_blob_parameters(epoch).max_blobs_per_block; + usize::try_from(active).unwrap_or(usize::MAX) } /// True when the builder can fund `bid_value` without dipping into reserves. @@ -45,25 +41,30 @@ impl BeaconState { /// already-queued outflow before the bid is charged against what remains. /// Queued outflows span both `builder_pending_withdrawals` and the /// payment-side of `builder_pending_payments`, so a builder cannot double - /// commit the same stake across overlapping slots. A self-build bid is - /// always considered funded since it carries no value. - #[must_use] - pub fn builder_balance_covers_bid(&self, builder_index: BuilderIndex, bid_value: Gwei) -> bool { - if builder_index == BUILDER_INDEX_SELF_BUILD { - return true; - } - let Some(builder) = self.builders.get(builder_index.as_usize()) else { - return false; - }; - let pending = self - .pending_balance_to_withdraw_for_builder(builder_index) - .as_u64(); - let min_balance = MIN_DEPOSIT_AMOUNT.as_u64().saturating_add(pending); - let balance = builder.balance.as_u64(); - if balance < min_balance { - return false; + /// commit the same stake across overlapping slots. + pub fn can_builder_cover_bid( + &self, + builder_index: BuilderIndex, + bid_value: Gwei, + ) -> Result { + let builder_balance = self.builder(builder_index)?.balance; + let pending = self.get_pending_balance_to_withdraw_for_builder(builder_index)?; + let min_balance = + MIN_DEPOSIT_AMOUNT + .checked_add(pending) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))?; + if builder_balance < min_balance { + return Ok(false); } - balance - min_balance >= bid_value.as_u64() + let available = + builder_balance + .checked_sub(min_balance) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))?; + Ok(available >= bid_value) } /// Verify the builder's BLS signature on a payload bid. @@ -71,20 +72,15 @@ impl BeaconState { /// Non-self-build bids are signed by the builder named in `builder_index` /// under the builder domain at the state's current epoch, and a signature /// that does not verify raises a [`SignatureError::ExecutionPayloadBid`]. - /// A self-build bid has no external builder and skips signature verification, - /// since its authenticity rides on the block proposer's own signature instead. pub fn verify_execution_payload_bid_signature( &self, signed_bid: &SignedExecutionPayloadBid, ) -> Result<(), TransitionError> { let builder_index = signed_bid.message.builder_index; - if builder_index == BUILDER_INDEX_SELF_BUILD { - return Ok(()); - } let builder = self.builder(builder_index)?; - let mut bid_msg = signed_bid.message.clone(); + let bid_msg = signed_bid.message.clone(); let signing_root = self.signing_root_for( - &mut bid_msg, + &bid_msg, DOMAIN_BEACON_BUILDER, self.slot.epoch(), MerkleError::ExecutionPayloadBid, @@ -105,7 +101,7 @@ impl BeaconState { /// blob-commitment limit. Self-builds must carry zero value and the point at /// infinity as the bid signature, relying on the proposer's signed block for /// authenticity. Other bids must name an active builder whose balance covers - /// the value, validated through [`BeaconState::builder_balance_covers_bid`]. + /// the value, validated through [`BeaconState::can_builder_cover_bid`]. /// On success the bid is stored in /// [`BeaconState::latest_execution_payload_bid`] as the terms the later /// payload envelope must satisfy. A non-zero bid opens a builder @@ -116,7 +112,6 @@ impl BeaconState { /// parent-payload handoff. /// Identity and funding failures raise [`OperationError`]. BLS failures /// raise [`SignatureError`]. Both surface as [`TransitionError`]. - /// Spec: `process_execution_payload_bid` pub fn process_execution_payload_bid( &mut self, signed_bid: &SignedExecutionPayloadBid, @@ -137,7 +132,7 @@ impl BeaconState { /// the state's history at the previous slot, not from any containing block, so /// the bid can be checked on its own. It does not check signer status or /// builder funding. - fn validate_bid_identity(&self, bid: &ExecutionPayloadBid) -> Result<(), TransitionError> { + pub fn validate_bid_identity(&self, bid: &ExecutionPayloadBid) -> Result<(), TransitionError> { if bid.slot != self.slot || self.slot == GENESIS_SLOT { return Err(OperationError::BuilderBidSlotMismatch.into()); } @@ -164,7 +159,7 @@ impl BeaconState { } /// Check the bid signer, self-build sentinel rules, and builder funding. - fn validate_bid_signer_and_funding( + pub fn validate_bid_signer_and_funding( &self, signed_bid: &SignedExecutionPayloadBid, ) -> Result<(), TransitionError> { @@ -186,7 +181,7 @@ impl BeaconState { if self.builder(builder_index)?.version != PAYLOAD_BUILDER_VERSION { return Err(OperationError::BuilderNotPayloadVersion(builder_index).into()); } - if !self.builder_balance_covers_bid(builder_index, bid.value) { + if !self.can_builder_cover_bid(builder_index, bid.value)? { return Err(OperationError::BuilderInsufficientBalance(builder_index).into()); } self.verify_execution_payload_bid_signature(signed_bid) @@ -196,7 +191,11 @@ impl BeaconState { /// /// This records only the bid commitment. The payload itself is still checked /// later through the envelope path and settled by a child block. - fn record_accepted_bid(&mut self, bid: &ExecutionPayloadBid, proposer_index: ValidatorIndex) { + pub fn record_accepted_bid( + &mut self, + bid: &ExecutionPayloadBid, + proposer_index: ValidatorIndex, + ) { self.latest_execution_payload_bid = bid.clone(); if bid.value.as_u64() == 0 { return; diff --git a/moonglass-core/src/state_transition/builder/lifecycle.rs b/moonglass-core/src/state_transition/builder/lifecycle.rs new file mode 100644 index 0000000..debe9d5 --- /dev/null +++ b/moonglass-core/src/state_transition/builder/lifecycle.rs @@ -0,0 +1,45 @@ +//! [Builder](crate::glossary#builder) registry lifecycle: activations and exits. + +use crate::constants::{FAR_FUTURE_EPOCH, MIN_BUILDER_WITHDRAWABILITY_DELAY}; +use crate::containers::BeaconState; +use crate::error::{TransitionArithmetic, TransitionError}; +use crate::primitives::{BuilderIndex, Epoch}; +use crate::state_transition::BeaconStateLookup; + +impl BeaconState { + /// True if the builder at `builder_index` is in the active set. + /// + /// A builder is active once its `deposit_epoch` is finalized and while its + /// `withdrawable_epoch` is still `FAR_FUTURE_EPOCH`, that is, it has not yet + /// initiated exit. Bid acceptance and builder exit both gate on this, so a + /// builder that has scheduled departure can no longer win a slot. An + /// out-of-range index raises a registry error. + pub fn is_active_builder(&self, builder_index: BuilderIndex) -> Result { + let builder = self.builder(builder_index)?; + Ok(builder.deposit_epoch < self.finalized_checkpoint.epoch + && builder.withdrawable_epoch == FAR_FUTURE_EPOCH) + } + + /// Schedule a builder's departure from the active set. + /// + /// The builder's `withdrawable_epoch` is set to the current epoch plus + /// `MIN_BUILDER_WITHDRAWABILITY_DELAY`, after which + /// [`BeaconState::is_active_builder`] reports it inactive and its remaining + /// balance can be swept. + pub fn initiate_builder_exit( + &mut self, + builder_index: BuilderIndex, + ) -> Result<(), TransitionError> { + let _ = self.builder(builder_index)?; + let current_epoch = self.slot.epoch(); + let withdrawable = current_epoch + .as_u64() + .checked_add(MIN_BUILDER_WITHDRAWABILITY_DELAY) + .map(Epoch::new) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Epoch, + ))?; + self.builders[builder_index.as_usize()].withdrawable_epoch = withdrawable; + Ok(()) + } +} diff --git a/moonglass/src/state_transition/builder/payload_attestations.rs b/moonglass-core/src/state_transition/builder/payload_attestations.rs similarity index 71% rename from moonglass/src/state_transition/builder/payload_attestations.rs rename to moonglass-core/src/state_transition/builder/payload_attestations.rs index c7edd70..c1f8e9a 100644 --- a/moonglass/src/state_transition/builder/payload_attestations.rs +++ b/moonglass-core/src/state_transition/builder/payload_attestations.rs @@ -1,15 +1,20 @@ //! Payload-attestation validation during block application. //! -//! A block carries aggregate PTC votes about the previous slot's payload. State -//! transition validates the aggregate signature and that the vote targets the -//! parent slot, but it does not write fork-choice vote vectors. After -//! `on_block` stores the post-state, fork choice replays these validated -//! aggregates into local PTC vote maps by committee position. +//! A block carries aggregate [payload attestation](crate::glossary#payload-attestation) +//! votes from the [payload-timeliness committee](crate::glossary#payload-timeliness-committee) +//! about the previous slot's payload. State transition validates the aggregate +//! signature and that the vote targets the parent slot, but it does not write +//! [fork-choice](crate::glossary#fork-choice) vote vectors. After `on_block` +//! stores the post-state, fork choice replays these validated aggregates into +//! local PTC vote maps by committee position. use crate::constants::{DOMAIN_PTC_ATTESTER, PTC_SIZE, SLOTS_PER_EPOCH}; use crate::containers::{BeaconState, IndexedPayloadAttestation, PayloadAttestation}; -use crate::error::{MerkleError, OperationError, SignatureError, TransitionError}; +use crate::error::{ + BoundedList, MerkleError, OperationError, SignatureError, TransitionArithmetic, TransitionError, +}; use crate::primitives::{BLSPubkey, Slot, ValidatorIndex}; +use crate::ssz::List; use crate::state_transition::{BeaconStateLookup, compute_signing_root, fast_aggregate_verify}; impl BeaconState { @@ -21,24 +26,22 @@ impl BeaconState { /// and signature. A slot with no live committee assignment raises /// [`OperationError::PayloadAttestationSlotMismatch`]. The sorted indices may /// repeat, since one validator can hold several committee positions. - pub fn indexed_payload_attestation( + pub fn get_indexed_payload_attestation( &self, - slot: Slot, attestation: &PayloadAttestation, ) -> Result { - let ptc_index = self - .ptc_window_index_for_slot(slot) - .ok_or(OperationError::PayloadAttestationSlotMismatch)?; - let committee = &self.ptc_window[ptc_index]; + let committee = self.get_ptc(attestation.data.slot)?; let mut attesting: Vec = committee .iter() .zip(attestation.aggregation_bits.iter()) .filter_map(|(vi, bit)| if *bit { Some(*vi) } else { None }) .collect(); attesting.sort_by_key(|v| v.as_u64()); - let mut indices = ssz_rs::List::::default(); + let mut indices = List::::default(); for vi in attesting { - indices.push(vi); + indices.push(vi).map_err(|_| { + TransitionError::BoundedListFull(BoundedList::IndexedPayloadAttestationIndices) + })?; } Ok(IndexedPayloadAttestation { attesting_indices: indices, @@ -56,15 +59,15 @@ impl BeaconState { /// validator can occupy several committee positions. An empty set, an /// out-of-order set, or a bad aggregate raises the matching /// [`OperationError`] or [`SignatureError`]. - pub fn validate_indexed_payload_attestation( + pub fn is_valid_indexed_payload_attestation( &self, indexed: &IndexedPayloadAttestation, ) -> Result<(), TransitionError> { if indexed.attesting_indices.is_empty() { return Err(OperationError::AttestationParticipantsEmpty.into()); } - // Spec: payload-attestation indices must be sorted. Unlike indexed - // beacon attestations, duplicate validator indices are valid because a + // Payload-attestation indices must be sorted. Unlike indexed beacon + // attestations, duplicate validator indices are valid because a // validator may appear in multiple PTC positions. if !indexed.attesting_indices.is_sorted() { return Err(OperationError::IndexedAttestationNotSorted.into()); @@ -74,10 +77,10 @@ impl BeaconState { .iter() .map(|i| self.validator(*i).map(|v| v.pubkey)) .collect::>()?; - let mut data = indexed.data; + let data = indexed.data; let domain = self.domain_for(DOMAIN_PTC_ATTESTER, data.slot.epoch())?; let signing_root = - compute_signing_root(&mut data, domain, MerkleError::PayloadAttestationData)?; + compute_signing_root(&data, domain, MerkleError::PayloadAttestationData)?; fast_aggregate_verify( &pubkeys, signing_root, @@ -86,14 +89,23 @@ impl BeaconState { ) } + /// Backward-compatible name for callers outside this builder pass. + pub fn validate_indexed_payload_attestation( + &self, + indexed: &IndexedPayloadAttestation, + ) -> Result<(), TransitionError> { + self.is_valid_indexed_payload_attestation(indexed) + } + /// Validate a payload-timeliness aggregate over the previous slot's payload. /// /// Called from `process_operations` after `process_block_header`, so /// `latest_block_header.parent_root` is the containing block's parent root. /// The attestation data must reference that parent block through - /// `beacon_block_root` and `self.slot - 1` through `slot`, and the expanded - /// aggregate signature must verify under the assigned payload-timeliness - /// committee. Shape and targeting failures raise [`OperationError`]. BLS + /// `beacon_block_root` and the slot immediately before `self.slot`, and the + /// expanded aggregate signature must verify under the assigned + /// payload-timeliness committee. Shape and targeting failures raise + /// [`OperationError`]. BLS /// failures raise [`SignatureError`]. Both surface as [`TransitionError`]. /// This writes no `BeaconState`. The payload attestation votes on timeliness /// and data availability and does not add builder-payment weight. That @@ -101,7 +113,6 @@ impl BeaconState { /// [`BeaconState::process_attestation`]. The per-slot payload-availability /// bit is set separately when the child block accepts the parent payload, and /// [`crate::fork_choice::Store::on_block()`] later records local PTC vote vectors. - /// Spec: `process_payload_attestation` pub fn process_payload_attestation( &mut self, attestation: &PayloadAttestation, @@ -111,12 +122,14 @@ impl BeaconState { if data.beacon_block_root != parent_block_root { return Err(OperationError::PayloadAttestationBlockRootMismatch.into()); } - let parent_slot = self.slot.saturating_sub(1); - if data.slot != parent_slot { + let next_slot = data.slot.as_u64().checked_add(1).map(Slot::new).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Slot), + )?; + if next_slot != self.slot { return Err(OperationError::PayloadAttestationSlotMismatch.into()); } - let indexed = self.indexed_payload_attestation(data.slot, attestation)?; - self.validate_indexed_payload_attestation(&indexed) + let indexed = self.get_indexed_payload_attestation(attestation)?; + self.is_valid_indexed_payload_attestation(&indexed) } /// Resolve a slot into its bucket inside `ptc_window`. @@ -126,7 +139,6 @@ impl BeaconState { /// `slot` must fall within those three epochs relative to `state.slot`. /// A slot two or more epochs in the past returns `None`, as does a slot /// two or more epochs in the future. - #[must_use] pub fn ptc_window_index_for_slot(&self, slot: Slot) -> Option { let epoch = slot.epoch().as_u64(); let state_epoch = self.slot.epoch().as_u64(); diff --git a/moonglass/src/state_transition/builder/payments.rs b/moonglass-core/src/state_transition/builder/payments.rs similarity index 72% rename from moonglass/src/state_transition/builder/payments.rs rename to moonglass-core/src/state_transition/builder/payments.rs index 3be8761..8acbf07 100644 --- a/moonglass/src/state_transition/builder/payments.rs +++ b/moonglass-core/src/state_transition/builder/payments.rs @@ -1,11 +1,12 @@ -//! Builder payment window accounting and pending withdrawals. +//! [Builder payment window](crate::glossary#builder-payment-window) accounting +//! and pending withdrawals. use crate::constants::{ BUILDER_PAYMENT_THRESHOLD_DENOMINATOR, BUILDER_PAYMENT_THRESHOLD_NUMERATOR, BUILDER_PENDING_WITHDRAWALS_LIMIT, SLOTS_PER_EPOCH, }; use crate::containers::{BeaconState, BuilderPendingPayment, BuilderPendingWithdrawal}; -use crate::error::{OperationError, TransitionError}; +use crate::error::{OperationError, TransitionArithmetic, TransitionError}; use crate::primitives::{BuilderIndex, Gwei, Slot}; impl BeaconState { @@ -16,7 +17,6 @@ impl BeaconState { /// either epoch maps to a live entry and `None` marks a slot that has aged /// out. Callers use this to find the pending payment a beacon attestation for /// the proposal slot should weight or a parent payment release should settle. - #[must_use] pub fn builder_payment_index_for_slot(&self, slot: Slot) -> Option { let offset = slot % SLOTS_PER_EPOCH; if slot.epoch() == self.slot.epoch() { @@ -35,13 +35,14 @@ impl BeaconState { /// stake. A payment whose accumulated weight reaches this value clears the /// quorum check in [`BeaconState::settle_builder_payment_if_quorum`], and one /// that never does is dropped when its window ages out. - #[must_use] - pub fn builder_payment_quorum_threshold(&self) -> Gwei { - let per_slot_balance = self.total_active_balance().as_u64() / SLOTS_PER_EPOCH as u64; - Gwei( - per_slot_balance.saturating_mul(BUILDER_PAYMENT_THRESHOLD_NUMERATOR) - / BUILDER_PAYMENT_THRESHOLD_DENOMINATOR.max(1), - ) + pub fn get_builder_payment_quorum_threshold(&self) -> Result { + let per_slot_balance = self.get_total_active_balance()?.as_u64() / SLOTS_PER_EPOCH as u64; + let quorum = per_slot_balance + .checked_mul(BUILDER_PAYMENT_THRESHOLD_NUMERATOR) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; + Ok(Gwei(quorum / BUILDER_PAYMENT_THRESHOLD_DENOMINATOR)) } /// Sum the balance still owed by `builder_index` across both pending queues. @@ -51,23 +52,38 @@ impl BeaconState { /// `builder_pending_payments` (the active window not yet finalized into /// withdrawals). Both queues must drain to zero before a builder may exit, /// and this same reserved amount is what - /// [`BeaconState::builder_balance_covers_bid`] holds back when checking a new + /// [`BeaconState::can_builder_cover_bid`] holds back when checking a new /// bid. - #[must_use] - pub fn pending_balance_to_withdraw_for_builder(&self, builder_index: BuilderIndex) -> Gwei { - let withdrawals = self + pub fn get_pending_balance_to_withdraw_for_builder( + &self, + builder_index: BuilderIndex, + ) -> Result { + let mut total = Gwei::ZERO; + for amount in self .builder_pending_withdrawals .iter() .filter(|w| w.builder_index == builder_index) .map(|w| w.amount) - .fold(Gwei::ZERO, Gwei::saturating_add); - let payments = self + { + total = total + .checked_add(amount) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))?; + } + for amount in self .builder_pending_payments .iter() .filter(|p| p.withdrawal.builder_index == builder_index) .map(|p| p.withdrawal.amount) - .fold(Gwei::ZERO, Gwei::saturating_add); - withdrawals.saturating_add(payments) + { + total = total + .checked_add(amount) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))?; + } + Ok(total) } /// Release the pending payment at `payment_index` into the withdrawal queue. @@ -93,7 +109,7 @@ impl BeaconState { /// Release `payment` only when its accumulated weight reached the quorum. /// /// A non-zero payment whose `weight` is at or above - /// [`BeaconState::builder_payment_quorum_threshold`] is queued onto + /// [`BeaconState::get_builder_payment_quorum_threshold`] is queued onto /// `builder_pending_withdrawals`, and one below it is left to be discarded as /// the window advances. This epoch-boundary path uses beacon attestations for /// the proposal slot to decide whether the builder's promised payment is @@ -102,9 +118,7 @@ impl BeaconState { &mut self, payment: BuilderPendingPayment, ) -> Result<(), TransitionError> { - if payment.withdrawal.amount.as_u64() > 0 - && payment.weight >= self.builder_payment_quorum_threshold() - { + if payment.weight >= self.get_builder_payment_quorum_threshold()? { self.queue_builder_pending_withdrawal(payment.withdrawal)?; } Ok(()) @@ -112,14 +126,16 @@ impl BeaconState { /// Append a builder-pending withdrawal, rejecting when the queue is at its /// hard cap. - pub(crate) fn queue_builder_pending_withdrawal( + pub fn queue_builder_pending_withdrawal( &mut self, withdrawal: BuilderPendingWithdrawal, ) -> Result<(), TransitionError> { if self.builder_pending_withdrawals.len() >= BUILDER_PENDING_WITHDRAWALS_LIMIT { return Err(OperationError::BuilderPendingWithdrawalsFull.into()); } - self.builder_pending_withdrawals.push(withdrawal); + self.builder_pending_withdrawals + .push(withdrawal) + .map_err(|_| OperationError::BuilderPendingWithdrawalsFull)?; Ok(()) } } diff --git a/moonglass-core/src/state_transition/committee.rs b/moonglass-core/src/state_transition/committee.rs new file mode 100644 index 0000000..f5de904 --- /dev/null +++ b/moonglass-core/src/state_transition/committee.rs @@ -0,0 +1,461 @@ +//! Committee and proposer-assignment behavior. +//! +//! Committees distribute validator duties so no small, predictable set controls +//! attestations for a slot. The flow is: collect the active validator set, +//! derive a RANDAO-backed seed, conceptually shuffle the active set, then slice +//! it into committees across the epoch. Proposer sampling uses the same +//! randomness with effective-balance weighting. Per-slot proposer lookup reads +//! the precomputed `state.proposer_lookahead`. + +use sha2::{Digest, Sha256}; + +use crate::constants::{ + DOMAIN_BEACON_ATTESTER, DOMAIN_PTC_ATTESTER, EPOCHS_PER_HISTORICAL_VECTOR, + MAX_COMMITTEES_PER_SLOT, MAX_EFFECTIVE_BALANCE, MIN_SEED_LOOKAHEAD, PTC_SIZE, + SHUFFLE_ROUND_COUNT, SLOTS_PER_EPOCH, TARGET_COMMITTEE_SIZE, +}; +use crate::containers::BeaconState; +use crate::error::{BlockError, OperationError, TransitionArithmetic, TransitionError}; +use crate::primitives::{ + Bytes32, CommitteeIndex, DomainType, Epoch, Slot, ValidatorIndex, u64_to_usize, +}; +use crate::ssz::{Bitvector, Vector}; +use crate::state_transition::BeaconStateLookup; + +/// Random-sample cap used by effective-balance-weighted validator sampling. +pub const MAX_RANDOM_VALUE: u64 = (1 << 16) - 1; + +/// Hash the seed inputs in the order used by the beacon-chain helpers. +pub fn seed_from_parts(domain_type: DomainType, epoch: Epoch, mix: Bytes32) -> Bytes32 { + let mut hasher = Sha256::new(); + hasher.update(domain_type.0); + hasher.update(epoch.as_u64().to_le_bytes()); + hasher.update(mix); + hasher.finalize().into() +} + +impl BeaconState { + /// Proposer for the state's current slot. + pub fn get_beacon_proposer_index(&self) -> Result { + let offset = self.slot % SLOTS_PER_EPOCH; + self.proposer_lookahead + .get(offset) + .copied() + .ok_or_else(|| BlockError::ProposerLookaheadOutOfRange(self.slot).into()) + } + + /// Existing shorter name retained for current call sites. + pub fn beacon_proposer_index(&self) -> Result { + self.get_beacon_proposer_index() + } + + /// Indices of validators active during `epoch`. + pub fn get_active_validator_indices(&self, epoch: Epoch) -> Vec { + self.validators + .iter() + .enumerate() + .filter_map(|(i, v)| { + v.is_active_validator(epoch) + .then_some(ValidatorIndex(i as u64)) + }) + .collect() + } + + /// Existing shorter name retained for current call sites. + pub fn active_validator_indices(&self, epoch: Epoch) -> Vec { + self.get_active_validator_indices(epoch) + } + + /// Indices of validators active and not slashed during `epoch`. + /// + /// Used for proposer selection: the candidate set excludes slashed validators. + pub fn active_unslashed_validator_indices(&self, epoch: Epoch) -> Vec { + self.get_active_validator_indices(epoch) + .into_iter() + .filter(|index| !self.validators[index.as_usize()].slashed) + .collect() + } + + /// RANDAO ring-buffer slot for `epoch`. + pub fn get_randao_mix(&self, epoch: Epoch) -> Bytes32 { + self.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] + } + + /// Existing shorter name retained for current call sites. + pub fn randao_mix(&self, epoch: Epoch) -> Bytes32 { + self.get_randao_mix(epoch) + } + + /// Seed mixing the domain tag, the epoch, and the randao value. + pub fn get_seed( + &self, + epoch: Epoch, + domain_type: DomainType, + ) -> Result { + let lookback = EPOCHS_PER_HISTORICAL_VECTOR as u64 - MIN_SEED_LOOKAHEAD as u64 - 1; + let mix_epoch = epoch.as_u64().checked_add(lookback).map(Epoch::new).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Epoch), + )?; + let mix = self.get_randao_mix(mix_epoch); + Ok(seed_from_parts(domain_type, epoch, mix)) + } + + /// Existing shorter name retained for current call sites. + pub fn seed(&self, epoch: Epoch, domain_type: DomainType) -> Bytes32 { + let lookback = EPOCHS_PER_HISTORICAL_VECTOR as u64 - MIN_SEED_LOOKAHEAD as u64 - 1; + let mix = self.randao_mix(epoch.saturating_add(lookback)); + seed_from_parts(domain_type, epoch, mix) + } + + /// Number of beacon committees produced per slot in `epoch`. + pub fn get_committee_count_per_slot(&self, epoch: Epoch) -> u64 { + let active = self.get_active_validator_indices(epoch).len() as u64; + let raw = active / SLOTS_PER_EPOCH as u64 / TARGET_COMMITTEE_SIZE; + raw.min(MAX_COMMITTEES_PER_SLOT as u64).max(1) + } + + /// Existing shorter name retained for current call sites. + pub fn committee_count_per_slot(&self, epoch: Epoch) -> u64 { + self.get_committee_count_per_slot(epoch) + } + + /// Beacon committee at (`slot`, `committee_index`). + pub fn get_beacon_committee( + &self, + slot: Slot, + committee_index: CommitteeIndex, + ) -> Result, TransitionError> { + let epoch = slot.epoch(); + let committees_per_slot = self.get_committee_count_per_slot(epoch); + if committee_index.as_u64() >= committees_per_slot { + return Err(BlockError::CommitteeIndexOutOfRange(committee_index).into()); + } + let indices = self.get_active_validator_indices(epoch); + let seed = self.get_seed(epoch, DOMAIN_BEACON_ATTESTER)?; + let count = committees_per_slot + .checked_mul(SLOTS_PER_EPOCH as u64) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))?; + let slot_committee_base = (slot % SLOTS_PER_EPOCH as u64) + .checked_mul(committees_per_slot) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))?; + let index = slot_committee_base + .checked_add(committee_index.as_u64()) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))?; + compute_committee(&indices, seed, index, count) + } + + /// Existing shorter name retained for current call sites. + pub fn beacon_committee( + &self, + slot: Slot, + committee_index: CommitteeIndex, + ) -> Result, TransitionError> { + self.get_beacon_committee(slot, committee_index) + } + + /// Effective-balance-weighted random proposer sample from `indices`. + pub fn compute_proposer_index( + &self, + indices: &[ValidatorIndex], + seed: Bytes32, + ) -> Result { + if indices.is_empty() { + return Err(BlockError::EmptyActiveValidatorSet.into()); + } + let total = indices.len() as u64; + let mut i: u64 = 0; + loop { + let candidate_index = + u64_to_usize(compute_shuffled_index_checked(i % total, total, seed)?); + let candidate = indices[candidate_index]; + let random_bytes = { + let mut hasher = Sha256::new(); + hasher.update(seed); + hasher.update((i / 16).to_le_bytes()); + hasher.finalize() + }; + let offset = u64_to_usize((i % 16) * 2); + let random_value = u64::from(u16::from_le_bytes([ + random_bytes[offset], + random_bytes[offset + 1], + ])); + let effective_balance = self.validator(candidate)?.effective_balance.as_u64(); + let weight = effective_balance.checked_mul(MAX_RANDOM_VALUE).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Weight), + )?; + let threshold = MAX_EFFECTIVE_BALANCE + .as_u64() + .checked_mul(random_value) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; + if weight >= threshold { + return Ok(candidate); + } + i = i.checked_add(1).ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))?; + } + } + + /// Sample `size` indices from `candidates`, weighted by each candidate's + /// effective balance. When `shuffle_indices` is true the candidate ordering + /// is itself permuted through [`compute_shuffled_index_checked`]. Otherwise + /// the candidate list is traversed in order. Duplicates are possible. + pub fn compute_balance_weighted_selection( + &self, + candidates: &[ValidatorIndex], + seed: Bytes32, + size: usize, + shuffle_indices: bool, + ) -> Result, TransitionError> { + if candidates.is_empty() { + return Err(BlockError::EmptyActiveValidatorSet.into()); + } + let total = candidates.len() as u64; + let effective_balances: Vec = candidates + .iter() + .map(|i| self.validator(*i).map(|v| v.effective_balance.as_u64())) + .collect::, _>>()?; + let mut selected: Vec = Vec::with_capacity(size); + let mut i: u64 = 0; + let mut random_bytes = [0_u8; 32]; + while selected.len() < size { + let offset = u64_to_usize((i % 16) * 2); + if offset == 0 { + let mut hasher = Sha256::new(); + hasher.update(seed); + hasher.update((i / 16).to_le_bytes()); + random_bytes = hasher.finalize().into(); + } + let mut next_index = i % total; + if shuffle_indices { + next_index = compute_shuffled_index_checked(next_index, total, seed)?; + } + let next_index_usize = u64_to_usize(next_index); + let weight = effective_balances[next_index_usize] + .checked_mul(MAX_RANDOM_VALUE) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; + let random_value = u64::from(u16::from_le_bytes([ + random_bytes[offset], + random_bytes[offset + 1], + ])); + let threshold = MAX_EFFECTIVE_BALANCE + .as_u64() + .checked_mul(random_value) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; + if weight >= threshold { + selected.push(candidates[next_index_usize]); + } + i = i.checked_add(1).ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))?; + } + Ok(selected) + } + + /// Payload-timeliness committee for `slot`. Concatenates every beacon + /// committee at this slot, derives a slot-specific seed, then samples + /// `PTC_SIZE` indices by effective balance without further shuffling. + pub fn compute_ptc(&self, slot: Slot) -> Result, TransitionError> { + let epoch = slot.epoch(); + let base_seed = self.get_seed(epoch, DOMAIN_PTC_ATTESTER)?; + let seed: Bytes32 = { + let mut hasher = Sha256::new(); + hasher.update(base_seed); + hasher.update(slot.as_u64().to_le_bytes()); + hasher.finalize().into() + }; + let committees_per_slot = self.get_committee_count_per_slot(epoch); + let mut indices: Vec = Vec::new(); + for ci in 0..committees_per_slot { + let committee = self.get_beacon_committee(slot, CommitteeIndex(ci))?; + indices.extend(committee); + } + self.compute_balance_weighted_selection(&indices, seed, PTC_SIZE, false) + } + + /// Payload-timeliness committee for `slot` from the cached window. + pub fn get_ptc(&self, slot: Slot) -> Result, TransitionError> { + let epoch = slot.epoch(); + let state_epoch = self.slot.epoch(); + let offset = if epoch < state_epoch { + let next_epoch = + epoch + .as_u64() + .checked_add(1) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Epoch, + ))?; + if next_epoch != state_epoch.as_u64() { + return Err(OperationError::PayloadAttestationSlotMismatch.into()); + } + slot % SLOTS_PER_EPOCH as u64 + } else { + let max_epoch = state_epoch + .as_u64() + .checked_add(MIN_SEED_LOOKAHEAD as u64) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Epoch, + ))?; + if epoch.as_u64() > max_epoch { + return Err(OperationError::PayloadAttestationSlotMismatch.into()); + } + let epoch_offset = epoch + .as_u64() + .checked_sub(state_epoch.as_u64()) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Epoch, + ))? + .checked_add(1) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Epoch, + ))?; + let bucket_offset = epoch_offset.checked_mul(SLOTS_PER_EPOCH as u64).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::BoundedListLength), + )?; + bucket_offset + .checked_add(slot % SLOTS_PER_EPOCH as u64) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))? + }; + self.ptc_window + .get(u64_to_usize(offset)) + .cloned() + .ok_or_else(|| OperationError::PayloadAttestationSlotMismatch.into()) + } +} + +/// Checked swap-or-not shuffle for `index` inside `index_count`. +pub fn compute_shuffled_index_checked( + index: u64, + index_count: u64, + seed: Bytes32, +) -> Result { + if index_count == 0 { + return Err(BlockError::EmptyActiveValidatorSet.into()); + } + if index >= index_count { + return Err(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + )); + } + let mut current = index; + for round in 0..SHUFFLE_ROUND_COUNT { + let round_byte = u8::try_from(round).map_err(|_| { + TransitionError::ArithmeticOverflow(TransitionArithmetic::BoundedListLength) + })?; + let pivot = pivot_for_round(seed, round_byte, index_count); + let flip = if pivot >= current { + pivot - current + } else { + index_count - (current - pivot) + }; + let position = current.max(flip); + let bit = source_bit(seed, round_byte, position)?; + if bit == 1 { + current = flip; + } + } + Ok(current) +} + +/// Round pivot from the lower bytes of `hash(seed || round)`. +pub fn pivot_for_round(seed: Bytes32, round_byte: u8, index_count: u64) -> u64 { + let mut hasher = Sha256::new(); + hasher.update(seed); + hasher.update([round_byte]); + let digest = hasher.finalize(); + let mut bytes = [0_u8; 8]; + bytes.copy_from_slice(&digest[..8]); + u64::from_le_bytes(bytes) % index_count +} + +/// Bit selector for the swap-or-not source, derived from `hash(seed || round +/// || position/256)`. +pub fn source_bit(seed: Bytes32, round_byte: u8, position: u64) -> Result { + let position_chunk = u32::try_from(position / 256).map_err(|_| { + TransitionError::ArithmeticOverflow(TransitionArithmetic::BoundedListLength) + })?; + let mut hasher = Sha256::new(); + hasher.update(seed); + hasher.update([round_byte]); + hasher.update(position_chunk.to_le_bytes()); + let source = hasher.finalize(); + let byte_index = ((position % 256) / 8) as usize; + let bit_index = (position % 8) as u8; + Ok((source[byte_index] >> bit_index) & 1) +} + +/// Slice of shuffled indices forming committee `index` of `count`. +/// +/// Example in words: if an epoch has `SLOTS_PER_EPOCH * committees_per_slot` +/// committees, each committee index selects one contiguous slice of the +/// shuffled active set. +pub fn compute_committee( + indices: &[ValidatorIndex], + seed: Bytes32, + index: u64, + count: u64, +) -> Result, TransitionError> { + if count == 0 { + return Err(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + )); + } + if index >= count { + return Err(BlockError::CommitteeIndexOutOfRange(CommitteeIndex(index)).into()); + } + let total = indices.len() as u64; + let start = total + .checked_mul(index) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))? + / count; + let end_index = index + .checked_add(1) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))?; + let end = total + .checked_mul(end_index) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))? + / count; + let mut committee = Vec::with_capacity(u64_to_usize(end - start)); + for i in start..end { + let shuffled = compute_shuffled_index_checked(i, total, seed)?; + let validator = indices.get(u64_to_usize(shuffled)).copied().ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::BoundedListLength), + )?; + committee.push(validator); + } + Ok(committee) +} + +/// Set committee indices encoded by a `committee_bits` bitvector. +pub fn get_committee_indices(committee_bits: &Bitvector) -> Vec { + committee_bits + .iter() + .enumerate() + .filter_map(|(i, bit)| (*bit).then_some(CommitteeIndex(i as u64))) + .collect() +} + +/// Existing shorter name retained for current call sites. +pub fn committee_indices(committee_bits: &Bitvector) -> Vec { + get_committee_indices(committee_bits) +} diff --git a/moonglass/src/state_transition/epoch.rs b/moonglass-core/src/state_transition/epoch.rs similarity index 55% rename from moonglass/src/state_transition/epoch.rs rename to moonglass-core/src/state_transition/epoch.rs index a45924e..0b2c1a1 100644 --- a/moonglass/src/state_transition/epoch.rs +++ b/moonglass-core/src/state_transition/epoch.rs @@ -1,24 +1,27 @@ -//! Epoch-boundary processing. +//! [Epoch](crate::glossary#epoch)-boundary processing. //! -//! Runs the epoch-boundary phases that update finality, inactivity, rewards, -//! registry churn, slashings, lifecycle queues, effective balances, historical -//! accumulators, sync committees, proposer lookahead, builder-payment windows, -//! and payload-timeliness committee assignments. +//! Runs the epoch-boundary phases that update +//! [finality](crate::glossary#justification-and-finalization), inactivity, +//! rewards, registry churn, slashings, lifecycle queues, +//! [effective balances](crate::glossary#effective-balance), historical +//! accumulators, sync committees, +//! [proposer lookahead](crate::glossary#proposer-lookahead), +//! [builder-payment windows](crate::glossary#builder-payment-window), and +//! [payload-timeliness committee](crate::glossary#payload-timeliness-committee) +//! assignments. -mod accounting; -mod finality; -mod registry; -mod resets; -mod sync_committees; -mod windows; +pub mod accounting; +pub mod finality; +pub mod registry; +pub mod resets; +pub mod sync_committees; +pub mod windows; use crate::containers::BeaconState; use crate::error::TransitionError; impl BeaconState { - /// Run all epoch sub-phases in consensus order. - /// - /// Spec: `process_epoch` + /// Run all [epoch](crate::glossary#epoch) sub-phases in consensus order. pub fn process_epoch(&mut self) -> Result<(), TransitionError> { self.process_justification_and_finalization()?; self.process_inactivity_updates()?; diff --git a/moonglass/src/state_transition/epoch/accounting.rs b/moonglass-core/src/state_transition/epoch/accounting.rs similarity index 59% rename from moonglass/src/state_transition/epoch/accounting.rs rename to moonglass-core/src/state_transition/epoch/accounting.rs index 4f24e18..cc6c409 100644 --- a/moonglass/src/state_transition/epoch/accounting.rs +++ b/moonglass-core/src/state_transition/epoch/accounting.rs @@ -1,54 +1,56 @@ //! Per-epoch reward and penalty accounting. +use std::collections::HashSet; + use crate::constants::{ EFFECTIVE_BALANCE_INCREMENT, EPOCHS_PER_SLASHINGS_VECTOR, GENESIS_EPOCH, INACTIVITY_SCORE_BIAS, INACTIVITY_SCORE_RECOVERY_RATE, PARTICIPATION_FLAG_WEIGHTS, PROPORTIONAL_SLASHING_MULTIPLIER, TIMELY_TARGET_FLAG_INDEX, }; use crate::containers::BeaconState; -use crate::error::TransitionError; -use crate::primitives::{Gwei, ValidatorIndex}; +use crate::error::{StateTransitionInvariant, TransitionArithmetic, TransitionError}; +use crate::primitives::{Epoch, Gwei, ValidatorIndex}; impl BeaconState { /// Bump per-validator inactivity scores for validators that missed the /// timely-target flag in the previous epoch, and decay scores when not leaking. - /// Spec: `process_inactivity_updates` pub fn process_inactivity_updates(&mut self) -> Result<(), TransitionError> { if self.slot.epoch() == GENESIS_EPOCH { return Ok(()); } let previous = self.previous_epoch(); - let matching: std::collections::HashSet = self + let matching: HashSet = self .unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, previous)? .iter() .map(|i| i.as_u64()) .collect(); - let in_leak = self.is_in_inactivity_leak(); - let eligible = self.eligible_validator_indices(); + let in_leak = self.is_in_inactivity_leak()?; + let eligible = self.eligible_validator_indices()?; for vi in eligible { let idx = vi.as_usize(); if idx >= self.inactivity_scores.len() { - continue; + return Err(StateTransitionInvariant::MissingInactivityScore(vi).into()); } if matching.contains(&vi.as_u64()) { - let s = self.inactivity_scores[idx]; - self.inactivity_scores[idx] = s.saturating_sub(1.min(s)); + let score = self.inactivity_scores[idx]; + self.inactivity_scores[idx] = score.saturating_sub(1.min(score)); } else { - self.inactivity_scores[idx] = - self.inactivity_scores[idx].saturating_add(INACTIVITY_SCORE_BIAS); + self.inactivity_scores[idx] = self.inactivity_scores[idx] + .checked_add(INACTIVITY_SCORE_BIAS) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; } if !in_leak { - let s = self.inactivity_scores[idx]; + let score = self.inactivity_scores[idx]; self.inactivity_scores[idx] = - s.saturating_sub(INACTIVITY_SCORE_RECOVERY_RATE.min(s)); + score.saturating_sub(INACTIVITY_SCORE_RECOVERY_RATE.min(score)); } } Ok(()) } /// Apply per-flag rewards and penalties plus the inactivity-leak deltas. - /// - /// Spec: `process_rewards_and_penalties` pub fn process_rewards_and_penalties(&mut self) -> Result<(), TransitionError> { if self.slot.epoch() == GENESIS_EPOCH { return Ok(()); @@ -66,13 +68,23 @@ impl BeaconState { /// Apply the proportional slashings sweep across all validators in their /// slashing window. - /// Spec: `process_slashings` pub fn process_slashings(&mut self) -> Result<(), TransitionError> { let epoch = self.slot.epoch(); - let total_balance = self.total_active_balance(); - let sum_slashings: u64 = self.slashings.iter().map(|g| g.as_u64()).sum(); + let total_balance = self.get_total_active_balance()?; + let mut sum_slashings: u64 = 0; + for amount in self.slashings.iter().map(|g| g.as_u64()) { + sum_slashings = + sum_slashings + .checked_add(amount) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))?; + } let adjusted = sum_slashings - .saturating_mul(PROPORTIONAL_SLASHING_MULTIPLIER) + .checked_mul(PROPORTIONAL_SLASHING_MULTIPLIER) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))? .min(total_balance.as_u64()); let increment = EFFECTIVE_BALANCE_INCREMENT.as_u64(); // Factor `increment` out of `total_balance` first so the per-increment @@ -83,14 +95,21 @@ impl BeaconState { // `total_active_balance` is floored at `EFFECTIVE_BALANCE_INCREMENT`, // so `total_increments >= 1` and the divisor below is always nonzero. debug_assert!(total_increments >= 1); - let penalty_per_increment = adjusted.checked_div(total_increments).unwrap_or(0); + let penalty_per_increment = adjusted / total_increments; let half = (EPOCHS_PER_SLASHINGS_VECTOR as u64) / 2; + let slash_epoch = epoch.as_u64().checked_add(half).map(Epoch).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Epoch), + )?; let len = self.validators.len(); for i in 0..len { let v = &self.validators[i]; - if v.slashed && epoch.as_u64() + half == v.withdrawable_epoch.as_u64() { + if v.slashed && slash_epoch == v.withdrawable_epoch { let effective_balance_increments = v.effective_balance.as_u64() / increment; - let penalty = penalty_per_increment.saturating_mul(effective_balance_increments); + let penalty = penalty_per_increment + .checked_mul(effective_balance_increments) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))?; self.decrease_balance(ValidatorIndex(i as u64), Gwei(penalty))?; } } diff --git a/moonglass/src/state_transition/epoch/finality.rs b/moonglass-core/src/state_transition/epoch/finality.rs similarity index 57% rename from moonglass/src/state_transition/epoch/finality.rs rename to moonglass-core/src/state_transition/epoch/finality.rs index 477288a..4284d86 100644 --- a/moonglass/src/state_transition/epoch/finality.rs +++ b/moonglass-core/src/state_transition/epoch/finality.rs @@ -1,26 +1,30 @@ -//! Justification and finalization from attestation participation. +//! [Justification and finalization](crate::glossary#justification-and-finalization) +//! from [attestation](crate::glossary#attestation) participation. + +use std::ops::Range; use crate::constants::TIMELY_TARGET_FLAG_INDEX; use crate::constants::{GENESIS_EPOCH, JUSTIFICATION_BITS_LENGTH}; use crate::containers::{BeaconState, Checkpoint}; -use crate::error::TransitionError; -use crate::primitives::Epoch; +use crate::error::{TransitionArithmetic, TransitionError}; +use crate::primitives::{Epoch, Gwei}; +use crate::ssz::Bitvector; /// Snapshot used to apply the four finalization rules after justification bits move. -struct FinalityUpdate { +pub struct FinalityUpdate { /// Previously justified checkpoint before this epoch's update. - old_previous: Checkpoint, + pub old_previous: Checkpoint, /// Currently justified checkpoint before this epoch's update. - old_current: Checkpoint, + pub old_current: Checkpoint, /// Epoch being processed. - current_epoch: Epoch, + pub current_epoch: Epoch, /// Justification bits after shifting and applying current support. - bits: [bool; JUSTIFICATION_BITS_LENGTH], + pub bits: [bool; JUSTIFICATION_BITS_LENGTH], } impl FinalityUpdate { /// Build the finality-rule snapshot from pre-update checkpoints and bits. - fn new( + pub fn new( old_previous: Checkpoint, old_current: Checkpoint, current_epoch: Epoch, @@ -39,82 +43,111 @@ impl FinalityUpdate { /// fire, the LAST matching one wins. Skipping that override would keep /// `finalized` pinned to an older epoch even when a newer checkpoint also /// has full support. - fn finalized_checkpoint(&self) -> Option { + pub fn finalized_checkpoint(&self) -> Result, TransitionError> { let mut finalized = None; - if self.previous_checkpoint_has_three_supporting_epochs() { + if self.previous_checkpoint_has_three_supporting_epochs()? { finalized = Some(self.old_previous); } - if self.previous_checkpoint_has_two_supporting_epochs() { + if self.previous_checkpoint_has_two_supporting_epochs()? { finalized = Some(self.old_previous); } - if self.current_checkpoint_has_two_supporting_epochs() { + if self.current_checkpoint_has_two_supporting_epochs()? { finalized = Some(self.old_current); } - if self.current_checkpoint_has_one_supporting_epoch() { + if self.current_checkpoint_has_one_supporting_epoch()? { finalized = Some(self.old_current); } - finalized + Ok(finalized) } /// True for the rule finalizing the old previous checkpoint after three /// supporting epochs. - fn previous_checkpoint_has_three_supporting_epochs(&self) -> bool { - self.bits_are_set(1..4) && self.old_previous_is(3) + pub fn previous_checkpoint_has_three_supporting_epochs(&self) -> Result { + Ok(self.bits_are_set(1..4) && self.old_previous_is(3)?) } /// True for the rule finalizing the old previous checkpoint after two /// supporting epochs. - fn previous_checkpoint_has_two_supporting_epochs(&self) -> bool { - self.bits_are_set(1..3) && self.old_previous_is(2) + pub fn previous_checkpoint_has_two_supporting_epochs(&self) -> Result { + Ok(self.bits_are_set(1..3) && self.old_previous_is(2)?) } /// True for the rule finalizing the old current checkpoint after two /// supporting epochs. - fn current_checkpoint_has_two_supporting_epochs(&self) -> bool { - self.bits_are_set(0..3) && self.old_current_is(2) + pub fn current_checkpoint_has_two_supporting_epochs(&self) -> Result { + Ok(self.bits_are_set(0..3) && self.old_current_is(2)?) } /// True for the rule finalizing the old current checkpoint after one /// supporting epoch. - fn current_checkpoint_has_one_supporting_epoch(&self) -> bool { - self.bits_are_set(0..2) && self.old_current_is(1) + pub fn current_checkpoint_has_one_supporting_epoch(&self) -> Result { + Ok(self.bits_are_set(0..2) && self.old_current_is(1)?) } /// True when every justification bit in `range` is set. - fn bits_are_set(&self, range: std::ops::Range) -> bool { + pub fn bits_are_set(&self, range: Range) -> bool { self.bits .get(range) .is_some_and(|bits| bits.iter().all(|bit| *bit)) } /// True when the old previous checkpoint is `delta` epochs behind current. - fn old_previous_is(&self, delta: u64) -> bool { - self.old_previous.epoch.as_u64() + delta == self.current_epoch.as_u64() + pub fn old_previous_is(&self, delta: u64) -> Result { + self.checkpoint_is(self.old_previous.epoch, delta) } /// True when the old current checkpoint is `delta` epochs behind current. - fn old_current_is(&self, delta: u64) -> bool { - self.old_current.epoch.as_u64() + delta == self.current_epoch.as_u64() + pub fn old_current_is(&self, delta: u64) -> Result { + self.checkpoint_is(self.old_current.epoch, delta) + } + + /// True when `checkpoint_epoch` is `delta` epochs behind current. + pub fn checkpoint_is( + &self, + checkpoint_epoch: Epoch, + delta: u64, + ) -> Result { + let expected = checkpoint_epoch.as_u64().checked_add(delta).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Epoch), + )?; + Ok(expected == self.current_epoch.as_u64()) } } +/// True when `target` has at least two-thirds of `total` support. +pub fn has_two_thirds_support(target: Gwei, total: Gwei) -> Result { + let lhs = target + .as_u64() + .checked_mul(3) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; + let rhs = total + .as_u64() + .checked_mul(2) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; + Ok(lhs >= rhs) +} + impl BeaconState { - /// Update justified checkpoints and finalize an older checkpoint when the - /// timely-target participation accumulates enough stake. - /// Spec: `process_justification_and_finalization` + /// Update justified + /// [checkpoints](crate::glossary#checkpoint) and finalize an older + /// checkpoint when timely-target participation accumulates enough stake. pub fn process_justification_and_finalization(&mut self) -> Result<(), TransitionError> { let current_epoch = self.slot.epoch(); if current_epoch.as_u64() <= GENESIS_EPOCH.as_u64() + 1 { return Ok(()); } let previous_epoch = self.previous_epoch(); - let total_balance = self.total_active_balance(); + let total_balance = self.get_total_active_balance()?; let previous = self.unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, previous_epoch)?; let current = self.unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, current_epoch)?; - let previous_target = self.total_balance(&previous); - let current_target = self.total_balance(¤t); + let previous_target = self.get_total_balance(&previous)?; + let current_target = self.get_total_balance(¤t)?; let old_previous = self.previous_justified_checkpoint; let old_current = self.current_justified_checkpoint; @@ -127,14 +160,14 @@ impl BeaconState { } self.justification_bits.set(0, false); - if previous_target.as_u64() * 3 >= total_balance.as_u64() * 2 { + if has_two_thirds_support(previous_target, total_balance)? { self.current_justified_checkpoint = Checkpoint { epoch: previous_epoch, root: self.block_root_at_slot(previous_epoch.start_slot()), }; self.justification_bits.set(1, true); } - if current_target.as_u64() * 3 >= total_balance.as_u64() * 2 { + if has_two_thirds_support(current_target, total_balance)? { self.current_justified_checkpoint = Checkpoint { epoch: current_epoch, root: self.block_root_at_slot(current_epoch.start_slot()), @@ -148,7 +181,7 @@ impl BeaconState { current_epoch, justification_bits(&self.justification_bits), ); - if let Some(finalized) = finality.finalized_checkpoint() { + if let Some(finalized) = finality.finalized_checkpoint()? { self.finalized_checkpoint = finalized; } Ok(()) @@ -156,7 +189,7 @@ impl BeaconState { } /// Copy an SSZ justification bitvector into an array for range checks. -fn justification_bits(bits: &ssz_rs::Bitvector) -> [bool; N] { +pub fn justification_bits(bits: &Bitvector) -> [bool; N] { let mut out = [false; N]; for (target, source) in out.iter_mut().zip(bits.iter()) { *target = *source; diff --git a/moonglass/src/state_transition/epoch/registry.rs b/moonglass-core/src/state_transition/epoch/registry.rs similarity index 54% rename from moonglass/src/state_transition/epoch/registry.rs rename to moonglass-core/src/state_transition/epoch/registry.rs index 96d65ad..c5f399f 100644 --- a/moonglass/src/state_transition/epoch/registry.rs +++ b/moonglass-core/src/state_transition/epoch/registry.rs @@ -1,24 +1,31 @@ -//! Validator registry updates: activations, exits, pending deposits. +//! [Validator](crate::glossary#validator) registry updates: activations, +//! exits, pending deposits. use crate::constants::{ EFFECTIVE_BALANCE_INCREMENT, EJECTION_BALANCE, FAR_FUTURE_EPOCH, HYSTERESIS_DOWNWARD_MULTIPLIER, HYSTERESIS_QUOTIENT, HYSTERESIS_UPWARD_MULTIPLIER, - MAX_PENDING_DEPOSITS_PER_EPOCH, MIN_ACTIVATION_BALANCE, + MAX_PENDING_DEPOSITS_PER_EPOCH, MIN_ACTIVATION_BALANCE, SLOTS_PER_EPOCH, }; use crate::containers::{BeaconState, PendingConsolidation, PendingDeposit}; -use crate::error::TransitionError; -use crate::primitives::{Gwei, ValidatorIndex}; +use crate::error::{BoundedList, StateTransitionInvariant, TransitionArithmetic, TransitionError}; +use crate::primitives::{Epoch, Gwei, Slot, ValidatorIndex}; +use crate::ssz::List; +use crate::state_transition::BeaconStateLookup; use crate::state_transition::compute_activation_exit_epoch; /// Convert a processed protocol count into a host queue offset. -fn u64_to_usize(value: u64) -> usize { +/// +/// # Panics +/// +/// Panics if `value` does not fit in `usize` on this host. +pub fn u64_to_usize(value: u64) -> usize { usize::try_from(value).expect("processed queue count fits host usize") } impl BeaconState { /// Move eligible queue entries into the active set, eject underbalanced - /// validators, and consume activation-churn budget. - /// Spec: `process_registry_updates` + /// validators, and consume the activation + /// [churn budget](crate::glossary#churn-budget). pub fn process_registry_updates(&mut self) -> Result<(), TransitionError> { let current = self.slot.epoch(); let len = self.validators.len(); @@ -31,7 +38,7 @@ impl BeaconState { self.validators[i].activation_eligibility_epoch = current.saturating_add(1); } let v = &self.validators[i]; - if v.is_active_at(current) && v.effective_balance <= EJECTION_BALANCE { + if v.is_active_validator(current) && v.effective_balance <= EJECTION_BALANCE { to_exit.push(ValidatorIndex(i as u64)); } } @@ -39,7 +46,7 @@ impl BeaconState { self.initiate_validator_exit(vi)?; } - let activation_epoch = compute_activation_exit_epoch(current); + let activation_epoch = compute_activation_exit_epoch(current)?; let finalized_epoch = self.finalized_checkpoint.epoch.as_u64(); for i in 0..self.validators.len() { let v = &self.validators[i]; @@ -55,22 +62,23 @@ impl BeaconState { /// Drain pending deposits into the registry. Walks the queue in order, /// stopping when the deposit is not yet finalized, the per-epoch deposit /// count cap is reached, or the next not-yet-exited validator's deposit - /// would exceed the activation churn budget. Already-withdrawn validators - /// take their deposit without consuming churn. Exiting validators have - /// their deposit moved to a postponed tail so it is reconsidered after - /// the withdrawable epoch. - /// Spec: `process_pending_deposits` + /// would exceed the activation [churn budget](crate::glossary#churn-budget). + /// Already-withdrawn validators take their deposit without consuming churn. + /// Exiting validators have their deposit moved to a postponed tail so it is + /// reconsidered after the withdrawable epoch. pub fn process_pending_deposits(&mut self) -> Result<(), TransitionError> { - let next_epoch = self.slot.epoch().saturating_add(1); + let next_epoch = checked_epoch_add(self.slot.epoch(), 1)?; let available_for_processing = self .deposit_balance_to_consume - .as_u64() - .saturating_add(self.activation_churn_limit().as_u64()); - let mut processed_amount: u64 = 0; + .checked_add(self.get_activation_churn_limit()?) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Churn, + ))?; + let mut processed_amount = Gwei::ZERO; let mut next_deposit_index: u64 = 0; let mut deposits_to_postpone: Vec = Vec::new(); let mut is_churn_limit_reached = false; - let finalized_slot = self.finalized_checkpoint.epoch.start_slot(); + let finalized_slot = checked_start_slot(self.finalized_checkpoint.epoch)?; let queue: Vec = self.pending_deposits.iter().copied().collect(); for deposit in &queue { @@ -84,7 +92,7 @@ impl BeaconState { let mut is_validator_exited = false; let mut is_validator_withdrawn = false; if let Some(v) = self.validators.iter().find(|v| v.pubkey == deposit.pubkey) { - is_validator_exited = v.exit_epoch < crate::constants::FAR_FUTURE_EPOCH; + is_validator_exited = v.exit_epoch < FAR_FUTURE_EPOCH; is_validator_withdrawn = v.withdrawable_epoch < next_epoch; } @@ -93,26 +101,36 @@ impl BeaconState { } else if is_validator_exited { deposits_to_postpone.push(*deposit); } else { - if processed_amount.saturating_add(deposit.amount.as_u64()) - > available_for_processing - { + let next_processed_amount = processed_amount.checked_add(deposit.amount).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Churn), + )?; + if next_processed_amount > available_for_processing { is_churn_limit_reached = true; break; } - processed_amount = processed_amount.saturating_add(deposit.amount.as_u64()); + processed_amount = next_processed_amount; self.apply_pending_deposit(deposit)?; } - next_deposit_index = next_deposit_index.saturating_add(1); + next_deposit_index = + next_deposit_index + .checked_add(1) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))?; } let consumed_count = u64_to_usize(next_deposit_index); let mut new_queue: Vec = queue.into_iter().skip(consumed_count).collect(); new_queue.extend(deposits_to_postpone); - self.keep_pending_deposits(new_queue); + self.keep_pending_deposits(new_queue)?; self.deposit_balance_to_consume = if is_churn_limit_reached { - Gwei(available_for_processing.saturating_sub(processed_amount)) + available_for_processing + .checked_sub(processed_amount) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Churn, + ))? } else { Gwei::ZERO }; @@ -123,17 +141,16 @@ impl BeaconState { /// validator registry entry. /// New-validator deposits with invalid proof-of-possession are consumed and /// dropped per spec rather than surfaced as transition errors. - fn apply_pending_deposit(&mut self, deposit: &PendingDeposit) -> Result<(), TransitionError> { + pub fn apply_pending_deposit( + &mut self, + deposit: &PendingDeposit, + ) -> Result<(), TransitionError> { let existing = self .validators .iter() .position(|v| v.pubkey == deposit.pubkey); if let Some(idx) = existing { - if idx < self.balances.len() { - self.balances[idx] = self.balances[idx] - .checked_add(deposit.amount) - .ok_or(TransitionError::BalanceOverflow)?; - } + self.increase_balance(ValidatorIndex(idx as u64), deposit.amount)?; return Ok(()); } // Verify proof-of-possession for new validators. Invalid deposits are @@ -154,28 +171,28 @@ impl BeaconState { } /// Replace the pending-deposit queue with entries that remain live. - fn keep_pending_deposits(&mut self, kept: Vec) { - self.pending_deposits = ssz_rs::List::default(); - for d in kept { - self.pending_deposits.push(d); - } + pub fn keep_pending_deposits( + &mut self, + kept: Vec, + ) -> Result<(), TransitionError> { + self.pending_deposits = List::try_from(kept) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::PendingDeposits))?; + Ok(()) } /// Drain consolidations whose source validator is withdrawable next epoch, - /// moving each source's effective balance into the target. Slashed sources - /// drop without moving balance. The walk halts at the first entry whose - /// source is not yet withdrawable, leaving the rest queued for later epochs. - /// Spec: `process_pending_consolidations` + /// moving each source's + /// [effective balance](crate::glossary#effective-balance) into the target. + /// Slashed sources drop without moving balance. The walk halts at the first + /// entry whose source is not yet withdrawable, leaving the rest queued for + /// later epochs. pub fn process_pending_consolidations(&mut self) -> Result<(), TransitionError> { - let next_epoch = self.slot.epoch().saturating_add(1); + let next_epoch = checked_epoch_add(self.slot.epoch(), 1)?; let queue: Vec = self.pending_consolidations.iter().copied().collect(); let mut consumed = 0usize; for entry in &queue { - let Some(source) = self.validators.get(entry.source_index.as_usize()) else { - consumed += 1; - continue; - }; + let source = *self.validator(entry.source_index)?; if source.slashed { consumed += 1; continue; @@ -183,32 +200,34 @@ impl BeaconState { if source.withdrawable_epoch > next_epoch { break; } - let source_effective_balance = self + let source_balance = self .balances .get(entry.source_index.as_usize()) .copied() - .unwrap_or(Gwei::ZERO) - .min(source.effective_balance); + .ok_or(StateTransitionInvariant::MissingBalance(entry.source_index))?; + let source_effective_balance = source_balance.min(source.effective_balance); self.decrease_balance(entry.source_index, source_effective_balance)?; self.increase_balance(entry.target_index, source_effective_balance)?; consumed += 1; } let remaining: Vec = queue.into_iter().skip(consumed).collect(); - self.keep_pending_consolidations(remaining); + self.keep_pending_consolidations(remaining)?; Ok(()) } /// Replace the pending-consolidation queue with entries that remain live. - fn keep_pending_consolidations(&mut self, kept: Vec) { - self.pending_consolidations = ssz_rs::List::default(); - for c in kept { - self.pending_consolidations.push(c); - } + pub fn keep_pending_consolidations( + &mut self, + kept: Vec, + ) -> Result<(), TransitionError> { + self.pending_consolidations = List::try_from(kept) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::PendingConsolidations))?; + Ok(()) } - /// Round each validator's effective balance toward its actual balance, - /// gated by a hysteresis band so it does not oscillate per slot. - /// Spec: `process_effective_balance_updates` + /// Round each validator's + /// [effective balance](crate::glossary#effective-balance) toward its actual + /// balance, gated by a hysteresis band so it does not oscillate per slot. pub fn process_effective_balance_updates(&mut self) -> Result<(), TransitionError> { let hysteresis_increment = EFFECTIVE_BALANCE_INCREMENT.as_u64() / HYSTERESIS_QUOTIENT.max(1); @@ -216,11 +235,28 @@ impl BeaconState { let upward = hysteresis_increment.saturating_mul(HYSTERESIS_UPWARD_MULTIPLIER); let len = self.validators.len(); for i in 0..len { - let balance = self.balances.get(i).copied().unwrap_or(Gwei::ZERO).as_u64(); + let index = ValidatorIndex(i as u64); + let balance = self + .balances + .get(i) + .copied() + .ok_or(StateTransitionInvariant::MissingBalance(index))? + .as_u64(); let v = &mut self.validators[i]; - let max = v.max_effective_balance().as_u64(); + let max = v.get_max_effective_balance().as_u64(); let eff = v.effective_balance.as_u64(); - if balance.saturating_add(downward) < eff || eff.saturating_add(upward) < balance { + let balance_plus_downward = + balance + .checked_add(downward) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))?; + let effective_plus_upward = + eff.checked_add(upward) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))?; + if balance_plus_downward < eff || effective_plus_upward < balance { let rounded = balance - balance % EFFECTIVE_BALANCE_INCREMENT.as_u64(); v.effective_balance = Gwei(rounded.min(max)); } @@ -228,3 +264,25 @@ impl BeaconState { Ok(()) } } + +/// Add `delta` epochs using the transition arithmetic error domain. +pub fn checked_epoch_add(epoch: Epoch, delta: u64) -> Result { + epoch + .as_u64() + .checked_add(delta) + .map(Epoch) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Epoch, + )) +} + +/// Return the first slot in `epoch`. +pub fn checked_start_slot(epoch: Epoch) -> Result { + epoch + .as_u64() + .checked_mul(SLOTS_PER_EPOCH as u64) + .map(Slot) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Slot, + )) +} diff --git a/moonglass/src/state_transition/epoch/resets.rs b/moonglass-core/src/state_transition/epoch/resets.rs similarity index 75% rename from moonglass/src/state_transition/epoch/resets.rs rename to moonglass-core/src/state_transition/epoch/resets.rs index 59624f6..0c1cda2 100644 --- a/moonglass/src/state_transition/epoch/resets.rs +++ b/moonglass-core/src/state_transition/epoch/resets.rs @@ -5,12 +5,17 @@ use crate::constants::{ SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT, }; use crate::containers::{BeaconState, HistoricalSummary}; -use crate::error::{MerkleError, TransitionError}; +use crate::error::{BoundedList, MerkleError, TransitionError}; use crate::primitives::{Gwei, ParticipationFlags}; +use crate::ssz::List; use crate::state_transition::TreeRootExt; /// Reduce a protocol epoch into a host ring-buffer index. -fn ring_index(epoch: u64, period: usize) -> usize { +/// +/// # Panics +/// +/// Panics if `period` or the derived ring index cannot fit on this host. +pub fn ring_index(epoch: u64, period: usize) -> usize { let period = u64::try_from(period).expect("ring-buffer length fits u64"); let index = epoch % period; usize::try_from(index).expect("ring-buffer index fits host usize") @@ -18,19 +23,15 @@ fn ring_index(epoch: u64, period: usize) -> usize { impl BeaconState { /// Clear the deposit-vote bag at the end of each voting period. - /// - /// Spec: `process_eth1_data_reset` pub fn process_eth1_data_reset(&mut self) -> Result<(), TransitionError> { let next_epoch = self.slot.epoch().as_u64() + 1; if next_epoch.is_multiple_of(EPOCHS_PER_ETH1_VOTING_PERIOD as u64) { - self.eth1_data_votes = ssz_rs::List::default(); + self.eth1_data_votes = List::default(); } Ok(()) } /// Clear the next-epoch slot of the slashings ring buffer. - /// - /// Spec: `process_slashings_reset` pub fn process_slashings_reset(&mut self) -> Result<(), TransitionError> { let next_epoch = self.slot.epoch().as_u64() + 1; let idx = ring_index(next_epoch, EPOCHS_PER_SLASHINGS_VECTOR); @@ -39,8 +40,6 @@ impl BeaconState { } /// Copy the current epoch's randao mix into the next epoch's ring-buffer slot. - /// - /// Spec: `process_randao_mixes_reset` pub fn process_randao_mixes_reset(&mut self) -> Result<(), TransitionError> { let current_epoch = self.slot.epoch(); let next_idx = ring_index(current_epoch.as_u64() + 1, EPOCHS_PER_HISTORICAL_VECTOR); @@ -50,35 +49,37 @@ impl BeaconState { } /// Append a historical-summary record at the historical-root boundary. - /// - /// Spec: `process_historical_summaries_update` pub fn process_historical_summaries_update(&mut self) -> Result<(), TransitionError> { let next_epoch = self.slot.epoch().as_u64() + 1; let boundary = (SLOTS_PER_HISTORICAL_ROOT / SLOTS_PER_EPOCH) as u64; if !next_epoch.is_multiple_of(boundary) { return Ok(()); } - let mut block_summary = self.block_roots.clone(); - let mut state_summary = self.state_roots.clone(); + let block_summary = self.block_roots.clone(); + let state_summary = self.state_roots.clone(); let block_summary_root = block_summary.tree_root(MerkleError::BlockRoots)?; let state_summary_root = state_summary.tree_root(MerkleError::StateRoots)?; - self.historical_summaries.push(HistoricalSummary { - block_summary_root, - state_summary_root, - }); + self.historical_summaries + .push(HistoricalSummary { + block_summary_root, + state_summary_root, + }) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::HistoricalSummaries))?; Ok(()) } /// Rotate the per-validator participation flags: current becomes previous, /// current is zeroed at the new validator-set length. - /// Spec: `process_participation_flag_updates` pub fn process_participation_flag_updates(&mut self) -> Result<(), TransitionError> { self.previous_epoch_participation = self.current_epoch_participation.clone(); let len = self.validators.len(); - self.current_epoch_participation = ssz_rs::List::default(); + self.current_epoch_participation = List::default(); for _ in 0..len { self.current_epoch_participation - .push(ParticipationFlags::NONE); + .push(ParticipationFlags::NONE) + .map_err(|_| { + TransitionError::BoundedListFull(BoundedList::CurrentEpochParticipation) + })?; } Ok(()) } diff --git a/moonglass-core/src/state_transition/epoch/sync_committees.rs b/moonglass-core/src/state_transition/epoch/sync_committees.rs new file mode 100644 index 0000000..7ad80fc --- /dev/null +++ b/moonglass-core/src/state_transition/epoch/sync_committees.rs @@ -0,0 +1,55 @@ +//! Next sync-committee selection. + +use std::mem; + +use crate::constants::{ + DOMAIN_SYNC_COMMITTEE, EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SYNC_COMMITTEE_SIZE, +}; +use crate::containers::{BeaconState, SyncCommittee}; +use crate::error::TransitionError; +use crate::primitives::{BLSPubkey, ValidatorIndex}; +use crate::ssz::Vector; +use crate::state_transition::{BeaconStateLookup, aggregate_pubkeys}; + +use super::registry::checked_epoch_add; + +impl BeaconState { + /// At a sync-committee period boundary, roll next into current and resample + /// the next sync committee from the active set at the following epoch. + pub fn process_sync_committee_updates(&mut self) -> Result<(), TransitionError> { + let next_epoch = checked_epoch_add(self.slot.epoch(), 1)?.as_u64(); + if next_epoch.is_multiple_of(EPOCHS_PER_SYNC_COMMITTEE_PERIOD) { + let new_next_sync_committee = self.get_next_sync_committee()?; + self.current_sync_committee = + mem::replace(&mut self.next_sync_committee, new_next_sync_committee); + } + Ok(()) + } + + /// Sample a fresh sync committee for the next sync-committee period. + pub fn get_next_sync_committee(&self) -> Result { + let indices = self.get_next_sync_committee_indices()?; + let pubkeys: Vec = indices + .iter() + .map(|i| self.validator(*i).map(|v| v.pubkey)) + .collect::>()?; + let aggregate_pubkey = aggregate_pubkeys(&pubkeys)?; + let mut pk_vec = Vector::::default(); + for (i, pk) in pubkeys.iter().enumerate().take(SYNC_COMMITTEE_SIZE) { + pk_vec[i] = *pk; + } + Ok(SyncCommittee { + pubkeys: pk_vec, + aggregate_pubkey, + }) + } + + /// Effective-balance-weighted sampling of `SYNC_COMMITTEE_SIZE` validators + /// from the active set at the next epoch. + pub fn get_next_sync_committee_indices(&self) -> Result, TransitionError> { + let epoch = checked_epoch_add(self.slot.epoch(), 1)?; + let active = self.active_validator_indices(epoch); + let seed = self.get_seed(epoch, DOMAIN_SYNC_COMMITTEE)?; + self.compute_balance_weighted_selection(&active, seed, SYNC_COMMITTEE_SIZE, true) + } +} diff --git a/moonglass/src/state_transition/epoch/windows.rs b/moonglass-core/src/state_transition/epoch/windows.rs similarity index 58% rename from moonglass/src/state_transition/epoch/windows.rs rename to moonglass-core/src/state_transition/epoch/windows.rs index 264abb1..1556fc8 100644 --- a/moonglass/src/state_transition/epoch/windows.rs +++ b/moonglass-core/src/state_transition/epoch/windows.rs @@ -1,19 +1,25 @@ -//! Per-epoch rolling-window advancement for PTC and builder payments. +//! Per-epoch rolling-window advancement for +//! [proposer lookahead](crate::glossary#proposer-lookahead), +//! [payload-timeliness committee](crate::glossary#payload-timeliness-committee), +//! and [builder payments](crate::glossary#builder-payment-window). use sha2::{Digest, Sha256}; use crate::constants::{DOMAIN_BEACON_PROPOSER, MIN_SEED_LOOKAHEAD, PTC_SIZE, SLOTS_PER_EPOCH}; use crate::containers::{BeaconState, BuilderPendingPayment}; -use crate::error::{BlockError, TransitionError}; -use crate::primitives::{Bytes32, ValidatorIndex}; +use crate::error::{BlockError, TransitionArithmetic, TransitionError}; +use crate::primitives::{Bytes32, Slot, ValidatorIndex}; +use crate::ssz::Vector; + +use super::registry::{checked_epoch_add, checked_start_slot}; impl BeaconState { - /// Shift the proposer-lookahead window forward by one epoch and fill the - /// next-epoch slots via the effective-balance-weighted sampler. - /// Spec: `process_proposer_lookahead` + /// Shift the [proposer-lookahead](crate::glossary#proposer-lookahead) + /// window forward by one epoch and fill the next-epoch slots via the + /// [effective-balance](crate::glossary#effective-balance)-weighted sampler. pub fn process_proposer_lookahead(&mut self) -> Result<(), TransitionError> { let current = self.slot.epoch(); - let next = current.saturating_add(1 + MIN_SEED_LOOKAHEAD as u64); + let next = checked_epoch_add(current, 1 + MIN_SEED_LOOKAHEAD as u64)?; let indices = self.active_unslashed_validator_indices(next); let seed = self.seed(next, DOMAIN_BEACON_PROPOSER); @@ -28,13 +34,13 @@ impl BeaconState { if indices.is_empty() { return Err(BlockError::EmptyActiveValidatorSet.into()); } - let next_start_slot = next.start_slot().as_u64(); + let next_start_slot = checked_start_slot(next)?; for offset in 0..epoch_slots { - let slot = next_start_slot + offset as u64; + let slot = checked_slot_offset(next_start_slot, offset)?; let slot_seed: Bytes32 = { let mut hasher = Sha256::new(); hasher.update(seed); - hasher.update(slot.to_le_bytes()); + hasher.update(slot.as_u64().to_le_bytes()); hasher.finalize().into() }; let proposer = self.compute_proposer_index(&indices, slot_seed)?; @@ -44,9 +50,9 @@ impl BeaconState { Ok(()) } - /// Settle the oldest builder-payment entries (the just-completed epoch worth) - /// and shift the window forward. - /// Spec: `process_builder_pending_payments` + /// Settle the oldest + /// [builder-payment](crate::glossary#builder-payment-window) entries for + /// the just-completed epoch and shift the window forward. pub fn process_builder_pending_payments(&mut self) -> Result<(), TransitionError> { let window_len = self.builder_pending_payments.len(); if window_len == 0 { @@ -59,7 +65,10 @@ impl BeaconState { } /// Settle the oldest epoch worth of builder-payment entries. - fn settle_builder_payment_window(&mut self, epoch_slots: usize) -> Result<(), TransitionError> { + pub fn settle_builder_payment_window( + &mut self, + epoch_slots: usize, + ) -> Result<(), TransitionError> { let snapshot: Vec = self .builder_pending_payments .iter() @@ -73,7 +82,7 @@ impl BeaconState { } /// Shift the builder-payment window forward and clear the newly empty tail. - fn advance_builder_payment_window(&mut self, epoch_slots: usize, window_len: usize) { + pub fn advance_builder_payment_window(&mut self, epoch_slots: usize, window_len: usize) { for i in 0..(window_len - epoch_slots) { self.builder_pending_payments[i] = self.builder_pending_payments[i + epoch_slots]; } @@ -82,10 +91,11 @@ impl BeaconState { } } - /// Shift the PTC assignment window forward and fill the next-epoch entries - /// via per-slot sampling. Each slot's PTC is computed independently, mixing - /// the slot index into the seed. - /// Spec: `process_ptc_window` + /// Shift the + /// [payload-timeliness committee](crate::glossary#payload-timeliness-committee) + /// assignment window forward and fill the next-epoch entries via per-slot + /// sampling. Each slot's committee is computed independently, mixing the + /// slot index into the seed. pub fn process_ptc_window(&mut self) -> Result<(), TransitionError> { let len = self.ptc_window.len(); if len < SLOTS_PER_EPOCH { @@ -93,16 +103,13 @@ impl BeaconState { } self.advance_ptc_window(len); - let next_epoch = self - .slot - .epoch() - .saturating_add(MIN_SEED_LOOKAHEAD as u64 + 1); - let start_slot = next_epoch.start_slot(); + let next_epoch = checked_epoch_add(self.slot.epoch(), MIN_SEED_LOOKAHEAD as u64 + 1)?; + let start_slot = checked_start_slot(next_epoch)?; let tail_base = len - SLOTS_PER_EPOCH; for offset in 0..SLOTS_PER_EPOCH { - let slot = start_slot.saturating_add(offset as u64); + let slot = checked_slot_offset(start_slot, offset)?; let sample = self.compute_ptc(slot)?; - let mut filled = ssz_rs::Vector::::default(); + let mut filled = Vector::::default(); for (i, vi) in sample.iter().enumerate().take(PTC_SIZE) { filled[i] = *vi; } @@ -112,9 +119,19 @@ impl BeaconState { } /// Shift the PTC assignment window forward by one epoch. - fn advance_ptc_window(&mut self, len: usize) { + pub fn advance_ptc_window(&mut self, len: usize) { for i in 0..(len - SLOTS_PER_EPOCH) { self.ptc_window[i] = self.ptc_window[i + SLOTS_PER_EPOCH].clone(); } } } + +/// Add a small host offset to a slot in the transition arithmetic domain. +pub fn checked_slot_offset(slot: Slot, offset: usize) -> Result { + slot.as_u64() + .checked_add(offset as u64) + .map(Slot) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Slot, + )) +} diff --git a/moonglass-core/src/state_transition/genesis.rs b/moonglass-core/src/state_transition/genesis.rs new file mode 100644 index 0000000..6d8c2c3 --- /dev/null +++ b/moonglass-core/src/state_transition/genesis.rs @@ -0,0 +1,6 @@ +//! Genesis-state checks. +//! +//! Building a state from deposit history belongs to the caller path that chooses +//! whether the devnet starts from deposits or from a provided state. The genesis +//! trigger predicate likewise lives with that caller, which evaluates the +//! thresholds against its runtime config rather than the compile-time preset. diff --git a/moonglass/src/state_transition/operations.rs b/moonglass-core/src/state_transition/operations.rs similarity index 88% rename from moonglass/src/state_transition/operations.rs rename to moonglass-core/src/state_transition/operations.rs index fdf5fa9..5bb7f1a 100644 --- a/moonglass/src/state_transition/operations.rs +++ b/moonglass-core/src/state_transition/operations.rs @@ -6,22 +6,19 @@ //! Execution-layer deposit, withdrawal, and consolidation requests live in this //! module tree, but they are applied only through the parent-payload handoff. -mod attestations; -mod deposits; -mod exits; -mod requests; -mod slashings; +pub mod attestations; +pub mod deposits; +pub mod exits; +pub mod requests; +pub mod slashings; -pub use deposits::is_valid_merkle_branch; -pub(crate) use slashings::is_slashable_attestation_data; +pub use slashings::is_slashable_attestation_data; use crate::containers::{BeaconBlockBody, BeaconState}; use crate::error::{OperationError, TransitionError}; impl BeaconState { /// Apply the body operations in consensus order. - /// - /// Spec: `process_operations` pub fn process_operations(&mut self, body: &BeaconBlockBody) -> Result<(), TransitionError> { // The spec asserts `len(body.deposits) == 0` before any other operation. if !body.deposits.is_empty() { diff --git a/moonglass/src/state_transition/operations/attestations.rs b/moonglass-core/src/state_transition/operations/attestations.rs similarity index 80% rename from moonglass/src/state_transition/operations/attestations.rs rename to moonglass-core/src/state_transition/operations/attestations.rs index a888044..1dde1cb 100644 --- a/moonglass/src/state_transition/operations/attestations.rs +++ b/moonglass-core/src/state_transition/operations/attestations.rs @@ -17,25 +17,29 @@ use crate::constants::{ use crate::containers::{ Attestation, AttestationData, BeaconState, BuilderPendingPayment, IndexedAttestation, }; -use crate::error::{BlockError, MerkleError, OperationError, SignatureError, TransitionError}; +use crate::error::{ + BlockError, BoundedList, MerkleError, OperationError, SignatureError, StateTransitionInvariant, + TransitionArithmetic, TransitionError, +}; use crate::primitives::{BLSPubkey, Gwei, Root, Slot, ValidatorIndex}; -use crate::state_transition::balance::isqrt_u64; +use crate::ssz::List; +use crate::state_transition::balance::integer_squareroot; use crate::state_transition::{ BeaconStateLookup, committee_indices, compute_signing_root, fast_aggregate_verify, }; /// Validated attestation data ready to mutate participation and builder payment state. -struct AcceptedAttestation { +pub struct AcceptedAttestation { /// Sorted validator indices whose aggregation bits participated. - attesting_indices: Vec, + pub attesting_indices: Vec, /// Participation flag indices this attestation earns. - participation_flags: Vec, + pub participation_flags: Vec, /// Whether the target epoch maps to current or previous participation flags. - target_is_current_epoch: bool, + pub target_is_current_epoch: bool, /// Whether the attested block was fresh in the attestation's own slot. - is_same_slot: bool, + pub is_same_slot: bool, /// Builder-payment window slot that may receive proposal-slot attestation weight. - builder_payment_index: usize, + pub builder_payment_index: usize, } impl BeaconState { @@ -52,12 +56,15 @@ impl BeaconState { let committee_indices = committee_indices(&attestation.committee_bits); let mut offset: usize = 0; let mut out: Vec = Vec::new(); - let agg_bits: Vec = attestation.aggregation_bits.iter().map(|b| *b).collect(); for ci in committee_indices { let committee = self.beacon_committee(attestation.data.slot, ci)?; let before = out.len(); for (i, vi) in committee.iter().enumerate() { - if agg_bits.get(offset + i).copied().unwrap_or(false) { + if attestation + .aggregation_bits + .get(offset + i) + .unwrap_or(false) + { out.push(*vi); } } @@ -66,7 +73,7 @@ impl BeaconState { } offset = offset.saturating_add(committee.len()); } - if agg_bits.len() != offset { + if attestation.aggregation_bits.len() != offset { return Err(OperationError::AttestationAggregationBitsLength.into()); } out.sort_by_key(|v| v.as_u64()); @@ -83,9 +90,11 @@ impl BeaconState { attestation: &Attestation, ) -> Result { let attesting = self.attesting_indices(attestation)?; - let mut indices = ssz_rs::List::::default(); + let mut indices = List::::default(); for vi in attesting { - indices.push(vi); + indices.push(vi).map_err(|_| { + TransitionError::BoundedListFull(BoundedList::IndexedAttestationIndices) + })?; } Ok(IndexedAttestation { attesting_indices: indices, @@ -119,15 +128,15 @@ impl BeaconState { .iter() .map(|i| self.validator(*i).map(|v| v.pubkey)) .collect::>()?; - let mut data = indexed.data; + let data = indexed.data; let domain = self.domain_for(DOMAIN_BEACON_ATTESTER, data.target.epoch)?; - let signing_root = compute_signing_root(&mut data, domain, MerkleError::AttestationData)?; + let signing_root = compute_signing_root(&data, domain, MerkleError::AttestationData)?; fast_aggregate_verify(&pubkeys, signing_root, &indexed.signature, on_fail) } /// Flag indices the attestation earns given its source/target/head match and /// inclusion delay. Returns indices into [`PARTICIPATION_FLAG_WEIGHTS`]. - pub fn participation_flags_for( + pub fn get_attestation_participation_flag_indices( &self, data: &AttestationData, inclusion_delay: u64, @@ -157,7 +166,7 @@ impl BeaconState { let is_matching_head = is_matching_target && data.beacon_block_root == self.block_root_at_slot(data.slot) && payload_matches; - let sqrt_slots = isqrt_u64(SLOTS_PER_EPOCH as u64); + let sqrt_slots = integer_squareroot(SLOTS_PER_EPOCH as u64); let mut out = Vec::new(); if is_matching_source && inclusion_delay <= sqrt_slots { out.push(TIMELY_SOURCE_FLAG_INDEX); @@ -175,7 +184,6 @@ impl BeaconState { /// block is distinct from the prior slot's root, i.e. a fresh block was /// proposed at `data.slot`. Gates the head-flag payload check and the /// per-slot builder-payment weight contribution. - #[must_use] pub fn is_attestation_same_slot(&self, data: &AttestationData) -> bool { if data.slot.as_u64() == 0 { return true; @@ -193,7 +201,6 @@ impl BeaconState { /// in its own slot. It does not write fork-choice latest messages. /// [`Store::on_attestation()`](crate::fork_choice::Store::on_attestation) /// handles that in the local store. - /// Spec: `process_attestation` pub fn process_attestation( &mut self, attestation: &Attestation, @@ -208,7 +215,7 @@ impl BeaconState { /// This rejects invalid timing, payload-branch indices, committee bits, and /// signatures before returning the participation flags and builder-payment /// slot used by the mutation phase. - fn accept_attestation( + pub fn accept_attestation( &self, attestation: &Attestation, ) -> Result { @@ -221,7 +228,14 @@ impl BeaconState { if data.target.epoch != data.slot.epoch() { return Err(OperationError::AttestationTargetEpochInvalid.into()); } - if data.slot.as_u64() + MIN_ATTESTATION_INCLUSION_DELAY > self.slot.as_u64() { + let earliest_inclusion_slot = data + .slot + .as_u64() + .checked_add(MIN_ATTESTATION_INCLUSION_DELAY) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Slot, + ))?; + if earliest_inclusion_slot > self.slot.as_u64() { return Err(OperationError::AttestationSlotInvalid(data.slot).into()); } if data.index.as_u64() >= 2 { @@ -238,11 +252,12 @@ impl BeaconState { if attesting_indices.is_empty() { return Err(OperationError::AttestationParticipantsEmpty.into()); } - let indexed = indexed_attestation_from_known_indices(attestation, &attesting_indices); + let indexed = indexed_attestation_from_known_indices(attestation, &attesting_indices)?; self.validate_indexed_attestation(&indexed, SignatureError::Attestation)?; let inclusion_delay = self.slot.as_u64().saturating_sub(data.slot.as_u64()); - let participation_flags = self.participation_flags_for(data, inclusion_delay)?; + let participation_flags = + self.get_attestation_participation_flag_indices(data, inclusion_delay)?; let is_same_slot = self.is_attestation_same_slot(data); let builder_payment_index = self .builder_payment_index_for_slot(data.slot) @@ -260,7 +275,7 @@ impl BeaconState { /// /// Same-slot beacon attestations also add effective balance to the pending /// builder payment. Payload attestations do not mutate that payment weight. - fn record_attestation_participation( + pub fn record_attestation_participation( &mut self, accepted: &AcceptedAttestation, ) -> Result { @@ -279,14 +294,26 @@ impl BeaconState { } else { &mut self.previous_epoch_participation }; + let participation_index = vi.as_usize(); + let missing_participation = if accepted.target_is_current_epoch { + StateTransitionInvariant::MissingCurrentEpochParticipation(*vi) + } else { + StateTransitionInvariant::MissingPreviousEpochParticipation(*vi) + }; let slot_flags = participation - .get(vi.as_usize()) - .copied() - .unwrap_or_default(); + .get_mut(participation_index) + .ok_or(missing_participation)?; if !slot_flags.has_flag(flag_index)? { - participation[vi.as_usize()] = slot_flags.with_flag(flag_index)?; + *slot_flags = slot_flags.with_flag(flag_index)?; + let reward_delta = + base.checked_mul(*weight) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Weight, + ))?; proposer_reward_numerator = - proposer_reward_numerator.saturating_add(base.saturating_mul(*weight)); + proposer_reward_numerator.checked_add(reward_delta).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Weight), + )?; will_set_new_flag = true; } } @@ -305,13 +332,15 @@ impl BeaconState { /// attestation participation for the slot. Payload attestation objects vote /// on timeliness and data availability. They do not directly add this /// payment weight. - fn record_builder_payment_weight_from_same_slot_attestation( + pub fn record_builder_payment_weight_from_same_slot_attestation( &self, payment: &mut BuilderPendingPayment, validator_index: ValidatorIndex, ) -> Result<(), TransitionError> { let effective_balance = self.validator(validator_index)?.effective_balance; - payment.weight = payment.weight.saturating_add(effective_balance); + payment.weight = payment.weight.checked_add(effective_balance).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Weight), + )?; Ok(()) } @@ -320,7 +349,7 @@ impl BeaconState { /// The numerator is accumulated only when a validator gains a new /// participation flag, so duplicate attestations do not keep paying the /// proposer. - fn reward_attestation_proposer( + pub fn reward_attestation_proposer( &mut self, proposer_reward_numerator: u64, ) -> Result<(), TransitionError> { @@ -337,23 +366,25 @@ impl BeaconState { /// /// Attestation processing uses this to compare a vote's claimed head and /// target against the state history. - pub(crate) fn block_root_at_slot(&self, slot: Slot) -> Root { + pub fn block_root_at_slot(&self, slot: Slot) -> Root { self.block_roots[slot % SLOTS_PER_HISTORICAL_ROOT] } } /// Build an indexed attestation from already decoded participant indices. -fn indexed_attestation_from_known_indices( +pub fn indexed_attestation_from_known_indices( attestation: &Attestation, attesting_indices: &[ValidatorIndex], -) -> IndexedAttestation { - let mut indexed_indices = ssz_rs::List::::default(); +) -> Result { + let mut indexed_indices = List::::default(); for vi in attesting_indices.iter().copied() { - indexed_indices.push(vi); + indexed_indices.push(vi).map_err(|_| { + TransitionError::BoundedListFull(BoundedList::IndexedAttestationIndices) + })?; } - IndexedAttestation { + Ok(IndexedAttestation { attesting_indices: indexed_indices, data: attestation.data, signature: attestation.signature, - } + }) } diff --git a/moonglass/src/state_transition/operations/deposits.rs b/moonglass-core/src/state_transition/operations/deposits.rs similarity index 53% rename from moonglass/src/state_transition/operations/deposits.rs rename to moonglass-core/src/state_transition/operations/deposits.rs index 1bbcd5c..eecdc72 100644 --- a/moonglass/src/state_transition/operations/deposits.rs +++ b/moonglass-core/src/state_transition/operations/deposits.rs @@ -8,54 +8,25 @@ //! arrive separately and register a new builder or top up an existing one after a //! signature check under the builder-deposit domain. -use sha2::{Digest, Sha256}; -use ssz_rs::prelude::*; - use crate::constants::{ - COMPOUNDING_WITHDRAWAL_PREFIX, DEPOSIT_CONTRACT_TREE_DEPTH, DOMAIN_BUILDER_DEPOSIT, - DOMAIN_DEPOSIT, EFFECTIVE_BALANCE_INCREMENT, FAR_FUTURE_EPOCH, GENESIS_FORK_VERSION, - GENESIS_SLOT, MAX_EFFECTIVE_BALANCE, MIN_ACTIVATION_BALANCE, MIN_BUILDER_WITHDRAWABILITY_DELAY, + BUILDER_REGISTRY_LIMIT, COMPOUNDING_WITHDRAWAL_PREFIX, DOMAIN_BUILDER_DEPOSIT, DOMAIN_DEPOSIT, + EFFECTIVE_BALANCE_INCREMENT, FAR_FUTURE_EPOCH, GENESIS_FORK_VERSION, MAX_EFFECTIVE_BALANCE, + MIN_ACTIVATION_BALANCE, MIN_BUILDER_WITHDRAWABILITY_DELAY, VALIDATOR_REGISTRY_LIMIT, }; -use crate::containers::{ - BeaconState, Builder, BuilderDepositRequest, Deposit, PendingDeposit, Validator, +use crate::containers::{BeaconState, Builder, BuilderDepositRequest, Validator}; +use crate::error::{ + BoundedList, MerkleError, SignatureError, TransitionArithmetic, TransitionError, }; -use crate::error::{MerkleError, OperationError, SignatureError, TransitionError}; -use crate::primitives::{BLSPubkey, BLSSignature, Bytes32, Gwei, ParticipationFlags, Root}; -use crate::state_transition::{ - TreeRootExt, compute_domain, compute_signing_root, verify_signature, +use crate::primitives::{ + BLSPubkey, BLSSignature, BuilderIndex, Bytes32, Epoch, ExecutionAddress, Gwei, + ParticipationFlags, Root, Slot, }; - -/// SHA-256 Merkle inclusion check against `root`. `leaf` is folded with -/// `branch[i]` per the path bit of `index` for `depth` levels. -#[must_use] -pub fn is_valid_merkle_branch( - leaf: Root, - branch: &[Bytes32], - depth: usize, - index: u64, - root: Root, -) -> bool { - if branch.len() != depth { - return false; - } - let mut value: [u8; 32] = leaf.0; - for (i, sibling) in branch.iter().enumerate() { - let mut hasher = Sha256::new(); - if (index >> i) & 1 == 1 { - hasher.update(sibling); - hasher.update(value); - } else { - hasher.update(value); - hasher.update(sibling); - } - value = hasher.finalize().into(); - } - value == root.0 -} +use crate::ssz::prelude::*; +use crate::state_transition::{compute_domain, compute_signing_root, verify_signature}; /// SSZ container used to compute the deposit signing root. -#[derive(Default, Clone, PartialEq, Eq, SimpleSerialize)] -struct DepositMessage { +#[derive(Default, Clone, PartialEq, Eq)] +pub struct DepositMessage { /// Depositing validator public key. pub pubkey: BLSPubkey, /// Withdrawal credentials committed by the deposit. @@ -77,6 +48,28 @@ impl BeaconState { withdrawal_credentials: Bytes32, amount: Gwei, ) -> Result<(), TransitionError> { + if self.validators.len() >= VALIDATOR_REGISTRY_LIMIT { + return Err(TransitionError::BoundedListFull(BoundedList::Validators)); + } + if self.balances.len() >= VALIDATOR_REGISTRY_LIMIT { + return Err(TransitionError::BoundedListFull(BoundedList::Balances)); + } + if self.previous_epoch_participation.len() >= VALIDATOR_REGISTRY_LIMIT { + return Err(TransitionError::BoundedListFull( + BoundedList::PreviousEpochParticipation, + )); + } + if self.current_epoch_participation.len() >= VALIDATOR_REGISTRY_LIMIT { + return Err(TransitionError::BoundedListFull( + BoundedList::CurrentEpochParticipation, + )); + } + if self.inactivity_scores.len() >= VALIDATOR_REGISTRY_LIMIT { + return Err(TransitionError::BoundedListFull( + BoundedList::InactivityScores, + )); + } + let compounding = withdrawal_credentials[0] == COMPOUNDING_WITHDRAWAL_PREFIX; let max = if compounding { MAX_EFFECTIVE_BALANCE @@ -95,13 +88,25 @@ impl BeaconState { exit_epoch: FAR_FUTURE_EPOCH, withdrawable_epoch: FAR_FUTURE_EPOCH, }; - self.validators.push(validator); - self.balances.push(amount); + self.validators + .push(validator) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::Validators))?; + self.balances + .push(amount) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::Balances))?; self.previous_epoch_participation - .push(ParticipationFlags::NONE); + .push(ParticipationFlags::NONE) + .map_err(|_| { + TransitionError::BoundedListFull(BoundedList::PreviousEpochParticipation) + })?; self.current_epoch_participation - .push(ParticipationFlags::NONE); - self.inactivity_scores.push(0); + .push(ParticipationFlags::NONE) + .map_err(|_| { + TransitionError::BoundedListFull(BoundedList::CurrentEpochParticipation) + })?; + self.inactivity_scores + .push(0) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::InactivityScores))?; Ok(()) } @@ -110,41 +115,43 @@ impl BeaconState { /// Reuses the lowest index of an exited builder whose balance is fully /// drained, otherwise appends at the end. This keeps builder indices stable /// while making emptied slots reusable. - #[must_use] - pub fn index_for_new_builder(&self) -> usize { + pub fn get_index_for_new_builder(&self) -> BuilderIndex { let current_epoch = self.slot.epoch(); for (i, builder) in self.builders.iter().enumerate() { if builder.withdrawable_epoch <= current_epoch && builder.balance == Gwei::ZERO { - return i; + return BuilderIndex(i as u64); } } - self.builders.len() + BuilderIndex(self.builders.len() as u64) } - /// Insert (or reassign at an exited slot) a builder record. Mirrors the - /// spec `add_builder_to_registry` plus the `set_or_append_list` semantics. + /// Insert a builder record or reassign an exited slot. pub fn add_builder_to_registry( &mut self, pubkey: BLSPubkey, - withdrawal_credentials: Bytes32, + version: u8, + execution_address: ExecutionAddress, amount: Gwei, + slot: Slot, ) -> Result<(), TransitionError> { - let mut execution_address = [0u8; 20]; - execution_address.copy_from_slice(&withdrawal_credentials[12..]); - let deposit_epoch = self.slot.epoch(); let builder = Builder { pubkey, - version: withdrawal_credentials[0], - execution_address: crate::primitives::ExecutionAddress(execution_address), + version, + execution_address, balance: amount, - deposit_epoch, + deposit_epoch: slot.epoch(), withdrawable_epoch: FAR_FUTURE_EPOCH, }; - let idx = self.index_for_new_builder(); + let idx = self.get_index_for_new_builder().as_usize(); if idx < self.builders.len() { self.builders[idx] = builder; } else { - self.builders.push(builder); + if self.builders.len() >= BUILDER_REGISTRY_LIMIT { + return Err(TransitionError::BoundedListFull(BoundedList::Builders)); + } + self.builders + .push(builder) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::Builders))?; } Ok(()) } @@ -155,22 +162,27 @@ impl BeaconState { /// its signature verifies under the builder-deposit domain. A deposit for an /// existing builder tops up its balance, and if that builder had already /// started exiting, pushes its withdrawable epoch back out so the new stake is - /// not paid out immediately. Spec: `process_builder_deposit_request`. + /// not paid out immediately. pub fn process_builder_deposit_request( &mut self, request: &BuilderDepositRequest, ) -> Result<(), TransitionError> { - match self + let existing = self .builders + .as_slice() .iter() - .position(|b| b.pubkey == request.pubkey) - { + .position(|b| b.pubkey == request.pubkey); + match existing { None => { if Self::is_valid_builder_deposit_signature(request)? { + let mut execution_address = [0u8; 20]; + execution_address.copy_from_slice(&request.withdrawal_credentials[12..]); self.add_builder_to_registry( request.pubkey, - request.withdrawal_credentials, + request.withdrawal_credentials[0], + ExecutionAddress(execution_address), request.amount, + self.slot, )?; } } @@ -182,8 +194,13 @@ impl BeaconState { .checked_add(request.amount) .ok_or(TransitionError::BalanceOverflow)?; if builder.withdrawable_epoch != FAR_FUTURE_EPOCH { - builder.withdrawable_epoch = - current_epoch.saturating_add(MIN_BUILDER_WITHDRAWABILITY_DELAY); + builder.withdrawable_epoch = current_epoch + .as_u64() + .checked_add(MIN_BUILDER_WITHDRAWABILITY_DELAY) + .map(Epoch) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Epoch, + ))?; } } } @@ -194,8 +211,7 @@ impl BeaconState { /// /// Mirrors validator deposit verification but with the builder domain, so a /// validator deposit signature cannot be replayed as a builder deposit. - /// Spec: `is_valid_builder_deposit_signature`. - fn is_valid_builder_deposit_signature( + pub fn is_valid_builder_deposit_signature( request: &BuilderDepositRequest, ) -> Result { let domain = compute_domain( @@ -203,12 +219,12 @@ impl BeaconState { GENESIS_FORK_VERSION, Root::default(), )?; - let mut msg = DepositMessage { + let msg = DepositMessage { pubkey: request.pubkey, withdrawal_credentials: request.withdrawal_credentials, amount: request.amount, }; - let signing_root = compute_signing_root(&mut msg, domain, MerkleError::DepositMessage)?; + let signing_root = compute_signing_root(&msg, domain, MerkleError::DepositMessage)?; match verify_signature( &request.pubkey, signing_root, @@ -221,68 +237,6 @@ impl BeaconState { } } - /// Apply an Eth1-bridge deposit. For a never-seen pubkey, validate the - /// `proof-of-possession` signature: a valid `PoP` eagerly adds the - /// validator to the registry with zero effective balance, and an invalid - /// `PoP` drops the deposit. The deposit payload is then queued onto - /// `pending_deposits` with `slot = GENESIS_SLOT` to distinguish bridge - /// deposits from EL deposit requests when the queue is drained during - /// epoch processing. - /// Spec: `apply_deposit`. - pub fn apply_deposit( - &mut self, - pubkey: BLSPubkey, - withdrawal_credentials: Bytes32, - amount: Gwei, - signature: BLSSignature, - ) -> Result<(), TransitionError> { - let is_new = !self.validators.iter().any(|v| v.pubkey == pubkey); - if is_new { - if !self.is_valid_deposit_signature( - &pubkey, - withdrawal_credentials, - amount, - &signature, - )? { - return Ok(()); - } - self.add_validator_to_registry(pubkey, withdrawal_credentials, Gwei::ZERO)?; - } - self.pending_deposits.push(PendingDeposit { - pubkey, - withdrawal_credentials, - amount, - signature, - slot: GENESIS_SLOT, - }); - Ok(()) - } - - /// Validate the deposit's Merkle inclusion proof, bump the deposit cursor, - /// and queue the payload via [`BeaconState::apply_deposit`]. - /// Spec: `process_deposit` - pub fn process_deposit(&mut self, deposit: &Deposit) -> Result<(), TransitionError> { - let mut deposit_data = deposit.data; - let leaf = deposit_data.tree_root(MerkleError::DepositMessage)?; - let branch: Vec = deposit.proof.iter().copied().collect(); - if !is_valid_merkle_branch( - leaf, - &branch, - DEPOSIT_CONTRACT_TREE_DEPTH + 1, - self.eth1_deposit_index, - self.eth1_data.deposit_root, - ) { - return Err(OperationError::DepositMerkleInvalid.into()); - } - self.eth1_deposit_index = self.eth1_deposit_index.saturating_add(1); - self.apply_deposit( - deposit.data.pubkey, - deposit.data.withdrawal_credentials, - deposit.data.amount, - deposit.data.signature, - ) - } - /// True when the deposit's BLS signature verifies as a proof-of-possession /// under the genesis fork-version deposit domain. Distinguishes signature /// failures (returns `Ok(false)`) from internal merkleization or domain @@ -306,19 +260,84 @@ impl BeaconState { /// The genesis-validators-root is intentionally fixed at the all-zero root /// so the same signed deposit is valid across forks. State-bound roots /// would partition the deposit message space per network. - fn verify_deposit_signature( + pub fn verify_deposit_signature( pubkey: &BLSPubkey, withdrawal_credentials: Bytes32, amount: Gwei, signature: &BLSSignature, ) -> Result<(), TransitionError> { let domain = compute_domain(DOMAIN_DEPOSIT, GENESIS_FORK_VERSION, Root::default())?; - let mut msg = DepositMessage { + let msg = DepositMessage { pubkey: *pubkey, withdrawal_credentials, amount, }; - let signing_root = compute_signing_root(&mut msg, domain, MerkleError::DepositMessage)?; + let signing_root = compute_signing_root(&msg, domain, MerkleError::DepositMessage)?; verify_signature(pubkey, signing_root, signature, SignatureError::Deposit) } } + +impl SszSized for DepositMessage { + fn is_variable_size() -> bool { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_is_variable_size(&fields) + } + + fn size_hint() -> usize { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + container_size_hint(&fields) + } +} + +impl Serialize for DepositMessage { + fn serialize(&self, buffer: &mut Vec) -> Result { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.pubkey)?; + encoder.write_field(&self.withdrawal_credentials)?; + encoder.write_field(&self.amount)?; + + encoder.finish(buffer) + } +} + +impl Deserialize for DepositMessage { + fn deserialize(encoding: &[u8]) -> Result { + let fields = [ + field_layout::(), + field_layout::(), + field_layout::(), + ]; + let mut decoder = ContainerDecoder::new(encoding, &fields)?; + Ok(Self { + pubkey: decoder.deserialize_next::()?, + withdrawal_credentials: decoder.deserialize_next::()?, + amount: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for DepositMessage { + fn hash_tree_root(&self) -> Result { + let roots = [ + Merkleized::hash_tree_root(&self.pubkey)?, + Merkleized::hash_tree_root(&self.withdrawal_credentials)?, + Merkleized::hash_tree_root(&self.amount)?, + ]; + + Ok(merkleize_roots(&roots)) + } +} + +impl SimpleSerialize for DepositMessage { + fn is_composite_type() -> bool { + true + } +} diff --git a/moonglass/src/state_transition/operations/exits.rs b/moonglass-core/src/state_transition/operations/exits.rs similarity index 91% rename from moonglass/src/state_transition/operations/exits.rs rename to moonglass-core/src/state_transition/operations/exits.rs index 23bbcbd..fe205a7 100644 --- a/moonglass/src/state_transition/operations/exits.rs +++ b/moonglass-core/src/state_transition/operations/exits.rs @@ -27,7 +27,6 @@ impl BeaconState { /// queued pending withdrawal, and a valid signature. A builder-flagged index /// is out of range for the validator registry and is rejected, since builders /// leave through their own exit request, not a voluntary exit. - /// Spec: `process_voluntary_exit` pub fn process_voluntary_exit( &mut self, signed_exit: &SignedVoluntaryExit, @@ -49,14 +48,14 @@ impl BeaconState { CAPELLA_FORK_VERSION, self.genesis_validators_root, )?; - let mut exit_msg = *exit; - let signing_root = compute_signing_root(&mut exit_msg, domain, MerkleError::VoluntaryExit)?; + let exit_msg = *exit; + let signing_root = compute_signing_root(&exit_msg, domain, MerkleError::VoluntaryExit)?; let validator = self.validator(exit.validator_index)?; let pubkey = validator.pubkey; let activation_epoch = validator.activation_epoch; let already_exiting = validator.exit_epoch != FAR_FUTURE_EPOCH; - let active = validator.is_active_at(current); + let active = validator.is_active_validator(current); if !active { return Err(OperationError::ValidatorNotActive(exit.validator_index).into()); @@ -73,7 +72,7 @@ impl BeaconState { } .into()); } - if self.pending_balance_to_withdraw(exit.validator_index) != Gwei::ZERO { + if self.get_pending_balance_to_withdraw(exit.validator_index)? != Gwei::ZERO { return Err(OperationError::ValidatorHasPendingWithdrawal(exit.validator_index).into()); } @@ -93,7 +92,6 @@ impl BeaconState { /// The operation proves ownership of the old BLS withdrawal key, then writes /// the new execution-address credential into the validator record. It does /// not move balance. It only changes where future withdrawals may go. - /// Spec: `process_bls_to_execution_change` pub fn process_bls_to_execution_change( &mut self, signed_change: &SignedBLSToExecutionChange, @@ -115,9 +113,9 @@ impl BeaconState { GENESIS_FORK_VERSION, self.genesis_validators_root, )?; - let mut change_msg = *change; + let change_msg = *change; let signing_root = - compute_signing_root(&mut change_msg, domain, MerkleError::BlsToExecutionChange)?; + compute_signing_root(&change_msg, domain, MerkleError::BlsToExecutionChange)?; verify_signature( &change.from_bls_pubkey, signing_root, diff --git a/moonglass/src/state_transition/operations/requests.rs b/moonglass-core/src/state_transition/operations/requests.rs similarity index 72% rename from moonglass/src/state_transition/operations/requests.rs rename to moonglass-core/src/state_transition/operations/requests.rs index 157fd1c..c4f932a 100644 --- a/moonglass/src/state_transition/operations/requests.rs +++ b/moonglass-core/src/state_transition/operations/requests.rs @@ -3,33 +3,33 @@ //! These operations are not arbitrary block-body messages from the current //! proposer. They are requests delivered by the execution payload whose request //! root was committed by the parent bid. The child block proves that root in -//! [`BeaconState::accept_parent_payload_commitment`](crate::containers::BeaconState::accept_parent_payload_commitment) +//! [`BeaconState::process_parent_execution_payload`](crate::containers::BeaconState::process_parent_execution_payload) //! before these handlers mutate deposit, withdrawal, and consolidation queues. use crate::constants::{ FAR_FUTURE_EPOCH, FULL_EXIT_REQUEST_AMOUNT, MIN_ACTIVATION_BALANCE, - MIN_VALIDATOR_WITHDRAWABILITY_DELAY, PENDING_CONSOLIDATIONS_LIMIT, + MIN_VALIDATOR_WITHDRAWABILITY_DELAY, PENDING_CONSOLIDATIONS_LIMIT, PENDING_DEPOSITS_LIMIT, PENDING_PARTIAL_WITHDRAWALS_LIMIT, SHARD_COMMITTEE_PERIOD, UNSET_DEPOSIT_REQUESTS_START_INDEX, }; use crate::containers::{ BeaconState, BuilderExitRequest, ConsolidationRequest, DepositRequest, PendingConsolidation, PendingDeposit, PendingPartialWithdrawal, WithdrawalRequest, }; -use crate::error::TransitionError; -use crate::primitives::Gwei; +use crate::error::{ + BoundedList, OperationError, StateTransitionInvariant, TransitionArithmetic, TransitionError, +}; +use crate::primitives::{Epoch, Gwei}; use crate::state_transition::BeaconStateLookup; impl BeaconState { /// Queue an execution-layer deposit request onto `pending_deposits`. /// /// The request queues with `slot = state.slot` so `process_pending_deposits` - /// can tell it apart from Eth1-bridge deposits, which queue with - /// `slot = GENESIS_SLOT` via [`BeaconState::apply_deposit`]. The first request - /// also fixes `deposit_requests_start_index`, the point where these requests - /// take over from the legacy Eth1 deposit queue. Builder deposits do not flow - /// through here. They arrive as their own request type, handled by + /// can process it under the execution-request path. The first request also + /// fixes `deposit_requests_start_index`, the point where these requests take + /// over from the legacy deposit queue. Builder deposits do not flow through + /// here. They arrive as their own request type, handled by /// [`process_builder_deposit_request`](BeaconState::process_builder_deposit_request). - /// Spec: `process_deposit_request` pub fn process_deposit_request( &mut self, request: &DepositRequest, @@ -37,13 +37,20 @@ impl BeaconState { if self.deposit_requests_start_index == UNSET_DEPOSIT_REQUESTS_START_INDEX { self.deposit_requests_start_index = request.index; } - self.pending_deposits.push(PendingDeposit { - pubkey: request.pubkey, - withdrawal_credentials: request.withdrawal_credentials, - amount: request.amount, - signature: request.signature, - slot: self.slot, - }); + if self.pending_deposits.len() >= PENDING_DEPOSITS_LIMIT { + return Err(TransitionError::BoundedListFull( + BoundedList::PendingDeposits, + )); + } + self.pending_deposits + .push(PendingDeposit { + pubkey: request.pubkey, + withdrawal_credentials: request.withdrawal_credentials, + amount: request.amount, + signature: request.signature, + slot: self.slot, + }) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::PendingDeposits))?; Ok(()) } @@ -57,7 +64,6 @@ impl BeaconState { /// balance over `MIN_ACTIVATION_BALANCE` net of already-queued partials. /// The actual amount queued consumes exit churn and is clamped to that /// excess. - /// Spec: `process_withdrawal_request` pub fn process_withdrawal_request( &mut self, request: &WithdrawalRequest, @@ -82,7 +88,7 @@ impl BeaconState { if creds[12..] != request.source_address.0[..] { return Ok(()); } - if !validator.is_active_at(self.slot.epoch()) { + if !validator.is_active_validator(self.slot.epoch()) { return Ok(()); } if validator.exit_epoch != FAR_FUTURE_EPOCH { @@ -97,7 +103,7 @@ impl BeaconState { return Ok(()); } - let pending = self.pending_balance_to_withdraw(validator_index); + let pending = self.get_pending_balance_to_withdraw(validator_index)?; if is_full_exit_request { if pending != Gwei::ZERO { @@ -113,24 +119,35 @@ impl BeaconState { return Ok(()); } let effective_balance = validator.effective_balance; - let balance = self.balances[validator_index.as_usize()]; + let balance = *self + .balances + .get(validator_index.as_usize()) + .ok_or(StateTransitionInvariant::MissingBalance(validator_index))?; let has_sufficient_effective_balance = effective_balance >= MIN_ACTIVATION_BALANCE; - let floor = MIN_ACTIVATION_BALANCE.saturating_add(pending); + let floor = MIN_ACTIVATION_BALANCE.checked_add(pending).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::BalanceSum), + )?; let has_excess_balance = balance > floor; if !has_sufficient_effective_balance || !has_excess_balance { return Ok(()); } let max_excess = balance.as_u64() - floor.as_u64(); let to_withdraw = Gwei(max_excess.min(request.amount.as_u64())); - let exit_queue_epoch = self.consume_exit_churn(to_withdraw); - let withdrawable_epoch = - exit_queue_epoch.saturating_add(MIN_VALIDATOR_WITHDRAWABILITY_DELAY); + let exit_queue_epoch = self.compute_exit_epoch_and_update_churn(to_withdraw)?; + let withdrawable_epoch = exit_queue_epoch + .as_u64() + .checked_add(MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + .map(Epoch) + .ok_or(OperationError::WithdrawableEpochOverflow(validator_index))?; self.pending_partial_withdrawals .push(PendingPartialWithdrawal { validator_index, amount: to_withdraw, withdrawable_epoch, - }); + }) + .map_err(|_| { + TransitionError::BoundedListFull(BoundedList::PendingPartialWithdrawals) + })?; Ok(()) } @@ -138,7 +155,6 @@ impl BeaconState { /// the switch-to-compounding helper. Otherwise validates the full set of /// preconditions and, if all pass, schedules the source exit via the /// consolidation-churn cursor and appends a pending consolidation. - /// Spec: `process_consolidation_request` pub fn process_consolidation_request( &mut self, request: &ConsolidationRequest, @@ -158,7 +174,7 @@ impl BeaconState { if self.pending_consolidations.len() == PENDING_CONSOLIDATIONS_LIMIT { return Ok(()); } - if self.consolidation_churn_limit() <= MIN_ACTIVATION_BALANCE { + if self.get_consolidation_churn_limit()? <= MIN_ACTIVATION_BALANCE { return Ok(()); } @@ -180,7 +196,8 @@ impl BeaconState { return Ok(()); } let current_epoch = self.slot.epoch(); - if !source.is_active_at(current_epoch) || !target.is_active_at(current_epoch) { + if !source.is_active_validator(current_epoch) || !target.is_active_validator(current_epoch) + { return Ok(()); } if source.exit_epoch != FAR_FUTURE_EPOCH || target.exit_epoch != FAR_FUTURE_EPOCH { @@ -193,21 +210,28 @@ impl BeaconState { { return Ok(()); } - if self.pending_balance_to_withdraw(source_index) > Gwei::ZERO { + if self.get_pending_balance_to_withdraw(source_index)? > Gwei::ZERO { return Ok(()); } let source_effective_balance = source.effective_balance; - let exit_epoch = self.consume_consolidation_churn(source_effective_balance); - let withdrawable_epoch = exit_epoch.saturating_add(MIN_VALIDATOR_WITHDRAWABILITY_DELAY); + let exit_epoch = + self.compute_consolidation_epoch_and_update_churn(source_effective_balance)?; + let withdrawable_epoch = exit_epoch + .as_u64() + .checked_add(MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + .map(Epoch) + .ok_or(OperationError::WithdrawableEpochOverflow(source_index))?; let v = &mut self.validators[source_index.as_usize()]; v.exit_epoch = exit_epoch; v.withdrawable_epoch = withdrawable_epoch; - self.pending_consolidations.push(PendingConsolidation { - source_index, - target_index, - }); + self.pending_consolidations + .push(PendingConsolidation { + source_index, + target_index, + }) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::PendingConsolidations))?; Ok(()) } @@ -216,7 +240,6 @@ impl BeaconState { /// Initiates the builder's exit only when the request names a known, active /// builder, comes from that builder's own execution address, and the builder /// has no pending withdrawals queued. Anything else is ignored. - /// Spec: `process_builder_exit_request`. pub fn process_builder_exit_request( &mut self, request: &BuilderExitRequest, @@ -230,7 +253,7 @@ impl BeaconState { if self.builder(builder_index)?.execution_address != request.source_address { return Ok(()); } - if self.pending_balance_to_withdraw_for_builder(builder_index) != Gwei::ZERO { + if self.get_pending_balance_to_withdraw_for_builder(builder_index)? != Gwei::ZERO { return Ok(()); } self.initiate_builder_exit(builder_index) @@ -238,7 +261,7 @@ impl BeaconState { /// True when a consolidation request is actually a self-targeted switch to /// compounding withdrawal credentials. - fn is_valid_switch_to_compounding_request(&self, request: &ConsolidationRequest) -> bool { + pub fn is_valid_switch_to_compounding_request(&self, request: &ConsolidationRequest) -> bool { if request.source_pubkey != request.target_pubkey { return false; } @@ -255,6 +278,6 @@ impl BeaconState { return false; } let current_epoch = self.slot.epoch(); - source.is_active_at(current_epoch) && source.exit_epoch == FAR_FUTURE_EPOCH + source.is_active_validator(current_epoch) && source.exit_epoch == FAR_FUTURE_EPOCH } } diff --git a/moonglass/src/state_transition/operations/slashings.rs b/moonglass-core/src/state_transition/operations/slashings.rs similarity index 84% rename from moonglass/src/state_transition/operations/slashings.rs rename to moonglass-core/src/state_transition/operations/slashings.rs index d91aa28..7457019 100644 --- a/moonglass/src/state_transition/operations/slashings.rs +++ b/moonglass-core/src/state_transition/operations/slashings.rs @@ -8,20 +8,19 @@ //! a separate attester-slashing handler that only removes equivocators from //! local latest-message weight. +use std::collections::BTreeSet; + use crate::constants::{DOMAIN_BEACON_PROPOSER, SLOTS_PER_EPOCH}; use crate::containers::{ AttestationData, AttesterSlashing, BeaconState, BuilderPendingPayment, ProposerSlashing, }; use crate::error::{MerkleError, OperationError, SignatureError, TransitionError}; use crate::primitives::ValidatorIndex; -use crate::state_transition::{ - BeaconStateLookup, TreeRootExt, compute_signing_root, verify_signature, -}; +use crate::state_transition::{BeaconStateLookup, compute_signing_root, verify_signature}; impl BeaconState { /// Validate a proposer slashing and apply the slashing mutation. The two /// signed headers must be by the same proposer for the same slot but differ. - /// Spec: `process_proposer_slashing` pub fn process_proposer_slashing( &mut self, slashing: &ProposerSlashing, @@ -34,26 +33,22 @@ impl BeaconState { if h1.proposer_index != h2.proposer_index { return Err(OperationError::ProposerSlashingProposerMismatch.into()); } - let mut h1c = *h1; - let mut h2c = *h2; - let r1 = h1c.tree_root(MerkleError::BeaconBlockHeader)?; - let r2 = h2c.tree_root(MerkleError::BeaconBlockHeader)?; - if r1 == r2 { + if h1 == h2 { return Err(OperationError::ProposerSlashingHeadersMatch.into()); } let proposer = self.validator(h1.proposer_index)?; let current_epoch = self.slot.epoch(); - if !proposer.is_slashable_at(current_epoch) { + if !proposer.is_slashable_validator(current_epoch) { return Err(OperationError::ProposerSlashingNotSlashable(h1.proposer_index).into()); } let pubkey = proposer.pubkey; - let mut h1m = *h1; - let mut h2m = *h2; + let h1m = *h1; + let h2m = *h2; let domain_1 = self.domain_for(DOMAIN_BEACON_PROPOSER, h1.slot.epoch())?; let domain_2 = self.domain_for(DOMAIN_BEACON_PROPOSER, h2.slot.epoch())?; - let sr1 = compute_signing_root(&mut h1m, domain_1, MerkleError::BeaconBlockHeader)?; - let sr2 = compute_signing_root(&mut h2m, domain_2, MerkleError::BeaconBlockHeader)?; + let sr1 = compute_signing_root(&h1m, domain_1, MerkleError::BeaconBlockHeader)?; + let sr2 = compute_signing_root(&h2m, domain_2, MerkleError::BeaconBlockHeader)?; verify_signature( &pubkey, sr1, @@ -96,7 +91,6 @@ impl BeaconState { /// Validate an attester slashing: the two indexed attestations must form /// slashable evidence, overlap on at least one attester, and each verify. - /// Spec: `process_attester_slashing` pub fn process_attester_slashing( &mut self, slashing: &AttesterSlashing, @@ -109,10 +103,8 @@ impl BeaconState { self.validate_indexed_attestation(a1, SignatureError::AttesterSlashingAttestationOne)?; self.validate_indexed_attestation(a2, SignatureError::AttesterSlashingAttestationTwo)?; - let set1: std::collections::BTreeSet = - a1.attesting_indices.iter().map(|i| i.as_u64()).collect(); - let set2: std::collections::BTreeSet = - a2.attesting_indices.iter().map(|i| i.as_u64()).collect(); + let set1: BTreeSet = a1.attesting_indices.iter().map(|i| i.as_u64()).collect(); + let set2: BTreeSet = a2.attesting_indices.iter().map(|i| i.as_u64()).collect(); let intersection: Vec = set1.intersection(&set2).copied().collect(); if intersection.is_empty() { return Err(OperationError::AttesterSlashingNoIntersection.into()); @@ -122,7 +114,7 @@ impl BeaconState { for raw in intersection { let vi = ValidatorIndex(raw); let v = self.validator(vi)?; - if v.is_slashable_at(epoch) { + if v.is_slashable_validator(epoch) { self.slash_validator(vi, None)?; slashed_any = true; } @@ -140,7 +132,7 @@ impl BeaconState { /// wraps one source/target interval around another. Signature and intersection /// checks happen in [`BeaconState::process_attester_slashing`]. This helper /// answers only the Casper FFG shape question. -pub(crate) fn is_slashable_attestation_data(d1: &AttestationData, d2: &AttestationData) -> bool { +pub fn is_slashable_attestation_data(d1: &AttestationData, d2: &AttestationData) -> bool { let double = d1 != d2 && d1.target.epoch == d2.target.epoch; let surround = d1.source.epoch.as_u64() < d2.source.epoch.as_u64() && d2.target.epoch.as_u64() < d1.target.epoch.as_u64(); diff --git a/moonglass/src/state_transition/signing.rs b/moonglass-core/src/state_transition/signing.rs similarity index 83% rename from moonglass/src/state_transition/signing.rs rename to moonglass-core/src/state_transition/signing.rs index 8fa704c..965c4b2 100644 --- a/moonglass/src/state_transition/signing.rs +++ b/moonglass-core/src/state_transition/signing.rs @@ -8,6 +8,7 @@ use crate::containers::{BeaconState, ForkData, SigningData}; use crate::error::{MerkleError, TransitionError}; use crate::primitives::{Domain, DomainType, Epoch, Root, Version}; +use crate::ssz::Merkleized; use crate::state_transition::TreeRootExt; pub use crate::crypto::bls::{aggregate_pubkeys, fast_aggregate_verify, verify_signature}; @@ -43,12 +44,12 @@ pub fn compute_domain( /// This is the tree root of `SigningData(tree_root(object), domain)`, not the /// object's tree root by itself. pub fn compute_signing_root( - object: &mut T, + object: &T, domain: Domain, on_object_fail: MerkleError, ) -> Result where - T: ssz_rs::Merkleized, + T: Merkleized, { let object_root = object.tree_root(on_object_fail)?; SigningData { @@ -60,7 +61,6 @@ where impl BeaconState { /// Signing version active at `epoch` per this state's version record. - #[must_use] pub fn fork_version_at(&self, epoch: Epoch) -> Version { if epoch < self.fork.epoch { self.fork.previous_version @@ -69,12 +69,13 @@ impl BeaconState { } } - /// State-aware signing domain for `domain_type` at `epoch`. - pub fn domain_for( + /// State-aware signing domain for `domain_type` at `epoch`, or now if omitted. + pub fn get_domain( &self, domain_type: DomainType, - epoch: Epoch, + epoch: Option, ) -> Result { + let epoch = epoch.unwrap_or(self.slot.epoch()); compute_domain( domain_type, self.fork_version_at(epoch), @@ -82,18 +83,27 @@ impl BeaconState { ) } + /// State-aware signing domain for callers that pass an explicit epoch. + pub fn domain_for( + &self, + domain_type: DomainType, + epoch: Epoch, + ) -> Result { + self.get_domain(domain_type, Some(epoch)) + } + /// Signing root for `object` under this state's domain at `epoch`. pub fn signing_root_for( &self, - object: &mut T, + object: &T, domain_type: DomainType, epoch: Epoch, on_object_fail: MerkleError, ) -> Result where - T: ssz_rs::Merkleized, + T: Merkleized, { - let domain = self.domain_for(domain_type, epoch)?; + let domain = self.get_domain(domain_type, Some(epoch))?; compute_signing_root(object, domain, on_object_fail) } @@ -104,7 +114,7 @@ impl BeaconState { domain_type: DomainType, epoch: Epoch, ) -> Result { - let domain = self.domain_for(domain_type, epoch)?; + let domain = self.get_domain(domain_type, Some(epoch))?; SigningData { object_root, domain, diff --git a/moonglass/src/state_transition/slot.rs b/moonglass-core/src/state_transition/slot.rs similarity index 82% rename from moonglass/src/state_transition/slot.rs rename to moonglass-core/src/state_transition/slot.rs index 9f48e7e..47ddcc0 100644 --- a/moonglass/src/state_transition/slot.rs +++ b/moonglass-core/src/state_transition/slot.rs @@ -11,12 +11,12 @@ use crate::error::{MerkleError, SlotError, TransitionError}; use crate::primitives::{Root, Slot}; use crate::state_transition::TreeRootExt; -/// Check whether the next slot begins a new epoch. -/// -/// Epoch processing runs while `self.slot` still points at the final slot of the -/// ending epoch, so the transition checks this before incrementing the clock. -fn next_slot_starts_epoch(slot: Slot) -> bool { - (slot.as_u64() + 1).is_multiple_of(SLOTS_PER_EPOCH as u64) +/// Return the next slot or fail when the `uint64` slot space is exhausted. +pub fn checked_next_slot(slot: Slot) -> Result { + slot.as_u64() + .checked_add(1) + .map(Slot::new) + .ok_or_else(|| SlotError::NextSlotOverflow(slot).into()) } impl BeaconState { @@ -29,7 +29,6 @@ impl BeaconState { /// otherwise the call raises [`SlotError::NotAfter`]. This advances the clock /// and the historical buffers only, it does not apply any block, so empty /// slots are filled in before a block at `target_slot` is processed. - /// Spec: `process_slots` pub fn process_slots(&mut self, target_slot: Slot) -> Result<(), TransitionError> { if self.slot >= target_slot { return Err(SlotError::NotAfter { @@ -40,12 +39,13 @@ impl BeaconState { } while self.slot < target_slot { self.process_slot()?; + let next_slot = checked_next_slot(self.slot)?; // Run epoch processing on the last slot of the ending epoch so that // `self.slot` inside `process_epoch` refers to that ending epoch. - if next_slot_starts_epoch(self.slot) { + if next_slot.as_u64().is_multiple_of(SLOTS_PER_EPOCH as u64) { self.process_epoch()?; } - self.slot += 1; + self.slot = next_slot; } Ok(()) } @@ -60,8 +60,8 @@ impl BeaconState { /// cleared so that slot starts out assumed empty until a child block proves /// and applies its parent payload. /// This makes both rings queryable at the slot just completed. - /// Spec: `process_slot` pub fn process_slot(&mut self) -> Result<(), TransitionError> { + let next_slot = checked_next_slot(self.slot)?; let previous_state_root = self.tree_root(MerkleError::BeaconState)?; let index = self.slot % SLOTS_PER_HISTORICAL_ROOT; @@ -79,14 +79,9 @@ impl BeaconState { .tree_root(MerkleError::BeaconBlockHeader)?; self.block_roots[index] = previous_block_root; - self.clear_next_payload_availability(); + let next_index = next_slot % SLOTS_PER_HISTORICAL_ROOT; + self.execution_payload_availability.set(next_index, false); Ok(()) } - - /// Reset the payload-availability bit for the next slot. - fn clear_next_payload_availability(&mut self) { - let next_index = (self.slot + 1) % SLOTS_PER_HISTORICAL_ROOT; - self.execution_payload_availability.set(next_index, false); - } } diff --git a/moonglass/src/state_transition/validator.rs b/moonglass-core/src/state_transition/validator.rs similarity index 61% rename from moonglass/src/state_transition/validator.rs rename to moonglass-core/src/state_transition/validator.rs index fcc3b0a..31de145 100644 --- a/moonglass/src/state_transition/validator.rs +++ b/moonglass-core/src/state_transition/validator.rs @@ -7,56 +7,47 @@ //! withdrawal amount. use crate::constants::{ - BLS_WITHDRAWAL_PREFIX, CHURN_LIMIT_QUOTIENT, COMPOUNDING_WITHDRAWAL_PREFIX, - CONSOLIDATION_CHURN_LIMIT_QUOTIENT, EFFECTIVE_BALANCE_INCREMENT, - ETH1_ADDRESS_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH, MAX_EFFECTIVE_BALANCE, - MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT, MAX_SEED_LOOKAHEAD, MIN_ACTIVATION_BALANCE, - MIN_PER_EPOCH_CHURN_LIMIT, MIN_VALIDATOR_WITHDRAWABILITY_DELAY, + CHURN_LIMIT_QUOTIENT, COMPOUNDING_WITHDRAWAL_PREFIX, CONSOLIDATION_CHURN_LIMIT_QUOTIENT, + EFFECTIVE_BALANCE_INCREMENT, ETH1_ADDRESS_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH, GENESIS_SLOT, + MAX_EFFECTIVE_BALANCE, MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT, MAX_SEED_LOOKAHEAD, + MIN_ACTIVATION_BALANCE, MIN_PER_EPOCH_CHURN_LIMIT, MIN_VALIDATOR_WITHDRAWABILITY_DELAY, + PENDING_DEPOSITS_LIMIT, }; -use crate::containers::{BeaconState, Builder, Validator}; -use crate::error::{RegistryError, TransitionError}; -use crate::primitives::{BLSPubkey, BuilderIndex, Epoch, Gwei, ValidatorIndex}; +use crate::containers::{BeaconState, Builder, PendingDeposit, Validator}; +use crate::error::{ + BoundedList, OperationError, RegistryError, StateTransitionInvariant, TransitionArithmetic, + TransitionError, +}; +use crate::primitives::{BLSPubkey, BLSSignature, BuilderIndex, Epoch, Gwei, ValidatorIndex}; impl Validator { /// True if this validator is in the active set during `epoch`. - #[must_use] - pub fn is_active_at(&self, epoch: Epoch) -> bool { + pub fn is_active_validator(&self, epoch: Epoch) -> bool { self.activation_epoch <= epoch && epoch < self.exit_epoch } /// True if this validator is slashable at `epoch`. - #[must_use] - pub fn is_slashable_at(&self, epoch: Epoch) -> bool { + pub fn is_slashable_validator(&self, epoch: Epoch) -> bool { !self.slashed && self.activation_epoch <= epoch && epoch < self.withdrawable_epoch } /// True if this validator has Eth1-address withdrawal credentials. - #[must_use] pub fn has_eth1_withdrawal_credential(&self) -> bool { self.withdrawal_credentials[0] == ETH1_ADDRESS_WITHDRAWAL_PREFIX } /// True if this validator has compounding withdrawal credentials. - #[must_use] pub fn has_compounding_withdrawal_credential(&self) -> bool { self.withdrawal_credentials[0] == COMPOUNDING_WITHDRAWAL_PREFIX } /// True if this validator has execution-address or compounding credentials. - #[must_use] pub fn has_execution_withdrawal_credential(&self) -> bool { self.has_eth1_withdrawal_credential() || self.has_compounding_withdrawal_credential() } - /// True if this validator still has bare BLS withdrawal credentials. - #[must_use] - pub fn has_bls_withdrawal_credential(&self) -> bool { - self.withdrawal_credentials[0] == BLS_WITHDRAWAL_PREFIX - } - /// True if this validator is fully withdrawable at `epoch`. - #[must_use] - pub fn is_fully_withdrawable_at(&self, balance: Gwei, epoch: Epoch) -> bool { + pub fn is_fully_withdrawable_validator(&self, balance: Gwei, epoch: Epoch) -> bool { self.has_execution_withdrawal_credential() && self.withdrawable_epoch <= epoch && balance.as_u64() > 0 @@ -64,10 +55,8 @@ impl Validator { /// True if this validator has effective-balance excess that can be swept /// as a partial withdrawal. - /// Spec: `is_partially_withdrawable_validator`. - #[must_use] - pub fn is_partially_withdrawable(&self, balance: Gwei) -> bool { - let max = self.max_effective_balance(); + pub fn is_partially_withdrawable_validator(&self, balance: Gwei) -> bool { + let max = self.get_max_effective_balance(); let has_max_effective_balance = self.effective_balance == max; let has_excess_balance = balance > max; self.has_execution_withdrawal_credential() @@ -76,8 +65,7 @@ impl Validator { } /// Effective-balance cap: 2048 ETH if compounding, else 32 ETH. - #[must_use] - pub fn max_effective_balance(&self) -> Gwei { + pub fn get_max_effective_balance(&self) -> Gwei { if self.has_compounding_withdrawal_credential() { MAX_EFFECTIVE_BALANCE } else { @@ -134,88 +122,93 @@ impl BeaconStateLookup for BeaconState { } impl BeaconState { - /// True if `pubkey` already has an entry in the validator registry or in the - /// pending-deposit queue. - #[must_use] - pub fn is_pending_validator(&self, pubkey: &BLSPubkey) -> bool { - self.validators.iter().any(|v| v.pubkey == *pubkey) - || self.pending_deposits.iter().any(|d| d.pubkey == *pubkey) - } - /// Sum of scheduled partial withdrawals queued against `index`. - #[must_use] - pub fn pending_balance_to_withdraw(&self, index: ValidatorIndex) -> Gwei { - self.pending_partial_withdrawals + pub fn get_pending_balance_to_withdraw( + &self, + index: ValidatorIndex, + ) -> Result { + let mut total = Gwei::ZERO; + for withdrawal in self + .pending_partial_withdrawals .iter() .filter(|w| w.validator_index == index) - .map(|w| w.amount) - .fold(Gwei::ZERO, Gwei::saturating_add) + { + total = + total + .checked_add(withdrawal.amount) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BalanceSum, + ))?; + } + Ok(total) } /// Total gwei the chain is willing to move into or out of the active set per epoch, /// before any activation or consolidation specific caps are applied. - #[must_use] - pub fn balance_churn_limit(&self) -> Gwei { - let stake_scaled = Gwei(self.total_active_balance().as_u64() / CHURN_LIMIT_QUOTIENT); + pub fn get_balance_churn_limit(&self) -> Result { + let stake_scaled = Gwei(self.get_total_active_balance()?.as_u64() / CHURN_LIMIT_QUOTIENT); let churn = MIN_PER_EPOCH_CHURN_LIMIT.max(stake_scaled); let remainder = churn.as_u64() % EFFECTIVE_BALANCE_INCREMENT.as_u64(); - Gwei(churn.as_u64() - remainder) + Ok(Gwei(churn.as_u64() - remainder)) } /// Per-epoch churn budget for activations, capped at /// `MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT`. - #[must_use] - pub fn activation_churn_limit(&self) -> Gwei { - MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT.min(self.balance_churn_limit()) + pub fn get_activation_churn_limit(&self) -> Result { + Ok(MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT.min(self.get_balance_churn_limit()?)) } - /// Per-epoch churn budget for exits. Equal to `balance_churn_limit`, with no + /// Per-epoch churn budget for exits. Equal to `get_balance_churn_limit`, with no /// activation cap applied: the exit pipeline is independent of activation. - #[must_use] - pub fn exit_churn_limit(&self) -> Gwei { - self.balance_churn_limit() + pub fn get_exit_churn_limit(&self) -> Result { + self.get_balance_churn_limit() } /// Per-epoch churn budget specifically for consolidations. Derived directly /// from `total_active_balance / CONSOLIDATION_CHURN_LIMIT_QUOTIENT` and /// rounded down to `EFFECTIVE_BALANCE_INCREMENT`. - #[must_use] - pub fn consolidation_churn_limit(&self) -> Gwei { - let raw = self.total_active_balance().as_u64() / CONSOLIDATION_CHURN_LIMIT_QUOTIENT; + pub fn get_consolidation_churn_limit(&self) -> Result { + let raw = self.get_total_active_balance()?.as_u64() / CONSOLIDATION_CHURN_LIMIT_QUOTIENT; let remainder = raw % EFFECTIVE_BALANCE_INCREMENT.as_u64(); - Gwei(raw - remainder) + Ok(Gwei(raw - remainder)) } /// Assign an exit epoch to `exit_balance` worth of departing stake. - pub fn consume_exit_churn(&mut self, exit_balance: Gwei) -> Epoch { + pub fn compute_exit_epoch_and_update_churn( + &mut self, + exit_balance: Gwei, + ) -> Result { let current = self.slot.epoch(); - let per_epoch_churn = self.exit_churn_limit(); + let per_epoch_churn = self.get_exit_churn_limit()?; let (exit_epoch, remaining) = consume_churn_budget( current, self.earliest_exit_epoch, self.exit_balance_to_consume, exit_balance, per_epoch_churn, - ); + )?; self.exit_balance_to_consume = remaining; self.earliest_exit_epoch = exit_epoch; - exit_epoch + Ok(exit_epoch) } /// Assign a consolidation epoch to `consolidation_balance` worth of stake. - pub fn consume_consolidation_churn(&mut self, consolidation_balance: Gwei) -> Epoch { + pub fn compute_consolidation_epoch_and_update_churn( + &mut self, + consolidation_balance: Gwei, + ) -> Result { let current = self.slot.epoch(); - let per_epoch_churn = self.consolidation_churn_limit(); + let per_epoch_churn = self.get_consolidation_churn_limit()?; let (consolidation_epoch, remaining) = consume_churn_budget( current, self.earliest_consolidation_epoch, self.consolidation_balance_to_consume, consolidation_balance, per_epoch_churn, - ); + )?; self.consolidation_balance_to_consume = remaining; self.earliest_consolidation_epoch = consolidation_epoch; - consolidation_epoch + Ok(consolidation_epoch) } /// Schedule `index` to exit the active set. @@ -228,14 +221,12 @@ impl BeaconState { return Ok(()); } let effective_balance = validator.effective_balance; - let exit_epoch = self.consume_exit_churn(effective_balance); + let exit_epoch = self.compute_exit_epoch_and_update_churn(effective_balance)?; let withdrawable = exit_epoch .as_u64() .checked_add(MIN_VALIDATOR_WITHDRAWABILITY_DELAY) - .map(crate::primitives::Epoch) - .ok_or(crate::error::OperationError::WithdrawableEpochOverflow( - index, - ))?; + .map(Epoch) + .ok_or(OperationError::WithdrawableEpochOverflow(index))?; let v = &mut self.validators[index.as_usize()]; v.exit_epoch = exit_epoch; v.withdrawable_epoch = withdrawable; @@ -247,12 +238,8 @@ impl BeaconState { &mut self, index: ValidatorIndex, ) -> Result<(), TransitionError> { - // Existence check: errors if `index` is out of bounds for the validator registry. let _ = self.validator(index)?; let v = &mut self.validators[index.as_usize()]; - if !v.has_eth1_withdrawal_credential() { - return Ok(()); - } v.withdrawal_credentials[0] = COMPOUNDING_WITHDRAWAL_PREFIX; self.queue_excess_active_balance(index)?; Ok(()) @@ -265,40 +252,48 @@ impl BeaconState { &mut self, index: ValidatorIndex, ) -> Result<(), TransitionError> { - let balance = self + let validator = self.validator(index)?; + let balance = *self .balances .get(index.as_usize()) - .copied() - .unwrap_or(Gwei::ZERO); + .ok_or(StateTransitionInvariant::MissingBalance(index))?; if balance <= MIN_ACTIVATION_BALANCE { return Ok(()); } - let excess_balance = balance.saturating_sub(MIN_ACTIVATION_BALANCE); - self.balances[index.as_usize()] = MIN_ACTIVATION_BALANCE; - let validator = self.validator(index)?; + if self.pending_deposits.len() >= PENDING_DEPOSITS_LIMIT { + return Err(TransitionError::BoundedListFull( + BoundedList::PendingDeposits, + )); + } + let excess_balance = balance.checked_sub(MIN_ACTIVATION_BALANCE).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Churn), + )?; let pubkey = validator.pubkey; let withdrawal_credentials = validator.withdrawal_credentials; - // Re-queued excess uses the G2 infinity signature and the genesis - // slot as sentinels so the queue can distinguish it from a fresh - // deposit request. + self.balances[index.as_usize()] = MIN_ACTIVATION_BALANCE; self.pending_deposits - .push(crate::containers::PendingDeposit { + .push(PendingDeposit { pubkey, withdrawal_credentials, amount: excess_balance, - signature: crate::primitives::BLSSignature::G2_POINT_AT_INFINITY, - slot: crate::constants::GENESIS_SLOT, - }); + signature: BLSSignature::G2_POINT_AT_INFINITY, + slot: GENESIS_SLOT, + }) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::PendingDeposits))?; Ok(()) } } -/// Earliest epoch at which a validator activating or exiting now becomes -/// effective. Adds the seed-lookahead buffer so committee shuffling has -/// settled. -#[must_use] -pub fn compute_activation_exit_epoch(epoch: Epoch) -> Epoch { - epoch.saturating_add(1 + MAX_SEED_LOOKAHEAD) +/// Earliest epoch at which a validator activating or exiting now becomes effective. +pub fn compute_activation_exit_epoch(epoch: Epoch) -> Result { + epoch + .as_u64() + .checked_add(1) + .and_then(|epoch| epoch.checked_add(MAX_SEED_LOOKAHEAD)) + .map(Epoch) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Epoch, + )) } /// Spend churn budget for a requested activation, exit, or consolidation amount. @@ -306,14 +301,14 @@ pub fn compute_activation_exit_epoch(epoch: Epoch) -> Epoch { /// Registry updates use this to serialize balance-moving operations across /// future epochs. It returns the epoch assigned to the operation and the /// remaining cursor balance in that epoch after accounting for the request. -fn consume_churn_budget( +pub fn consume_churn_budget( current_epoch: Epoch, cursor_epoch: Epoch, cursor_balance: Gwei, requested_balance: Gwei, per_epoch_churn: Gwei, -) -> (Epoch, Gwei) { - let earliest = cursor_epoch.max(compute_activation_exit_epoch(current_epoch)); +) -> Result<(Epoch, Gwei), TransitionError> { + let earliest = cursor_epoch.max(compute_activation_exit_epoch(current_epoch)?); let mut available = if cursor_epoch < earliest { per_epoch_churn } else { @@ -324,14 +319,40 @@ fn consume_churn_budget( if requested_balance > available { let per_epoch = per_epoch_churn.as_u64(); if per_epoch == 0 { - return (epoch, Gwei::ZERO); + return Err(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Churn, + )); } - let overflow = requested_balance.as_u64() - available.as_u64(); - let additional_epochs = (overflow - 1) / per_epoch + 1; - epoch = epoch.saturating_add(additional_epochs); - let added_budget = additional_epochs.saturating_mul(per_epoch); - available = Gwei(available.as_u64().saturating_add(added_budget)); + let balance_to_process = + requested_balance + .checked_sub(available) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Churn, + ))?; + let additional_epochs = (balance_to_process.as_u64() - 1) / per_epoch + 1; + epoch = epoch + .as_u64() + .checked_add(additional_epochs) + .map(Epoch) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Churn, + ))?; + let added_budget = + additional_epochs + .checked_mul(per_epoch) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Churn, + ))?; + available = Gwei(available.as_u64().checked_add(added_budget).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::Churn), + )?); } - (epoch, available.saturating_sub(requested_balance)) + let remaining = + available + .checked_sub(requested_balance) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::Churn, + ))?; + Ok((epoch, remaining)) } diff --git a/moonglass-core/src/state_transition/withdrawal.rs b/moonglass-core/src/state_transition/withdrawal.rs new file mode 100644 index 0000000..bbc21af --- /dev/null +++ b/moonglass-core/src/state_transition/withdrawal.rs @@ -0,0 +1,582 @@ +//! [Withdrawal sweep](crate::glossary#withdrawal-sweep) transition phases. +//! +//! Computes the per-slot expected withdrawals for queued +//! [builder](crate::glossary#builder) withdrawals first, then pending partial +//! withdrawals, then a builder sweep, then a +//! [validator](crate::glossary#validator) sweep. The result is stored in +//! `state.payload_expected_withdrawals` for the +//! [execution payload](crate::glossary#execution-payload) path to verify. +//! Validator and builder balances move here. + +use crate::ssz::List; + +use crate::constants::{ + BUILDER_PENDING_WITHDRAWALS_LIMIT, FAR_FUTURE_EPOCH, MAX_BUILDERS_PER_WITHDRAWALS_SWEEP, + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP, + MAX_WITHDRAWALS_PER_PAYLOAD, MIN_ACTIVATION_BALANCE, PENDING_PARTIAL_WITHDRAWALS_LIMIT, +}; +use crate::containers::{ + BeaconState, BuilderPendingWithdrawal, PendingPartialWithdrawal, Validator, Withdrawal, +}; +use crate::error::{ + BoundedList, RegistryError, StateTransitionInvariant, TransitionArithmetic, TransitionError, +}; +use crate::primitives::{BuilderIndex, ExecutionAddress, Gwei, ValidatorIndex, WithdrawalIndex}; + +/// Per-sweep accounting that flows from `expected_withdrawals` into the +/// post-state update step. +pub struct ExpectedWithdrawals { + /// Ordered withdrawals the next execution payload must include. + pub withdrawals: Vec, + /// Number of builder pending-withdrawal queue entries consumed. + pub processed_builder_withdrawals_count: u64, + /// Number of pending partial-withdrawal entries consumed. + pub processed_partial_withdrawals_count: u64, + /// Number of builders visited by the builder sweep. + pub processed_builders_sweep_count: u64, + /// Number of validators visited by the validator sweep. + pub processed_sweep_withdrawals_count: u64, +} + +/// Extract the execution withdrawal address from withdrawal credentials. +pub fn withdrawal_address_from_credentials(credentials: &[u8; 32]) -> ExecutionAddress { + let mut address = [0u8; 20]; + address.copy_from_slice(&credentials[12..]); + ExecutionAddress(address) +} + +/// Advance a withdrawal sequence index by one. +pub fn increment_withdrawal_index( + index: WithdrawalIndex, +) -> Result { + index + .as_u64() + .checked_add(1) + .map(WithdrawalIndex) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::WithdrawalIndex, + )) +} + +/// Advance a processed-entry counter by one. +pub fn increment_processed_count(count: u64) -> Result { + count + .checked_add(1) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + )) +} + +/// Convert a builder index into a host collection index. +pub fn builder_index_as_usize(index: BuilderIndex) -> Result { + usize::try_from(index.as_u64()) + .map_err(|_| RegistryError::BuilderIndexOutOfRange(index.as_u64()).into()) +} + +/// Convert a validator index into a host collection index. +pub fn validator_index_as_usize(index: ValidatorIndex) -> Result { + usize::try_from(index.as_u64()) + .map_err(|_| RegistryError::ValidatorIndexOutOfRange(index.as_u64()).into()) +} + +/// Convert a host collection index into a protocol index value. +pub fn cursor_as_u64(cursor: usize) -> Result { + u64::try_from(cursor) + .map_err(|_| TransitionError::ArithmeticOverflow(TransitionArithmetic::BoundedListLength)) +} + +/// Convert a host collection length into a protocol value. +pub fn len_as_u64(len: usize) -> Result { + u64::try_from(len) + .map_err(|_| TransitionError::ArithmeticOverflow(TransitionArithmetic::BoundedListLength)) +} + +/// Convert a protocol sweep limit into a host loop bound. +pub fn sweep_limit(limit: u64, len: usize) -> Result { + Ok(usize::try_from(limit) + .map_err(|_| TransitionError::ArithmeticOverflow(TransitionArithmetic::BoundedListLength))? + .min(len)) +} + +/// Length of `prior_withdrawals + withdrawals`. +pub fn combined_withdrawals_len( + prior_withdrawals: &[Withdrawal], + withdrawals: &[Withdrawal], +) -> Result { + prior_withdrawals + .len() + .checked_add(withdrawals.len()) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + )) +} + +impl BeaconState { + /// Encode a builder index into the withdrawal path's validator index space. + pub fn convert_builder_index_to_validator_index( + builder_index: BuilderIndex, + ) -> Result { + Ok(builder_index.to_validator_index()?) + } + + /// Balance after already selected withdrawals for `validator_index`. + pub fn get_balance_after_withdrawals( + &self, + validator_index: ValidatorIndex, + withdrawals: &[Withdrawal], + ) -> Result { + let starting = *self + .balances + .get(validator_index_as_usize(validator_index)?) + .ok_or(StateTransitionInvariant::MissingBalance(validator_index))?; + let mut withdrawn = Gwei::ZERO; + for withdrawal in withdrawals + .iter() + .filter(|w| w.validator_index == validator_index) + { + withdrawn = withdrawn.checked_add(withdrawal.amount).ok_or( + TransitionError::ArithmeticOverflow(TransitionArithmetic::BalanceSum), + )?; + } + Ok(starting.saturating_sub(withdrawn)) + } + + /// Return whether `validator` can make a pending partial withdrawal. + pub fn is_eligible_for_partial_withdrawals( + &self, + validator: &Validator, + balance: Gwei, + ) -> bool { + validator.exit_epoch == FAR_FUTURE_EPOCH + && validator.effective_balance >= MIN_ACTIVATION_BALANCE + && balance > MIN_ACTIVATION_BALANCE + } + + /// Build withdrawals from the explicit builder pending-withdrawal queue. + /// + /// These are emitted before partial withdrawals and sweeps, capped at one + /// less than the payload limit so later sweep phases can still contribute. + pub fn get_builder_withdrawals( + &self, + mut withdrawal_index: WithdrawalIndex, + prior_withdrawals: &[Withdrawal], + ) -> Result<(Vec, WithdrawalIndex, u64), TransitionError> { + let withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD - 1; + if prior_withdrawals.len() > withdrawals_limit { + return Err(TransitionError::BoundedListFull( + BoundedList::PayloadExpectedWithdrawals, + )); + } + let mut withdrawals: Vec = Vec::new(); + let mut processed_count: u64 = 0; + for entry in self.builder_pending_withdrawals.iter() { + let all_count = combined_withdrawals_len(prior_withdrawals, &withdrawals)?; + if all_count >= withdrawals_limit { + break; + } + let builder_index = entry.builder_index; + withdrawals.push(Withdrawal { + index: withdrawal_index, + validator_index: Self::convert_builder_index_to_validator_index(builder_index)?, + address: entry.fee_recipient, + amount: entry.amount, + }); + withdrawal_index = increment_withdrawal_index(withdrawal_index)?; + processed_count = increment_processed_count(processed_count)?; + } + Ok((withdrawals, withdrawal_index, processed_count)) + } + + /// Build withdrawals from finalized pending partial-withdrawal requests. + /// + /// Queue entries that are due but no longer eligible are still counted as + /// processed so the queue can drain. + pub fn get_pending_partial_withdrawals( + &self, + mut withdrawal_index: WithdrawalIndex, + prior_withdrawals: &[Withdrawal], + ) -> Result<(Vec, WithdrawalIndex, u64), TransitionError> { + let epoch = self.slot.epoch(); + let max_pending_partials = usize::try_from(MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP) + .map_err(|_| { + TransitionError::ArithmeticOverflow(TransitionArithmetic::BoundedListLength) + })?; + let withdrawals_limit = prior_withdrawals + .len() + .checked_add(max_pending_partials) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))? + .min(MAX_WITHDRAWALS_PER_PAYLOAD - 1); + if prior_withdrawals.len() > withdrawals_limit { + return Err(TransitionError::BoundedListFull( + BoundedList::PayloadExpectedWithdrawals, + )); + } + let mut withdrawals: Vec = Vec::new(); + let mut processed_count: u64 = 0; + for entry in self.pending_partial_withdrawals.iter() { + let all_count = combined_withdrawals_len(prior_withdrawals, &withdrawals)?; + if entry.withdrawable_epoch > epoch || all_count >= withdrawals_limit { + break; + } + let validator_index = entry.validator_index; + let validator_index_usize = validator_index_as_usize(validator_index)?; + if validator_index_usize >= self.validators.len() { + return Err( + RegistryError::ValidatorIndexOutOfRange(validator_index.as_u64()).into(), + ); + } + let validator = &self.validators[validator_index_usize]; + let combined: Vec = prior_withdrawals + .iter() + .copied() + .chain(withdrawals.iter().copied()) + .collect(); + let balance = self.get_balance_after_withdrawals(validator_index, &combined)?; + if self.is_eligible_for_partial_withdrawals(validator, balance) { + let max_withdraw = balance + .as_u64() + .saturating_sub(MIN_ACTIVATION_BALANCE.as_u64()); + let amount = Gwei(entry.amount.as_u64().min(max_withdraw)); + withdrawals.push(Withdrawal { + index: withdrawal_index, + validator_index, + address: withdrawal_address_from_credentials(&validator.withdrawal_credentials), + amount, + }); + withdrawal_index = increment_withdrawal_index(withdrawal_index)?; + } + processed_count = increment_processed_count(processed_count)?; + } + Ok((withdrawals, withdrawal_index, processed_count)) + } + + /// Sweep builders from `next_withdrawal_builder_index` for withdrawable balances. + /// + /// Returns both withdrawals and the number of builder records visited so the + /// cursor can be advanced by withdrawal processing during block transition. + pub fn get_builders_sweep_withdrawals( + &self, + mut withdrawal_index: WithdrawalIndex, + prior_withdrawals: &[Withdrawal], + ) -> Result<(Vec, WithdrawalIndex, u64), TransitionError> { + let epoch = self.slot.epoch(); + let builder_len = self.builders.len(); + if builder_len == 0 { + return Ok((Vec::new(), withdrawal_index, 0)); + } + let next_builder_index = builder_index_as_usize(self.next_withdrawal_builder_index)?; + if next_builder_index >= builder_len { + return Err(RegistryError::BuilderIndexOutOfRange( + self.next_withdrawal_builder_index.as_u64(), + ) + .into()); + } + let builders_limit = sweep_limit(MAX_BUILDERS_PER_WITHDRAWALS_SWEEP, builder_len)?; + let withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD - 1; + if prior_withdrawals.len() > withdrawals_limit { + return Err(TransitionError::BoundedListFull( + BoundedList::PayloadExpectedWithdrawals, + )); + } + + let mut withdrawals: Vec = Vec::new(); + let mut processed_count: u64 = 0; + let mut cursor = next_builder_index; + for _ in 0..builders_limit { + let all_count = combined_withdrawals_len(prior_withdrawals, &withdrawals)?; + if all_count >= withdrawals_limit { + break; + } + let builder = &self.builders[cursor]; + if builder.withdrawable_epoch <= epoch && builder.balance.as_u64() > 0 { + let builder_index = BuilderIndex(cursor_as_u64(cursor)?); + withdrawals.push(Withdrawal { + index: withdrawal_index, + validator_index: Self::convert_builder_index_to_validator_index(builder_index)?, + address: builder.execution_address, + amount: builder.balance, + }); + withdrawal_index = increment_withdrawal_index(withdrawal_index)?; + } + cursor = (cursor + 1) % builder_len; + processed_count = increment_processed_count(processed_count)?; + } + Ok((withdrawals, withdrawal_index, processed_count)) + } + + /// Sweep validators from `next_withdrawal_validator_index` for full or + /// partial withdrawals. + pub fn get_validators_sweep_withdrawals( + &self, + mut withdrawal_index: WithdrawalIndex, + prior_withdrawals: &[Withdrawal], + ) -> Result<(Vec, WithdrawalIndex, u64), TransitionError> { + let epoch = self.slot.epoch(); + let registry_len = self.validators.len(); + if registry_len == 0 { + return Ok((Vec::new(), withdrawal_index, 0)); + } + if prior_withdrawals.len() >= MAX_WITHDRAWALS_PER_PAYLOAD { + return Err(TransitionError::BoundedListFull( + BoundedList::PayloadExpectedWithdrawals, + )); + } + let next_validator_index = validator_index_as_usize(self.next_withdrawal_validator_index)?; + if next_validator_index >= registry_len { + return Err(RegistryError::ValidatorIndexOutOfRange( + self.next_withdrawal_validator_index.as_u64(), + ) + .into()); + } + let validators_limit = sweep_limit(MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP, registry_len)?; + let withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD; + + let mut withdrawals: Vec = Vec::new(); + let mut processed_count: u64 = 0; + let mut cursor = next_validator_index; + for _ in 0..validators_limit { + let all_count = combined_withdrawals_len(prior_withdrawals, &withdrawals)?; + if all_count >= withdrawals_limit { + break; + } + let validator = &self.validators[cursor]; + let combined: Vec = prior_withdrawals + .iter() + .copied() + .chain(withdrawals.iter().copied()) + .collect(); + let validator_index = ValidatorIndex(cursor_as_u64(cursor)?); + let balance = self.get_balance_after_withdrawals(validator_index, &combined)?; + let address = withdrawal_address_from_credentials(&validator.withdrawal_credentials); + if validator.is_fully_withdrawable_validator(balance, epoch) { + withdrawals.push(Withdrawal { + index: withdrawal_index, + validator_index, + address, + amount: balance, + }); + withdrawal_index = increment_withdrawal_index(withdrawal_index)?; + } else if validator.is_partially_withdrawable_validator(balance) { + let max = validator.get_max_effective_balance(); + let amount = balance.saturating_sub(max); + withdrawals.push(Withdrawal { + index: withdrawal_index, + validator_index, + address, + amount, + }); + withdrawal_index = increment_withdrawal_index(withdrawal_index)?; + } + cursor = (cursor + 1) % registry_len; + processed_count = increment_processed_count(processed_count)?; + } + Ok((withdrawals, withdrawal_index, processed_count)) + } + + /// Compute the withdrawals expected in the next execution payload and the + /// queue/cursor deltas needed after applying them. + pub fn get_expected_withdrawals(&self) -> Result { + let mut withdrawals: Vec = Vec::new(); + let mut withdrawal_index = self.next_withdrawal_index; + + let (builder_withdrawals, next_index, processed_builder_withdrawals_count) = + self.get_builder_withdrawals(withdrawal_index, &withdrawals)?; + withdrawal_index = next_index; + withdrawals.extend(builder_withdrawals); + + let (partial_withdrawals, next_index, processed_partial_withdrawals_count) = + self.get_pending_partial_withdrawals(withdrawal_index, &withdrawals)?; + withdrawal_index = next_index; + withdrawals.extend(partial_withdrawals); + + let (builders_sweep_withdrawals, next_index, processed_builders_sweep_count) = + self.get_builders_sweep_withdrawals(withdrawal_index, &withdrawals)?; + withdrawal_index = next_index; + withdrawals.extend(builders_sweep_withdrawals); + + let (validators_sweep_withdrawals, _withdrawal_index, processed_sweep_withdrawals_count) = + self.get_validators_sweep_withdrawals(withdrawal_index, &withdrawals)?; + withdrawals.extend(validators_sweep_withdrawals); + + Ok(ExpectedWithdrawals { + withdrawals, + processed_builder_withdrawals_count, + processed_partial_withdrawals_count, + processed_builders_sweep_count, + processed_sweep_withdrawals_count, + }) + } + + /// Apply selected withdrawals to validator and builder balances. + pub fn apply_withdrawals(&mut self, withdrawals: &[Withdrawal]) -> Result<(), TransitionError> { + for withdrawal in withdrawals { + if withdrawal.validator_index.is_builder_index() { + let builder_index = withdrawal.validator_index.to_builder_index()?; + let builder_index_usize = builder_index_as_usize(builder_index)?; + if builder_index_usize >= self.builders.len() { + return Err( + RegistryError::BuilderIndexOutOfRange(builder_index.as_u64()).into(), + ); + } + let builder = &mut self.builders[builder_index_usize]; + let current = builder.balance; + let amount = Gwei(withdrawal.amount.as_u64().min(current.as_u64())); + builder.balance = current.saturating_sub(amount); + } else { + self.decrease_balance(withdrawal.validator_index, withdrawal.amount)?; + } + } + Ok(()) + } + + /// Update the global withdrawal sequence cursor. + pub fn update_next_withdrawal_index( + &mut self, + withdrawals: &[Withdrawal], + ) -> Result<(), TransitionError> { + if let Some(latest_withdrawal) = withdrawals.last() { + self.next_withdrawal_index = increment_withdrawal_index(latest_withdrawal.index)?; + } + Ok(()) + } + + /// Mirror selected withdrawals onto the payload expected-withdrawals list. + pub fn update_payload_expected_withdrawals( + &mut self, + withdrawals: &[Withdrawal], + ) -> Result<(), TransitionError> { + self.payload_expected_withdrawals = + List::::try_from(withdrawals.to_vec()) + .map_err(|_| { + TransitionError::BoundedListFull(BoundedList::PayloadExpectedWithdrawals) + })?; + Ok(()) + } + + /// Remove consumed entries from the builder pending-withdrawals queue. + pub fn update_builder_pending_withdrawals( + &mut self, + processed_builder_withdrawals_count: u64, + ) -> Result<(), TransitionError> { + let skip = usize::try_from(processed_builder_withdrawals_count).map_err(|_| { + TransitionError::ArithmeticOverflow(TransitionArithmetic::BoundedListLength) + })?; + let remaining: Vec = self + .builder_pending_withdrawals + .iter() + .skip(skip) + .copied() + .collect(); + self.builder_pending_withdrawals = List::< + BuilderPendingWithdrawal, + BUILDER_PENDING_WITHDRAWALS_LIMIT, + >::try_from(remaining) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::BuilderPendingWithdrawals))?; + Ok(()) + } + + /// Remove consumed entries from the pending partial-withdrawals queue. + pub fn update_pending_partial_withdrawals( + &mut self, + processed_partial_withdrawals_count: u64, + ) -> Result<(), TransitionError> { + let skip = usize::try_from(processed_partial_withdrawals_count).map_err(|_| { + TransitionError::ArithmeticOverflow(TransitionArithmetic::BoundedListLength) + })?; + let remaining: Vec = self + .pending_partial_withdrawals + .iter() + .skip(skip) + .copied() + .collect(); + self.pending_partial_withdrawals = List::< + PendingPartialWithdrawal, + PENDING_PARTIAL_WITHDRAWALS_LIMIT, + >::try_from(remaining) + .map_err(|_| TransitionError::BoundedListFull(BoundedList::PendingPartialWithdrawals))?; + Ok(()) + } + + /// Rotate the builder sweep cursor by the number of builders visited. + pub fn update_next_withdrawal_builder_index( + &mut self, + processed_builders_sweep_count: u64, + ) -> Result<(), TransitionError> { + let builder_len = self.builders.len(); + if builder_len > 0 { + let next_index = self + .next_withdrawal_builder_index + .as_u64() + .checked_add(processed_builders_sweep_count) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))?; + self.next_withdrawal_builder_index = + BuilderIndex(next_index % len_as_u64(builder_len)?); + } + Ok(()) + } + + /// Rotate the validator sweep cursor after payload withdrawals are selected. + pub fn update_next_withdrawal_validator_index( + &mut self, + withdrawals: &[Withdrawal], + ) -> Result<(), TransitionError> { + let registry_len = self.validators.len(); + if registry_len > 0 { + let registry_len_u64 = len_as_u64(registry_len)?; + if withdrawals.len() == MAX_WITHDRAWALS_PER_PAYLOAD { + if let Some(latest_withdrawal) = withdrawals.last() { + let next_index = latest_withdrawal + .validator_index + .as_u64() + .checked_add(1) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))?; + self.next_withdrawal_validator_index = + ValidatorIndex(next_index % registry_len_u64); + } + } else { + let next_index = self + .next_withdrawal_validator_index + .as_u64() + .checked_add(MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) + .ok_or(TransitionError::ArithmeticOverflow( + TransitionArithmetic::BoundedListLength, + ))?; + self.next_withdrawal_validator_index = + ValidatorIndex(next_index % registry_len_u64); + } + } + Ok(()) + } + + /// Compute expected withdrawals for the current payload branch and apply them. + /// + /// If the latest settled execution block hash does not match the latest + /// [bid](crate::glossary#execution-payload-bid)'s promised block hash, the + /// payload branch is not settled and this phase is a no-op. Otherwise it + /// drains builder and validator withdrawal queues, mirrors selected + /// withdrawals into `payload_expected_withdrawals`, and rotates the builder + /// and validator sweep cursors. + pub fn process_withdrawals(&mut self) -> Result<(), TransitionError> { + if self.latest_block_hash != self.latest_execution_payload_bid.block_hash { + return Ok(()); + } + + let expected = self.get_expected_withdrawals()?; + + self.apply_withdrawals(&expected.withdrawals)?; + self.update_next_withdrawal_index(&expected.withdrawals)?; + self.update_payload_expected_withdrawals(&expected.withdrawals)?; + self.update_builder_pending_withdrawals(expected.processed_builder_withdrawals_count)?; + self.update_pending_partial_withdrawals(expected.processed_partial_withdrawals_count)?; + self.update_next_withdrawal_builder_index(expected.processed_builders_sweep_count)?; + self.update_next_withdrawal_validator_index(&expected.withdrawals)?; + + Ok(()) + } +} diff --git a/moonglass-node/Cargo.toml b/moonglass-node/Cargo.toml new file mode 100644 index 0000000..4aaaa1f --- /dev/null +++ b/moonglass-node/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "moonglass-node" +version = "0.1.0" +edition = "2024" +rust-version = "1.96" +description = "Devnet wrapper for the moonglass consensus core." +license = "AGPL-3.0-only" +repository = "https://github.com/brech1/moonglass" +documentation = "https://brech1.github.io/moonglass/moonglass_node/" +readme = "README.md" +keywords = ["ethereum", "consensus", "devnet"] +categories = ["cryptography::cryptocurrencies"] + +[features] +default = ["mainnet"] +mainnet = ["moonglass-core/mainnet"] +minimal = ["moonglass-core/minimal"] +follower = ["dep:snap"] +node = [ + "follower", + "dep:libp2p", + "dep:discv5", + "dep:tokio", + "dep:futures", + "dep:async-trait", + "dep:tracing", + "dep:tracing-subscriber", + "dep:reqwest", + "dep:axum", + "dep:serde_json", +] + +[dependencies] +moonglass-core = { version = "0.1.0", path = "../moonglass-core", default-features = false } +serde_yaml = "0.9" +sha2 = "0.10" +thiserror = "2" +snap = { version = "1", optional = true } +libp2p = { version = "0.54", features = ["tokio", "tcp", "noise", "yamux", "gossipsub", "request-response", "identify", "ping", "secp256k1", "macros"], optional = true } +discv5 = { version = "0.9", optional = true } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "signal"], optional = true } +futures = { version = "0.3", optional = true } +async-trait = { version = "0.1", optional = true } +tracing = { version = "0.1", optional = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"], optional = true } +axum = { version = "0.7", optional = true } +serde_json = { version = "1", optional = true } + +[[bin]] +name = "moonglass-node" +path = "src/bin/node.rs" +required-features = ["node"] + +[lints] +workspace = true + +[package.metadata.docs.rs] +no-default-features = true +features = ["minimal"] +rustdoc-args = ["--document-private-items"] diff --git a/moonglass-node/README.md b/moonglass-node/README.md new file mode 100644 index 0000000..b9fbd63 --- /dev/null +++ b/moonglass-node/README.md @@ -0,0 +1,76 @@ +# moonglass-node + +`moonglass-node` runs [`moonglass-core`](../moonglass-core/) against a real +devnet. It parses a `config.yaml` into a typed `ChainConfig`, loads a +genesis/anchor `GenesisBundle`, and, behind feature flags, hosts a read-only +follower that feeds gossip into the engine's fork choice to track the chain head. + +The default build is just the config and genesis library; the network-facing +parts are feature-gated. + +## Features + +| Feature | Adds | +| --- | --- | +| _default_ | `ChainConfig` parsing and `GenesisBundle` loading. No extra dependencies. | +| `follower` | The engine seam: gossip decode, topic dispatch, fork-choice handlers, and a replay-driven correctness oracle. Adds only `snap`. | +| `node` | The live transport: a libp2p swarm, discv5 discovery, the follow loop, a consensus-client checkpoint fetch, and the runnable binary. Adds the async networking stack (libp2p, discv5, tokio, futures) plus reqwest and tracing. | + +The preset (`mainnet` / `minimal`) is inherited from `moonglass-core` and is +orthogonal to these features. + +## The follower + +The follower is the smallest thing that runs the engine against a live network: +it anchors at a checkpoint, subscribes to the chain's gossip, and feeds each +message through fork choice. Every piece is a permanent part of the node, not +throwaway scaffolding. + +- **Engine seam (`follower`).** `codec` handles snappy (raw block, used by both + gossip and fixtures); `topics` builds the subscribe set for a fork digest; + `dispatch` maps a topic to a decoded container and the owning fork-choice + handler; `replay` drives a captured message stream offline and verifies the + head. `anchor`, `clock`, and `runtime` bridge to a live session. +- **Transport (`node`).** `node/network` builds the libp2p swarm and consensus + gossipsub (anonymous validation, content message id, a 10 MiB limit); + `node/discovery` runs discv5 and forwards discovered peers as dial targets; + `node/run` is the loop that ticks the clock and routes each gossip message + through the seam. Blocks apply their embedded attestations and aggregate + attestations feed fork-choice weight, so the head reflects votes. +- **Execution boundary.** Payload execution validity is an injected + `ExecutionPayloadVerifier`. The follower accepts all payloads, so a recorded + payload is consensus-checked, not engine-confirmed; a real engine verifier + slots into the same seam later. +- **Anchor rule.** Moonglass models a single live fork, so the follower anchors + at a checkpoint already inside that fork's range, never a pre-fork genesis. + `anchor::adopt_checkpoint` enforces this. + +## Running the node + +```bash +cargo run -p moonglass-node --features node -- \ + config.yaml genesis.ssz http://localhost:5052 \ + /ip4/0.0.0.0/tcp/9000 9000 5053 ... +``` + +The follower reads `config.yaml` and `genesis.ssz` from the launcher, fetches the +finalized checkpoint state and block from the consensus client at the given URL, +anchors on it, then listens on the multiaddr, discovers peers over discv5 on the +UDP port, subscribes to the chain's gossip, and logs the head each slot. +`RUST_LOG` controls verbosity. + +## Build and test + +```bash +cargo build -p moonglass-node # default: config and genesis only +cargo test -p moonglass-node --no-default-features --features minimal,follower +cargo clippy -p moonglass-node --features node --all-targets +``` + +## Status + +The engine seam, the replay oracle, and the libp2p + discv5 transport with the +runnable binary are all in place and build on both presets. It is not yet +verified against a live devnet. Deferred refinements: per-subnet attestation +topics, request/response sync, peer filtering by fork digest, and a real +execution-engine verifier. diff --git a/moonglass-node/src/bin/node.rs b/moonglass-node/src/bin/node.rs new file mode 100644 index 0000000..52506b8 --- /dev/null +++ b/moonglass-node/src/bin/node.rs @@ -0,0 +1,195 @@ +//! Runnable read-only devnet follower (behind the `node` feature). +//! +//! Reads the launcher configuration and genesis state from files, fetches the +//! finalized checkpoint state and block from a consensus client over HTTP, +//! anchors the engine, subscribes to the chain's gossip, and tracks the head. +//! Usage: +//! +//! ```text +//! moonglass-node \ +//! [bootnode-enr ...] +//! ``` +//! +//! The finalized state and block are fetched in two separate requests against +//! the moving `finalized` alias. If finalization advances on the consensus +//! client between the two fetches, the state and block belong to different +//! checkpoints and anchoring fails with `AnchorStateRootMismatch`. This is a +//! transient startup failure at a finalization boundary, so simply re-run the +//! binary when it occurs. The fork-choice store requires a finalized anchor, so +//! on a chain whose finality has stalled far behind the head the anchor lags the +//! gossip head until block backfill bridges the gap. + +use std::process::ExitCode; + +use libp2p::Multiaddr; + +use moonglass_core::constants::FAR_FUTURE_EPOCH; +use moonglass_core::containers::{BeaconState, SignedBeaconBlock}; +use moonglass_core::primitives::Slot; +use moonglass_core::ssz::Deserialize; + +use moonglass_node::follower::anchor::{AnchorError, adopt_checkpoint, load_context}; +use moonglass_node::follower::clock::{self, SlotClock}; +use moonglass_node::follower::topics::TopicTable; +use moonglass_node::node::discovery::{self, DiscoveryConfig, DiscoveryError}; +use moonglass_node::node::run::{RunConfig, RunError, run}; + +/// A startup failure before the follow loop takes over. +#[derive(Debug, thiserror::Error)] +enum FollowError { + /// The command line did not supply the required arguments. + #[error( + "usage: moonglass-node [bootnode-enr ...]" + )] + Usage, + /// A consensus REST request failed. + #[error("consensus client request failed: {0}")] + Http(reqwest::Error), + /// An input file could not be read. + #[error("reading {path}: {source}")] + Io { + /// Path that failed to read. + path: String, + /// Underlying I/O error. + source: std::io::Error, + }, + /// A listen address was not a valid multiaddr. + #[error("invalid multiaddr: {0}")] + Multiaddr(#[from] libp2p::multiaddr::Error), + /// A UDP or API port was not a valid number. + #[error("invalid port: {0}")] + Port(std::num::ParseIntError), + /// A state or block SSZ payload failed to decode. + #[error("ssz decode failed: {0}")] + Ssz(#[from] moonglass_core::ssz::DeserializeError), + /// The checkpoint could not anchor the engine. + #[error(transparent)] + Anchor(#[from] AnchorError), + /// The gossip topic set could not be built. + #[error("topic set failed: {0}")] + Topics(#[from] moonglass_core::error::TransitionError), + /// discv5 discovery could not start. + #[error(transparent)] + Discovery(#[from] DiscoveryError), + /// The follow loop failed to start or run. + #[error(transparent)] + Run(#[from] RunError), +} + +/// Read a file, tagging any error with its path. +fn read_file(path: &str) -> Result, FollowError> { + std::fs::read(path).map_err(|source| FollowError::Io { + path: path.to_owned(), + source, + }) +} + +/// Fetch SSZ bytes from a consensus REST endpoint. +async fn fetch_ssz(client: &reqwest::Client, url: &str) -> Result, FollowError> { + let response = client + .get(url) + .header("accept", "application/octet-stream") + .send() + .await + .map_err(FollowError::Http)? + .error_for_status() + .map_err(FollowError::Http)?; + let bytes = response.bytes().await.map_err(FollowError::Http)?; + Ok(bytes.to_vec()) +} + +/// Anchor the engine from files and run the follow loop. +async fn follow() -> Result<(), FollowError> { + let args: Vec = std::env::args().collect(); + if args.len() < 7 { + return Err(FollowError::Usage); + } + + let context = load_context(&read_file(&args[1])?, &read_file(&args[2])?)?; + let cl_url = &args[3]; + let listen: Multiaddr = args[4].parse()?; + let udp_port: u16 = args[5].parse().map_err(FollowError::Port)?; + let api_port: u16 = args[6].parse().map_err(FollowError::Port)?; + let bootnodes = args[7..].to_vec(); + + // Fetch the finalized checkpoint state and block from the consensus client. + let client = reqwest::Client::new(); + let state_ssz = fetch_ssz( + &client, + &format!("{cl_url}/eth/v2/debug/beacon/states/finalized"), + ) + .await?; + let block_ssz = fetch_ssz(&client, &format!("{cl_url}/eth/v2/beacon/blocks/finalized")).await?; + + // Decode and anchor in a scope so the large structures drop before the loop. + let (engine, genesis_time) = { + let state = BeaconState::deserialize(&state_ssz)?; + let signed = SignedBeaconBlock::deserialize(&block_ssz)?; + let genesis_time = state.genesis_time; + ( + adopt_checkpoint(&context, &state, &signed.message)?, + genesis_time, + ) + }; + + // Topics follow the fork digest of the current epoch. + let current_slot = SlotClock::new(genesis_time).slot_at(clock::unix_now()); + let epoch = Slot::new(current_slot).epoch(); + let topics = TopicTable::for_config( + &context.chain_config, + context.genesis_validators_root, + epoch, + )?; + + // Advertise the fork digest in the ENR so consensus peers recognize the + // follower: ENRForkID = fork_digest, next_fork_version, next_fork_epoch. + let fork_digest = context + .chain_config + .compute_fork_digest(context.genesis_validators_root, epoch)?; + let fork_version = context.chain_config.compute_fork_version(epoch); + let mut eth2_field = Vec::with_capacity(16); + eth2_field.extend_from_slice(&fork_digest.0); + eth2_field.extend_from_slice(&fork_version.0); + eth2_field.extend_from_slice(&FAR_FUTURE_EPOCH.as_u64().to_le_bytes()); + + let discovered = discovery::spawn(DiscoveryConfig { + udp_port, + eth2_field, + bootnodes, + }) + .await?; + + tracing::info!( + topics = topics.subscribe.len(), + udp_port, + "starting follower" + ); + Box::pin(run( + engine, + RunConfig { + listen, + topics, + discovered, + api_port, + chain_config: context.chain_config, + }, + )) + .await?; + Ok(()) +} + +/// Entry point: install logging, then run the follower to completion. +#[tokio::main] +async fn main() -> ExitCode { + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); + tracing_subscriber::fmt().with_env_filter(filter).init(); + + match follow().await { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + tracing::error!(%error, "follower exited"); + ExitCode::FAILURE + } + } +} diff --git a/moonglass-node/src/config.rs b/moonglass-node/src/config.rs new file mode 100644 index 0000000..7051ef0 --- /dev/null +++ b/moonglass-node/src/config.rs @@ -0,0 +1,784 @@ +//! Public chain configuration for externally driven devnets. +//! +//! A launcher supplies the active consensus configuration beside the genesis +//! state. This module turns that YAML into typed values and configured helpers +//! that call the plain core APIs. + +use serde_yaml::{Mapping, Value}; +use sha2::{Digest, Sha256}; + +use moonglass_core::constants::{ + AGGREGATE_DUE_BPS_GLOAS, ALTAIR_FORK_EPOCH, ALTAIR_FORK_VERSION, ATTESTATION_DUE_BPS_GLOAS, + BELLATRIX_FORK_EPOCH, BELLATRIX_FORK_VERSION, BLOB_SCHEDULE, CAPELLA_FORK_EPOCH, + CAPELLA_FORK_VERSION, CONTRIBUTION_DUE_BPS_GLOAS, CUSTODY_REQUIREMENT, DENEB_FORK_EPOCH, + DENEB_FORK_VERSION, DEPOSIT_CHAIN_ID, DEPOSIT_CONTRACT_ADDRESS, DEPOSIT_NETWORK_ID, + ELECTRA_FORK_EPOCH, ELECTRA_FORK_VERSION, FULU_FORK_EPOCH, FULU_FORK_VERSION, GENESIS_DELAY, + GENESIS_FORK_VERSION, GLOAS_FORK_EPOCH, GLOAS_FORK_VERSION, MAX_BLOBS_PER_BLOCK, + MAX_REQUEST_BLOCKS_DENEB, MAX_REQUEST_PAYLOADS, MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS, + MIN_GENESIS_ACTIVE_VALIDATOR_COUNT, MIN_GENESIS_TIME, PAYLOAD_ATTESTATION_DUE_BPS, + SAMPLES_PER_SLOT, SLOT_DURATION_MS, SYNC_MESSAGE_DUE_BPS_GLOAS, +}; +use moonglass_core::containers::BlobParameters; +use moonglass_core::error::TransitionError; +use moonglass_core::networking::first_four_bytes; +use moonglass_core::primitives::{Epoch, ExecutionAddress, ForkDigest, Root, Version}; +use moonglass_core::state_transition::compute_fork_data_root; + +use crate::error::ConfigError; + +/// Mainnet preset name used by consensus configuration files. +pub const MAINNET_PRESET_NAME: &str = "mainnet"; + +/// Minimal preset name used by consensus configuration files. +pub const MINIMAL_PRESET_NAME: &str = "minimal"; + +/// Compile-time preset selected for this build. +#[cfg(feature = "mainnet")] +pub const ACTIVE_PRESET: PresetBase = PresetBase::Mainnet; + +/// Compile-time preset selected for this build. +#[cfg(feature = "minimal")] +pub const ACTIVE_PRESET: PresetBase = PresetBase::Minimal; + +/// Spec preset selected by a chain configuration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PresetBase { + /// Production bounds and constants. + Mainnet, + /// Test-vector bounds and constants. + Minimal, +} + +impl PresetBase { + /// Parse a preset name. + pub fn from_name(name: &str) -> Result { + match name { + MAINNET_PRESET_NAME => Ok(Self::Mainnet), + MINIMAL_PRESET_NAME => Ok(Self::Minimal), + _ => Err(ConfigError::InvalidPreset), + } + } + + /// Return the name used in configuration files. + pub const fn as_str(self) -> &'static str { + match self { + Self::Mainnet => MAINNET_PRESET_NAME, + Self::Minimal => MINIMAL_PRESET_NAME, + } + } +} + +/// Fork versions and activation epochs read from a chain configuration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ForkSchedule { + /// Version stamped on the genesis state. + pub genesis_version: Version, + /// Version for scheduled upgrade one. + pub altair_version: Version, + /// Activation epoch for scheduled upgrade one. + pub altair_epoch: Epoch, + /// Version for scheduled upgrade two. + pub bellatrix_version: Version, + /// Activation epoch for scheduled upgrade two. + pub bellatrix_epoch: Epoch, + /// Version for scheduled upgrade three. + pub capella_version: Version, + /// Activation epoch for scheduled upgrade three. + pub capella_epoch: Epoch, + /// Version for scheduled upgrade four. + pub deneb_version: Version, + /// Activation epoch for scheduled upgrade four. + pub deneb_epoch: Epoch, + /// Version for scheduled upgrade five. + pub electra_version: Version, + /// Activation epoch for scheduled upgrade five. + pub electra_epoch: Epoch, + /// Version for scheduled upgrade six. + pub fulu_version: Version, + /// Activation epoch for scheduled upgrade six. + pub fulu_epoch: Epoch, + /// Version for the active single-fork core. + pub gloas_version: Version, + /// Activation epoch for the active single-fork core. + pub gloas_epoch: Epoch, +} + +impl ForkSchedule { + /// Return the configured version for `epoch`. + pub const fn compute_fork_version(self, epoch: Epoch) -> Version { + if epoch.0 >= self.gloas_epoch.0 { + return self.gloas_version; + } + if epoch.0 >= self.fulu_epoch.0 { + return self.fulu_version; + } + if epoch.0 >= self.electra_epoch.0 { + return self.electra_version; + } + if epoch.0 >= self.deneb_epoch.0 { + return self.deneb_version; + } + if epoch.0 >= self.capella_epoch.0 { + return self.capella_version; + } + if epoch.0 >= self.bellatrix_epoch.0 { + return self.bellatrix_version; + } + if epoch.0 >= self.altair_epoch.0 { + return self.altair_version; + } + self.genesis_version + } +} + +impl Default for ForkSchedule { + fn default() -> Self { + Self { + genesis_version: GENESIS_FORK_VERSION, + altair_version: ALTAIR_FORK_VERSION, + altair_epoch: ALTAIR_FORK_EPOCH, + bellatrix_version: BELLATRIX_FORK_VERSION, + bellatrix_epoch: BELLATRIX_FORK_EPOCH, + capella_version: CAPELLA_FORK_VERSION, + capella_epoch: CAPELLA_FORK_EPOCH, + deneb_version: DENEB_FORK_VERSION, + deneb_epoch: DENEB_FORK_EPOCH, + electra_version: ELECTRA_FORK_VERSION, + electra_epoch: ELECTRA_FORK_EPOCH, + fulu_version: FULU_FORK_VERSION, + fulu_epoch: FULU_FORK_EPOCH, + gloas_version: GLOAS_FORK_VERSION, + gloas_epoch: GLOAS_FORK_EPOCH, + } + } +} + +/// Blob limit schedule from a chain configuration. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlobSchedule { + /// Limit active before the first schedule entry. + pub default_max_blobs_per_block: u64, + /// Epoch used when only the request-carrying payload limit is configured. + pub electra_epoch: Epoch, + /// Limit active from the request-carrying payload upgrade. + pub electra_max_blobs_per_block: u64, + /// Stepwise limit entries sorted by activation epoch. + pub entries: Vec, +} + +impl BlobSchedule { + /// Return the blob-parameter tuple active at `epoch`. + pub fn get_blob_parameters(&self, epoch: Epoch) -> BlobParameters { + self.entries + .iter() + .rev() + .find_map(|entry| (epoch >= entry.epoch).then_some(*entry)) + .unwrap_or_else(|| { + if epoch >= self.electra_epoch { + return BlobParameters { + epoch: self.electra_epoch, + max_blobs_per_block: self.electra_max_blobs_per_block, + }; + } + BlobParameters { + epoch: Epoch::new(0), + max_blobs_per_block: self.default_max_blobs_per_block, + } + }) + } +} + +impl Default for BlobSchedule { + fn default() -> Self { + Self { + default_max_blobs_per_block: MAX_BLOBS_PER_BLOCK, + electra_epoch: ELECTRA_FORK_EPOCH, + electra_max_blobs_per_block: MAX_BLOBS_PER_BLOCK, + entries: BLOB_SCHEDULE + .iter() + .map(|(epoch, limit)| BlobParameters { + epoch: *epoch, + max_blobs_per_block: *limit, + }) + .collect(), + } + } +} + +/// Timing fields that affect launcher admission and payload checks. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TimingConfig { + /// Slot duration in milliseconds. + pub slot_duration_ms: u64, + /// Delay between genesis trigger and genesis slot. + pub genesis_delay: u64, + /// Attestation deadline in basis points of one slot. + pub attestation_due_bps_gloas: u64, + /// Aggregate deadline in basis points of one slot. + pub aggregate_due_bps_gloas: u64, + /// Sync-message deadline in basis points of one slot. + pub sync_message_due_bps_gloas: u64, + /// Contribution deadline in basis points of one slot. + pub contribution_due_bps_gloas: u64, + /// Payload-attestation deadline in basis points of one slot. + pub payload_attestation_due_bps: u64, +} + +impl Default for TimingConfig { + fn default() -> Self { + Self { + slot_duration_ms: SLOT_DURATION_MS, + genesis_delay: GENESIS_DELAY, + attestation_due_bps_gloas: ATTESTATION_DUE_BPS_GLOAS, + aggregate_due_bps_gloas: AGGREGATE_DUE_BPS_GLOAS, + sync_message_due_bps_gloas: SYNC_MESSAGE_DUE_BPS_GLOAS, + contribution_due_bps_gloas: CONTRIBUTION_DUE_BPS_GLOAS, + payload_attestation_due_bps: PAYLOAD_ATTESTATION_DUE_BPS, + } + } +} + +/// Network identity fields from a chain configuration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NetworkConfig { + /// Chain ID used by execution payloads and deposits. + pub deposit_chain_id: u64, + /// Network ID used by the deposit contract. + pub deposit_network_id: u64, + /// Deposit contract address. + pub deposit_contract_address: ExecutionAddress, +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { + deposit_chain_id: DEPOSIT_CHAIN_ID, + deposit_network_id: DEPOSIT_NETWORK_ID, + deposit_contract_address: DEPOSIT_CONTRACT_ADDRESS, + } + } +} + +/// Consensus configuration supplied by the caller. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChainConfig { + /// Spec preset selected by the configuration. + pub preset_base: PresetBase, + /// Minimum active validators needed for genesis checks. + pub min_genesis_active_validator_count: u64, + /// Earliest timestamp where genesis is valid. + pub min_genesis_time: u64, + /// Fork versions and epochs. + pub forks: ForkSchedule, + /// Blob limits used by fork digests and bid checks. + pub blob_schedule: BlobSchedule, + /// Timing values used by wrapper helpers. + pub timing: TimingConfig, + /// Network identity fields. + pub network: NetworkConfig, + /// Data samples expected per slot. + pub samples_per_slot: u64, + /// Custody group requirement. + pub custody_requirement: u64, + /// Maximum block roots in a sidecar request. + pub max_request_blocks_deneb: u64, + /// Maximum payload roots in a payload request. + pub max_request_payloads: usize, + /// Minimum epochs over which data columns are served. + pub min_epochs_for_data_column_sidecars_requests: u64, +} + +impl ChainConfig { + /// Return the compile-time preset configuration. + pub fn preset() -> Self { + Self::default() + } + + /// Return the configured fork version for `epoch`. + pub fn compute_fork_version(&self, epoch: Epoch) -> Version { + self.forks.compute_fork_version(epoch) + } + + /// Return the configured blob-parameter tuple active at `epoch`. + pub fn get_blob_parameters(&self, epoch: Epoch) -> BlobParameters { + self.blob_schedule.get_blob_parameters(epoch) + } + + /// Return the configured fork digest for `genesis_validators_root`. + pub fn compute_fork_digest( + &self, + genesis_validators_root: Root, + epoch: Epoch, + ) -> Result { + let fork_version = self.compute_fork_version(epoch); + let base_digest = compute_fork_data_root(fork_version, genesis_validators_root)?; + + if epoch < self.forks.fulu_epoch { + return Ok(ForkDigest(first_four_bytes(base_digest.0))); + } + + let blob_parameters = self.get_blob_parameters(epoch); + let mut input = [0_u8; 16]; + input[..8].copy_from_slice(&blob_parameters.epoch.as_u64().to_le_bytes()); + input[8..].copy_from_slice(&blob_parameters.max_blobs_per_block.to_le_bytes()); + let parameter_digest: [u8; 32] = Sha256::digest(input).into(); + let mut digest = [0_u8; 32]; + for (out, (base, parameter)) in digest + .iter_mut() + .zip(base_digest.0.into_iter().zip(parameter_digest)) + { + *out = base ^ parameter; + } + + Ok(ForkDigest(first_four_bytes(digest))) + } + + /// Parse a consensus `config.yaml` document. + pub fn from_yaml_str(input: &str) -> Result { + let normalized = quote_bare_hex_scalars(input); + let value: Value = + serde_yaml::from_str(&normalized).map_err(|source| ConfigError::Yaml { source })?; + Self::from_yaml_value(&value) + } + + /// Parse a consensus `config.yaml` byte slice. + pub fn from_yaml_slice(input: &[u8]) -> Result { + Self::from_yaml_str(&String::from_utf8_lossy(input)) + } + + /// Build from a parsed YAML value. + pub fn from_yaml_value(value: &Value) -> Result { + let root = value + .as_mapping() + .ok_or(ConfigError::InvalidField("config"))?; + let map = chain_config_mapping(root)?; + let preset_name = required_str(map, "PRESET_BASE", "preset")?; + let preset_base = PresetBase::from_name(&preset_name)?; + if preset_base != ACTIVE_PRESET { + return Err(ConfigError::PresetMismatch { + configured: preset_base, + active: ACTIVE_PRESET, + }); + } + let mut config = Self { + preset_base, + ..Self::default() + }; + + apply_genesis_fields(&mut config, map)?; + apply_fork_fields(&mut config, map)?; + apply_timing_fields(&mut config, map)?; + apply_blob_fields(&mut config, map)?; + apply_network_fields(&mut config, map)?; + apply_data_request_fields(&mut config, map)?; + + Ok(config) + } +} + +/// Wrap bare hexadecimal scalar values in quotes so the YAML reader keeps them +/// as strings. +/// +/// Consensus configs write address, hash, and fork version fields as unquoted +/// `0x` scalars. The generic YAML value type resolves those as integers and +/// rejects any that exceed 64 bits, such as the deposit contract address, so the +/// document is normalized before parsing. Only flat `KEY: 0x...` value lines are +/// affected, which leaves nested entries such as the blob schedule untouched. +pub fn quote_bare_hex_scalars(input: &str) -> String { + let mut out = String::with_capacity(input.len() + 64); + for line in input.lines() { + out.push_str("e_bare_hex_line(line)); + out.push('\n'); + } + out +} + +/// Quote the value of a single `KEY: 0x...` line, or return the line unchanged. +fn quote_bare_hex_line(line: &str) -> String { + let Some(separator) = line.find(": ") else { + return line.to_owned(); + }; + let (prefix, rest) = line.split_at(separator + 2); + let value = rest.trim_start(); + let leading = &rest[..rest.len() - value.len()]; + let token_end = value.find(char::is_whitespace).unwrap_or(value.len()); + let (token, trailing) = value.split_at(token_end); + if is_bare_hex_scalar(token) { + format!("{prefix}{leading}\"{token}\"{trailing}") + } else { + line.to_owned() + } +} + +/// True when `token` is a non-empty unquoted `0x` hexadecimal scalar. +fn is_bare_hex_scalar(token: &str) -> bool { + token.strip_prefix("0x").is_some_and(|digits| { + !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_hexdigit()) + }) +} + +/// Apply genesis-trigger fields to a config. +pub fn apply_genesis_fields(config: &mut ChainConfig, map: &Mapping) -> Result<(), ConfigError> { + config.min_genesis_active_validator_count = optional_u64( + map, + "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT", + "min_genesis_active_validator_count", + )? + .unwrap_or(config.min_genesis_active_validator_count); + config.min_genesis_time = optional_u64(map, "MIN_GENESIS_TIME", "min_genesis_time")? + .unwrap_or(config.min_genesis_time); + Ok(()) +} + +/// Apply fork schedule fields to a config. +pub fn apply_fork_fields(config: &mut ChainConfig, map: &Mapping) -> Result<(), ConfigError> { + config.forks.genesis_version = + optional_version(map, "GENESIS_FORK_VERSION", "genesis_fork_version")? + .unwrap_or(config.forks.genesis_version); + config.forks.altair_version = + optional_version(map, "ALTAIR_FORK_VERSION", "altair_fork_version")? + .unwrap_or(config.forks.altair_version); + config.forks.altair_epoch = optional_epoch(map, "ALTAIR_FORK_EPOCH", "altair_fork_epoch")? + .unwrap_or(config.forks.altair_epoch); + config.forks.bellatrix_version = + optional_version(map, "BELLATRIX_FORK_VERSION", "bellatrix_fork_version")? + .unwrap_or(config.forks.bellatrix_version); + config.forks.bellatrix_epoch = + optional_epoch(map, "BELLATRIX_FORK_EPOCH", "bellatrix_fork_epoch")? + .unwrap_or(config.forks.bellatrix_epoch); + config.forks.capella_version = + optional_version(map, "CAPELLA_FORK_VERSION", "capella_fork_version")? + .unwrap_or(config.forks.capella_version); + config.forks.capella_epoch = optional_epoch(map, "CAPELLA_FORK_EPOCH", "capella_fork_epoch")? + .unwrap_or(config.forks.capella_epoch); + config.forks.deneb_version = optional_version(map, "DENEB_FORK_VERSION", "deneb_fork_version")? + .unwrap_or(config.forks.deneb_version); + config.forks.deneb_epoch = optional_epoch(map, "DENEB_FORK_EPOCH", "deneb_fork_epoch")? + .unwrap_or(config.forks.deneb_epoch); + config.forks.electra_version = + optional_version(map, "ELECTRA_FORK_VERSION", "electra_fork_version")? + .unwrap_or(config.forks.electra_version); + config.forks.electra_epoch = optional_epoch(map, "ELECTRA_FORK_EPOCH", "electra_fork_epoch")? + .unwrap_or(config.forks.electra_epoch); + config.forks.fulu_version = optional_version(map, "FULU_FORK_VERSION", "fulu_fork_version")? + .unwrap_or(config.forks.fulu_version); + config.forks.fulu_epoch = optional_epoch(map, "FULU_FORK_EPOCH", "fulu_fork_epoch")? + .unwrap_or(config.forks.fulu_epoch); + config.forks.gloas_version = optional_version(map, "GLOAS_FORK_VERSION", "gloas_fork_version")? + .unwrap_or(config.forks.gloas_version); + config.forks.gloas_epoch = optional_epoch(map, "GLOAS_FORK_EPOCH", "gloas_fork_epoch")? + .unwrap_or(config.forks.gloas_epoch); + config.blob_schedule.electra_epoch = config.forks.electra_epoch; + Ok(()) +} + +/// Apply timing fields to a config. +pub fn apply_timing_fields(config: &mut ChainConfig, map: &Mapping) -> Result<(), ConfigError> { + config.timing.slot_duration_ms = optional_u64(map, "SLOT_DURATION_MS", "slot_duration_ms")? + .unwrap_or(config.timing.slot_duration_ms); + // Slot timing is taken from the compile-time constant, so a configured value + // that disagrees with this build would be silently ignored. Reject it here. + if config.timing.slot_duration_ms != SLOT_DURATION_MS { + return Err(ConfigError::SlotDurationMismatch { + configured: config.timing.slot_duration_ms, + active: SLOT_DURATION_MS, + }); + } + config.timing.genesis_delay = + optional_u64(map, "GENESIS_DELAY", "genesis_delay")?.unwrap_or(config.timing.genesis_delay); + config.timing.attestation_due_bps_gloas = optional_u64( + map, + "ATTESTATION_DUE_BPS_GLOAS", + "attestation_due_bps_gloas", + )? + .unwrap_or(config.timing.attestation_due_bps_gloas); + config.timing.aggregate_due_bps_gloas = + optional_u64(map, "AGGREGATE_DUE_BPS_GLOAS", "aggregate_due_bps_gloas")? + .unwrap_or(config.timing.aggregate_due_bps_gloas); + config.timing.sync_message_due_bps_gloas = optional_u64( + map, + "SYNC_MESSAGE_DUE_BPS_GLOAS", + "sync_message_due_bps_gloas", + )? + .unwrap_or(config.timing.sync_message_due_bps_gloas); + config.timing.contribution_due_bps_gloas = optional_u64( + map, + "CONTRIBUTION_DUE_BPS_GLOAS", + "contribution_due_bps_gloas", + )? + .unwrap_or(config.timing.contribution_due_bps_gloas); + config.timing.payload_attestation_due_bps = optional_u64( + map, + "PAYLOAD_ATTESTATION_DUE_BPS", + "payload_attestation_due_bps", + )? + .unwrap_or(config.timing.payload_attestation_due_bps); + Ok(()) +} + +/// Apply blob schedule fields to a config. +pub fn apply_blob_fields(config: &mut ChainConfig, map: &Mapping) -> Result<(), ConfigError> { + config.blob_schedule.default_max_blobs_per_block = + optional_u64(map, "MAX_BLOBS_PER_BLOCK", "max_blobs_per_block")? + .unwrap_or(config.blob_schedule.default_max_blobs_per_block); + config.blob_schedule.electra_max_blobs_per_block = optional_u64( + map, + "MAX_BLOBS_PER_BLOCK_ELECTRA", + "max_blobs_per_block_electra", + )? + .unwrap_or(config.blob_schedule.electra_max_blobs_per_block); + if let Some(entries) = optional_blob_schedule(map)? { + config.blob_schedule.entries = entries; + } + Ok(()) +} + +/// Apply network identity fields to a config. +pub fn apply_network_fields(config: &mut ChainConfig, map: &Mapping) -> Result<(), ConfigError> { + let deposit_chain_id = match optional_u64(map, "DEPOSIT_CHAIN_ID", "deposit_chain_id")? { + Some(value) => Some(value), + None => optional_u64(map, "CHAIN_ID", "chain_id")?, + }; + config.network.deposit_chain_id = deposit_chain_id.unwrap_or(config.network.deposit_chain_id); + config.network.deposit_network_id = optional_u64(map, "DEPOSIT_NETWORK_ID", "network_id")? + .unwrap_or(config.network.deposit_network_id); + config.network.deposit_contract_address = + optional_execution_address(map, "DEPOSIT_CONTRACT_ADDRESS", "deposit_contract_address")? + .unwrap_or(config.network.deposit_contract_address); + Ok(()) +} + +/// Apply data-request and custody fields to a config. +pub fn apply_data_request_fields( + config: &mut ChainConfig, + map: &Mapping, +) -> Result<(), ConfigError> { + config.samples_per_slot = optional_u64(map, "SAMPLES_PER_SLOT", "samples_per_slot")? + .unwrap_or(config.samples_per_slot); + config.custody_requirement = optional_u64(map, "CUSTODY_REQUIREMENT", "custody_requirement")? + .unwrap_or(config.custody_requirement); + config.max_request_blocks_deneb = + optional_u64(map, "MAX_REQUEST_BLOCKS_DENEB", "max_request_blocks_deneb")? + .unwrap_or(config.max_request_blocks_deneb); + config.max_request_payloads = + optional_usize(map, "MAX_REQUEST_PAYLOADS", "max_request_payloads")? + .unwrap_or(config.max_request_payloads); + config.min_epochs_for_data_column_sidecars_requests = optional_u64( + map, + "MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS", + "min_epochs_for_data_column_sidecars_requests", + )? + .unwrap_or(config.min_epochs_for_data_column_sidecars_requests); + Ok(()) +} + +impl Default for ChainConfig { + fn default() -> Self { + Self { + preset_base: ACTIVE_PRESET, + min_genesis_active_validator_count: MIN_GENESIS_ACTIVE_VALIDATOR_COUNT, + min_genesis_time: MIN_GENESIS_TIME, + forks: ForkSchedule::default(), + blob_schedule: BlobSchedule::default(), + timing: TimingConfig::default(), + network: NetworkConfig::default(), + samples_per_slot: SAMPLES_PER_SLOT, + custody_requirement: CUSTODY_REQUIREMENT, + max_request_blocks_deneb: MAX_REQUEST_BLOCKS_DENEB, + max_request_payloads: MAX_REQUEST_PAYLOADS, + min_epochs_for_data_column_sidecars_requests: + MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS, + } + } +} + +/// Return the mapping that contains chain configuration fields. +pub fn chain_config_mapping(root: &Mapping) -> Result<&Mapping, ConfigError> { + if let Some(value) = root.get(Value::String("network_params".to_owned())) { + return value + .as_mapping() + .ok_or(ConfigError::InvalidField("network_params")); + } + Ok(root) +} + +/// Return the first matching YAML value for a primary or alternate key. +pub fn value_for(map: &Mapping, primary: &'static str, alternate: &'static str) -> Option { + map.get(Value::String(primary.to_owned())) + .or_else(|| map.get(Value::String(alternate.to_owned()))) + .cloned() +} + +/// Return a required string field. +pub fn required_str( + map: &Mapping, + primary: &'static str, + alternate: &'static str, +) -> Result { + value_for(map, primary, alternate) + .ok_or(ConfigError::MissingField(primary)) + .and_then(|value| yaml_str(&value, primary)) +} + +/// Return an optional `u64` field. +pub fn optional_u64( + map: &Mapping, + primary: &'static str, + alternate: &'static str, +) -> Result, ConfigError> { + value_for(map, primary, alternate) + .map(|value| yaml_u64(&value, primary)) + .transpose() +} + +/// Return an optional `usize` field. +pub fn optional_usize( + map: &Mapping, + primary: &'static str, + alternate: &'static str, +) -> Result, ConfigError> { + optional_u64(map, primary, alternate)? + .map(|value| usize::try_from(value).map_err(|_| ConfigError::InvalidField(primary))) + .transpose() +} + +/// Return an optional epoch field. +pub fn optional_epoch( + map: &Mapping, + primary: &'static str, + alternate: &'static str, +) -> Result, ConfigError> { + Ok(optional_u64(map, primary, alternate)?.map(Epoch)) +} + +/// Return an optional version field. +pub fn optional_version( + map: &Mapping, + primary: &'static str, + alternate: &'static str, +) -> Result, ConfigError> { + value_for(map, primary, alternate) + .map(|value| yaml_version(&value, primary)) + .transpose() +} + +/// Return an optional execution address field. +pub fn optional_execution_address( + map: &Mapping, + primary: &'static str, + alternate: &'static str, +) -> Result, ConfigError> { + value_for(map, primary, alternate) + .map(|value| yaml_execution_address(&value, primary)) + .transpose() +} + +/// Return an optional blob schedule. +pub fn optional_blob_schedule(map: &Mapping) -> Result>, ConfigError> { + let Some(value) = value_for(map, "BLOB_SCHEDULE", "blob_schedule") else { + return Ok(None); + }; + let sequence = value + .as_sequence() + .ok_or(ConfigError::InvalidField("BLOB_SCHEDULE"))?; + let mut entries = Vec::with_capacity(sequence.len()); + for entry in sequence { + let entry_map = entry + .as_mapping() + .ok_or(ConfigError::InvalidField("BLOB_SCHEDULE"))?; + let epoch = required_u64(entry_map, "EPOCH", "epoch")?; + let limit = required_u64(entry_map, "MAX_BLOBS_PER_BLOCK", "max_blobs_per_block")?; + entries.push(BlobParameters { + epoch: Epoch(epoch), + max_blobs_per_block: limit, + }); + } + entries.sort_by_key(|entry| entry.epoch.as_u64()); + Ok(Some(entries)) +} + +/// Return a required `u64` field. +pub fn required_u64( + map: &Mapping, + primary: &'static str, + alternate: &'static str, +) -> Result { + value_for(map, primary, alternate) + .ok_or(ConfigError::MissingField(primary)) + .and_then(|value| yaml_u64(&value, primary)) +} + +/// Decode a YAML value as a string. +pub fn yaml_str(value: &Value, field: &'static str) -> Result { + value + .as_str() + .map(ToOwned::to_owned) + .ok_or(ConfigError::InvalidField(field)) +} + +/// Decode a YAML value as `u64`. +pub fn yaml_u64(value: &Value, field: &'static str) -> Result { + if let Some(number) = value.as_u64() { + return Ok(number); + } + let text = yaml_str(value, field)?; + if let Some(hex) = text.strip_prefix("0x") { + return u64::from_str_radix(hex, 16).map_err(|_| ConfigError::InvalidHex(field)); + } + text.parse::() + .map_err(|_| ConfigError::InvalidField(field)) +} + +/// Decode a YAML value as a fork version. +pub fn yaml_version(value: &Value, field: &'static str) -> Result { + if let Some(number) = value.as_u64() { + let version = + u32::try_from(number).map_err(|_| ConfigError::InvalidVersionLength(field))?; + return Ok(Version(version.to_be_bytes())); + } + let bytes = yaml_hex_bytes(value, field)?; + let version: [u8; 4] = bytes + .try_into() + .map_err(|_| ConfigError::InvalidVersionLength(field))?; + Ok(Version(version)) +} + +/// Decode a YAML value as an execution address. +pub fn yaml_execution_address( + value: &Value, + field: &'static str, +) -> Result { + let bytes = yaml_hex_bytes(value, field)?; + let address: [u8; 20] = bytes + .try_into() + .map_err(|_| ConfigError::InvalidExecutionAddressLength(field))?; + Ok(ExecutionAddress(address)) +} + +/// Decode a YAML value as raw hex bytes. +pub fn yaml_hex_bytes(value: &Value, field: &'static str) -> Result, ConfigError> { + let text = yaml_str(value, field)?; + let hex = text + .strip_prefix("0x") + .ok_or(ConfigError::InvalidHex(field))?; + if !hex.len().is_multiple_of(2) { + return Err(ConfigError::InvalidHex(field)); + } + let mut bytes = Vec::with_capacity(hex.len() / 2); + for chunk in hex.as_bytes().chunks_exact(2) { + let high = hex_nibble(chunk[0]).ok_or(ConfigError::InvalidHex(field))?; + let low = hex_nibble(chunk[1]).ok_or(ConfigError::InvalidHex(field))?; + bytes.push((high << 4) | low); + } + Ok(bytes) +} + +/// Decode one ASCII hex nibble. +pub const fn hex_nibble(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} diff --git a/moonglass-node/src/error.rs b/moonglass-node/src/error.rs new file mode 100644 index 0000000..144e74e --- /dev/null +++ b/moonglass-node/src/error.rs @@ -0,0 +1,106 @@ +//! Error taxonomy for devnet wrapper inputs. + +use thiserror::Error; + +use crate::config::PresetBase; +use moonglass_core::primitives::{Epoch, Root}; +use moonglass_core::ssz::{DeserializeError, MerkleizationError}; + +/// Failures raised while reading a chain configuration. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum ConfigError { + /// YAML parsing failed before field validation could run. + #[error("chain configuration YAML is invalid")] + Yaml { + /// YAML parser error. + source: serde_yaml::Error, + }, + + /// A required field was absent. + #[error("chain configuration is missing {0}")] + MissingField(&'static str), + + /// A field existed but had the wrong YAML type. + #[error("chain configuration field {0} has an invalid type")] + InvalidField(&'static str), + + /// Preset name was not one of the supported spec presets. + #[error("chain configuration preset is unsupported")] + InvalidPreset, + + /// Preset name did not match this build. + #[error("chain configuration preset {configured:?} does not match active preset {active:?}")] + PresetMismatch { + /// Preset selected by the configuration. + configured: PresetBase, + /// Preset selected at compile time. + active: PresetBase, + }, + + /// Slot duration did not match this build. + #[error("chain configuration slot duration {configured} ms does not match active {active} ms")] + SlotDurationMismatch { + /// Slot duration in milliseconds selected by the configuration. + configured: u64, + /// Slot duration in milliseconds selected at compile time. + active: u64, + }, + + /// A hex field could not be decoded. + #[error("chain configuration field {0} is not valid hex")] + InvalidHex(&'static str), + + /// A version field did not decode to four bytes. + #[error("chain configuration field {0} is not a fork version")] + InvalidVersionLength(&'static str), + + /// An execution address field did not decode to twenty bytes. + #[error("chain configuration field {0} is not an execution address")] + InvalidExecutionAddressLength(&'static str), +} + +/// Failures raised while loading a genesis bundle. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum GenesisError { + /// Chain configuration could not be read. + #[error(transparent)] + Config(#[from] ConfigError), + + /// Genesis state SSZ could not be decoded. + #[error("genesis state SSZ is invalid")] + Ssz { + /// SSZ decoding error. + source: DeserializeError, + }, + + /// Genesis state SSZ is too short to hold the leading validators root. + #[error("genesis state SSZ is too short for the validators root")] + GenesisStateTooShort, + + /// Decoded validator registry root does not match the state field. + #[error("genesis validators root mismatch: got {got:?}, want {want:?}")] + GenesisValidatorsRootMismatch { + /// Root carried by the state field. + got: Root, + /// Root computed from the decoded registry. + want: Root, + }, + + /// State is earlier than the active single-fork boundary. + #[error("state epoch {state_epoch:?} is before active fork epoch {activation_epoch:?}")] + SingleForkNotActive { + /// Epoch of the supplied state. + state_epoch: Epoch, + /// Epoch where the active fork starts. + activation_epoch: Epoch, + }, + + /// Merkleization failed while checking the bundle. + #[error("genesis bundle merkleization failed")] + Merkleization { + /// SSZ merkleization error. + source: MerkleizationError, + }, +} diff --git a/moonglass-node/src/follower/anchor.rs b/moonglass-node/src/follower/anchor.rs new file mode 100644 index 0000000..1c70cd2 --- /dev/null +++ b/moonglass-node/src/follower/anchor.rs @@ -0,0 +1,81 @@ +//! Anchoring a follower at a configured genesis and checkpoint. +//! +//! [`load_context`] reads the launcher `config.yaml` and the genesis validators +//! root into the chain configuration and root the engine needs, without decoding +//! the full genesis state. +//! [`adopt_checkpoint`] then builds a [`FollowEngine`] from a finalized +//! checkpoint, enforcing the single-live-fork rule before the store is seeded. + +use moonglass_core::containers::{BeaconBlock, BeaconState}; +use moonglass_core::error::ForkChoiceError; +use moonglass_core::primitives::Root; + +use crate::config::ChainConfig; +use crate::error::GenesisError; +use crate::genesis::{ensure_single_live_fork_anchor, read_genesis_validators_root}; + +use super::FollowEngine; + +/// The configuration context a follower anchors against. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AnchorContext { + /// Chain configuration parsed from the launcher `config.yaml`. + pub chain_config: ChainConfig, + /// Genesis validators root, used for fork digests and signing domains. + pub genesis_validators_root: Root, +} + +/// A failure while anchoring a follower. +#[derive(Debug, thiserror::Error)] +pub enum AnchorError { + /// The configuration or genesis state was invalid. + #[error(transparent)] + Genesis(#[from] GenesisError), + /// The checkpoint state belongs to a different chain than the configuration. + #[error("checkpoint genesis validators root {got:?} does not match configured {want:?}")] + GenesisValidatorsRootMismatch { + /// Root carried by the checkpoint state. + got: Root, + /// Root from the launcher configuration. + want: Root, + }, + /// The checkpoint could not seed the fork-choice store. + #[error(transparent)] + ForkChoice(#[from] ForkChoiceError), +} + +/// Parse the launcher configuration and genesis validators root into an [`AnchorContext`]. +/// Returns [`AnchorError::Genesis`] when configuration or genesis SSZ is invalid. +/// +/// The genesis state itself is not decoded, so a genesis predating the active +/// fork still yields a usable context for anchoring at a later checkpoint. +pub fn load_context(config_yaml: &[u8], genesis_ssz: &[u8]) -> Result { + let chain_config = ChainConfig::from_yaml_slice(config_yaml).map_err(GenesisError::from)?; + let genesis_validators_root = read_genesis_validators_root(genesis_ssz)?; + Ok(AnchorContext { + chain_config, + genesis_validators_root, + }) +} + +/// Build a [`FollowEngine`] anchored at a finalized checkpoint. +/// Returns [`AnchorError`] when the checkpoint is inactive, from another chain, or cannot seed the store. +pub fn adopt_checkpoint( + context: &AnchorContext, + checkpoint_state: &BeaconState, + checkpoint_block: &BeaconBlock, +) -> Result { + ensure_single_live_fork_anchor(&context.chain_config, checkpoint_state)?; + if checkpoint_state.genesis_validators_root != context.genesis_validators_root { + return Err(AnchorError::GenesisValidatorsRootMismatch { + got: checkpoint_state.genesis_validators_root, + want: context.genesis_validators_root, + }); + } + let engine = FollowEngine::new( + checkpoint_state, + checkpoint_block, + context.genesis_validators_root, + )?; + Ok(engine) +} diff --git a/moonglass-node/src/follower/clock.rs b/moonglass-node/src/follower/clock.rs new file mode 100644 index 0000000..dbfa8a7 --- /dev/null +++ b/moonglass-node/src/follower/clock.rs @@ -0,0 +1,48 @@ +//! Slot timing for a follower, sharing the fork-choice store's clock. +//! +//! The store keeps time in whole seconds and derives slots from the compile-time +//! [`SLOT_DURATION_MS`]. [`SlotClock`] uses that same constant and genesis time, +//! so the live loop decides when to tick and which slot a wall-clock time falls +//! in with the exact arithmetic the store uses internally, and the two can never +//! disagree. + +use moonglass_core::constants::SLOT_DURATION_MS; +use moonglass_core::fork_choice::helpers::seconds_to_milliseconds; + +/// Converts between Unix seconds and slot numbers for one genesis. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SlotClock { + /// Genesis time in Unix seconds. + pub genesis_time: u64, +} + +impl SlotClock { + /// A clock anchored at `genesis_time` (Unix seconds). + pub fn new(genesis_time: u64) -> Self { + Self { genesis_time } + } + + /// The slot that `unix_time` falls in. + pub fn slot_at(&self, unix_time: u64) -> u64 { + seconds_to_milliseconds(unix_time.saturating_sub(self.genesis_time)) / SLOT_DURATION_MS + } + + /// The Unix second at which `slot` opens. + pub fn slot_start_unix(&self, slot: u64) -> u64 { + self.genesis_time + .saturating_add(slot.saturating_mul(SLOT_DURATION_MS) / 1_000) + } + + /// The Unix second at which the slot after `unix_time` opens. + pub fn next_slot_start_unix(&self, unix_time: u64) -> u64 { + self.slot_start_unix(self.slot_at(unix_time).saturating_add(1)) + } +} + +/// Current Unix time in whole seconds. +pub fn unix_now() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|elapsed| elapsed.as_secs()) + .unwrap_or_default() +} diff --git a/moonglass-node/src/follower/codec.rs b/moonglass-node/src/follower/codec.rs new file mode 100644 index 0000000..f113239 --- /dev/null +++ b/moonglass-node/src/follower/codec.rs @@ -0,0 +1,50 @@ +//! Snappy decompression for gossip and replay-capture payloads. +//! +//! libp2p `ssz_snappy` gossip compresses its payloads with the raw snappy block +//! format, the same format used by reference fixtures and replay captures, so +//! the gossip and replay paths share [`decompress_raw`]. The length-prefixed +//! request/response protocols instead use the snappy frame format. That wire is +//! not yet driven by this crate, so [`decompress_frame`] is the entry point held +//! ready for it. + +use std::io::Read; + +use snap::raw::Decoder as RawDecoder; +use snap::read::FrameDecoder; + +/// A snappy decompression failure. +#[derive(Debug, thiserror::Error)] +pub enum CodecError { + /// The payload was not a valid snappy frame stream. + #[error("snappy frame decompression failed")] + SnappyFrame { + /// Snappy frame reader error. + source: std::io::Error, + }, + /// The payload was not a valid raw snappy block. + #[error("raw snappy decompression failed")] + SnappyRaw { + /// Raw snappy decoder error. + source: snap::Error, + }, +} + +/// Decompress a snappy frame payload (the length-prefixed request/response +/// `ssz_snappy` format). +/// Returns [`CodecError::SnappyFrame`] when `bytes` is not a valid frame stream. +pub fn decompress_frame(bytes: &[u8]) -> Result, CodecError> { + let mut out = Vec::new(); + FrameDecoder::new(bytes) + .read_to_end(&mut out) + .map_err(|source| CodecError::SnappyFrame { source })?; + Ok(out) +} + +/// Decompress a raw snappy block (the gossip, reference-fixture, and +/// replay-capture format). +/// Returns [`CodecError::SnappyRaw`] when `bytes` is not a valid raw block. +pub fn decompress_raw(bytes: &[u8]) -> Result, CodecError> { + RawDecoder::new() + .decompress_vec(bytes) + .map_err(|source| CodecError::SnappyRaw { source }) +} diff --git a/moonglass-node/src/follower/dispatch.rs b/moonglass-node/src/follower/dispatch.rs new file mode 100644 index 0000000..ecd8993 --- /dev/null +++ b/moonglass-node/src/follower/dispatch.rs @@ -0,0 +1,138 @@ +//! Topic classification and the gossip-to-fork-choice dispatch table. +//! +//! Each gossip topic maps to a [`GossipKind`], and [`FollowEngine::handle_gossip`] +//! decodes the SSZ payload and routes it to the matching fork-choice handler. +//! Bids and proposer preferences are decoded for shape only, since no +//! fork-choice handler consumes them, so they report [`GossipOutcome::LoggedOnly`]. + +use moonglass_core::containers::{ + DataColumnSidecar, PayloadAttestationMessage, SignedAggregateAndProof, SignedBeaconBlock, + SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedProposerPreferences, +}; +use moonglass_core::error::ForkChoiceError; +use moonglass_core::networking::{ + BEACON_AGGREGATE_AND_PROOF_TOPIC, BEACON_BLOCK_TOPIC, DATA_COLUMN_SIDECAR_TOPIC, + EXECUTION_PAYLOAD_BID_TOPIC, EXECUTION_PAYLOAD_TOPIC, PAYLOAD_ATTESTATION_MESSAGE_TOPIC, + PROPOSER_PREFERENCES_TOPIC, +}; +use moonglass_core::ssz::{Deserialize, DeserializeError}; + +use super::FollowEngine; + +/// The kind of consensus message carried on a gossip topic. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GossipKind { + /// A `SignedBeaconBlock`. + BeaconBlock, + /// A `SignedAggregateAndProof`. + AggregateAndProof, + /// A `SignedExecutionPayloadEnvelope`. + ExecutionPayload, + /// A `PayloadAttestationMessage`. + PayloadAttestation, + /// A `DataColumnSidecar` on a subnet. + DataColumnSidecar, + /// A `SignedExecutionPayloadBid`. + ExecutionPayloadBid, + /// A `SignedProposerPreferences`. + ProposerPreferences, +} + +/// What feeding one gossip message into the engine did. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GossipOutcome { + /// The message was decoded and applied to fork-choice state. + Applied, + /// The message was decoded but no fork-choice handler consumes it. + LoggedOnly, +} + +/// A gossip handling failure: a malformed payload or a fork-choice rejection. +#[derive(Debug, thiserror::Error)] +pub enum DispatchError { + /// The payload did not decode as the expected SSZ container. + #[error("ssz decode failed: {0}")] + Decode(#[from] DeserializeError), + /// Fork choice rejected the decoded message. + #[error(transparent)] + ForkChoice(#[from] ForkChoiceError), +} + +/// Classify a full gossip topic string into its [`GossipKind`]. +/// +/// Returns `None` for any topic the follower does not consume, so an unexpected +/// topic surfaces as a loud miss rather than a silent drop. +pub fn classify(topic: &str) -> Option { + // Topics are `/eth2///ssz_snappy`. + let name = topic.split('/').nth(3)?; + let kind = match name { + BEACON_BLOCK_TOPIC => GossipKind::BeaconBlock, + BEACON_AGGREGATE_AND_PROOF_TOPIC => GossipKind::AggregateAndProof, + EXECUTION_PAYLOAD_TOPIC => GossipKind::ExecutionPayload, + EXECUTION_PAYLOAD_BID_TOPIC => GossipKind::ExecutionPayloadBid, + PAYLOAD_ATTESTATION_MESSAGE_TOPIC => GossipKind::PayloadAttestation, + PROPOSER_PREFERENCES_TOPIC => GossipKind::ProposerPreferences, + _ if is_column_topic(name) => GossipKind::DataColumnSidecar, + _ => return None, + }; + Some(kind) +} + +/// Whether `name` is a subnet-suffixed column-sidecar topic name. +pub fn is_column_topic(name: &str) -> bool { + name.strip_prefix(DATA_COLUMN_SIDECAR_TOPIC) + .is_some_and(|rest| rest.starts_with('_')) +} + +impl FollowEngine { + /// Decode a gossip payload of `kind` and feed it to fork choice. A live + /// caller advances the clock to the message's arrival time with + /// [`FollowEngine::advance_to`] first, matching the replay path. + /// Returns [`DispatchError`] when decoding or fork-choice handling fails. + pub fn handle_gossip( + &mut self, + kind: GossipKind, + ssz_bytes: &[u8], + ) -> Result { + match kind { + GossipKind::BeaconBlock => { + let block = SignedBeaconBlock::deserialize(ssz_bytes)?; + // Import the block and replay the votes it carries together, so a + // block's embedded attestations reach fork choice rather than + // being dropped by bare on_block. + self.store_mut().on_block_with_embedded_messages(&block)?; + Ok(GossipOutcome::Applied) + } + GossipKind::AggregateAndProof => { + let aggregate = SignedAggregateAndProof::deserialize(ssz_bytes)?; + self.store_mut() + .on_attestation(&aggregate.message.aggregate, false)?; + Ok(GossipOutcome::Applied) + } + GossipKind::ExecutionPayload => { + let envelope = SignedExecutionPayloadEnvelope::deserialize(ssz_bytes)?; + self.store_mut().on_execution_payload_envelope(&envelope)?; + Ok(GossipOutcome::Applied) + } + GossipKind::PayloadAttestation => { + let message = PayloadAttestationMessage::deserialize(ssz_bytes)?; + self.store_mut() + .on_payload_attestation_message(&message, false)?; + Ok(GossipOutcome::Applied) + } + GossipKind::DataColumnSidecar => { + let sidecar = DataColumnSidecar::deserialize(ssz_bytes)?; + self.store_mut().record_data_column_sidecar(sidecar)?; + Ok(GossipOutcome::Applied) + } + GossipKind::ExecutionPayloadBid => { + let _bid = SignedExecutionPayloadBid::deserialize(ssz_bytes)?; + Ok(GossipOutcome::LoggedOnly) + } + GossipKind::ProposerPreferences => { + let _preferences = SignedProposerPreferences::deserialize(ssz_bytes)?; + Ok(GossipOutcome::LoggedOnly) + } + } + } +} diff --git a/moonglass-node/src/follower/mod.rs b/moonglass-node/src/follower/mod.rs new file mode 100644 index 0000000..0045928 --- /dev/null +++ b/moonglass-node/src/follower/mod.rs @@ -0,0 +1,73 @@ +//! Read-only devnet follower built on the consensus engine. +//! +//! The follower decodes gossip messages and feeds them through +//! [`moonglass_core::fork_choice`] to track the chain head. This module owns the +//! engine boundary: decode, topic dispatch, replay, and fork-choice updates. + +pub mod anchor; +pub mod clock; +pub mod codec; +pub mod dispatch; +pub mod replay; +pub mod session; +pub mod topics; + +use moonglass_core::containers::{BeaconBlock, BeaconState}; +use moonglass_core::error::ForkChoiceError; +use moonglass_core::fork_choice::{Store, get_forkchoice_store}; +use moonglass_core::primitives::Root; + +pub use moonglass_core::fork_choice::{ForkChoiceNode, PayloadStatus}; + +/// Owns a fork-choice [`Store`] and feeds it decoded gossip to track the head. +pub struct FollowEngine { + /// Local fork-choice store, updated by each gossip message and advanced in + /// time by [`Self::advance_to`]. + store: Store, + /// Genesis validators root, used for fork digests and signing domains. + genesis_validators_root: Root, +} + +impl FollowEngine { + /// Build an engine anchored at `anchor_state` and `anchor_block`. + /// Returns a [`ForkChoiceError`] when the anchor cannot seed the store. + pub fn new( + anchor_state: &BeaconState, + anchor_block: &BeaconBlock, + genesis_validators_root: Root, + ) -> Result { + let store = get_forkchoice_store(anchor_state, anchor_block)?; + Ok(Self { + store, + genesis_validators_root, + }) + } + + /// The genesis validators root, used for fork digests and signing domains. + pub fn genesis_validators_root(&self) -> Root { + self.genesis_validators_root + } + + /// Borrow the fork-choice store. + pub fn store(&self) -> &Store { + &self.store + } + + /// Mutably borrow the fork-choice store. + pub fn store_mut(&mut self) -> &mut Store { + &mut self.store + } + + /// Advance the store clock to `unix_time`, the ordering step a live caller + /// runs before handling a message timed at that moment. + /// Returns a [`ForkChoiceError`] when the tick cannot be applied. + pub fn advance_to(&mut self, unix_time: u64) -> Result<(), ForkChoiceError> { + self.store.on_tick(unix_time) + } + + /// Return the current head node selected by fork choice. + /// Returns a [`ForkChoiceError`] when head selection cannot complete. + pub fn get_head(&self) -> Result { + self.store.get_head() + } +} diff --git a/moonglass-node/src/follower/replay.rs b/moonglass-node/src/follower/replay.rs new file mode 100644 index 0000000..7e033dd --- /dev/null +++ b/moonglass-node/src/follower/replay.rs @@ -0,0 +1,139 @@ +//! Deterministic replay of a captured message stream: the engine's oracle. +//! +//! A [`Capture`] records the anchor plus an ordered stream of gossip messages +//! with their arrival times. [`drive`] replays them through a fresh +//! [`FollowEngine`] the way a live follower is meant to (advance the clock to +//! each message's arrival, handle it, check store invariants) and verifies the +//! resulting head, so a captured devnet session can be replayed offline to find +//! where a head diverges. + +use moonglass_core::containers::{BeaconBlock, BeaconState}; +use moonglass_core::error::{ForkChoiceError, StoreInvariant}; +use moonglass_core::primitives::{Root, Slot}; +use moonglass_core::ssz::{Deserialize, DeserializeError}; + +use super::dispatch::{DispatchError, GossipKind}; +use super::{FollowEngine, ForkChoiceNode, PayloadStatus}; + +/// One captured gossip message with the wall-clock time it arrived. +pub struct CapturedMessage { + /// Unix-seconds arrival time, used to advance the store clock first. + pub recv_unix_time: u64, + /// Message kind, selecting the decode type and fork-choice handler. + pub kind: GossipKind, + /// The raw (already snappy-decompressed) SSZ payload. + pub ssz_bytes: Vec, +} + +/// A replayable session: an anchor plus an ordered message stream. +pub struct Capture { + /// Raw SSZ of the anchor `BeaconState`. + pub anchor_state_ssz: Vec, + /// Raw SSZ of the anchor `BeaconBlock`. + pub anchor_block_ssz: Vec, + /// Genesis validators root for the anchor. + pub genesis_validators_root: Root, + /// Ordered captured messages. + pub messages: Vec, + /// Head block root expected after the full replay. + pub expected_head_root: Root, + /// Head slot expected after the full replay. + pub expected_head_slot: Slot, + /// Head payload branch expected after the full replay, checked when `Some`. + pub expected_head_payload_status: Option, +} + +/// A replay failure. +/// +/// Anchor SSZ decode failures arrive under [`Self::Decode`]. +/// Clock, anchor-seeding, and head failures arrive under [`Self::ForkChoice`]. +/// Fork-choice rejections while handling a message arrive under [`Self::Dispatch`]. +#[derive(Debug, thiserror::Error)] +pub enum ReplayError { + /// An anchor SSZ payload failed to decode. + #[error("anchor decode failed: {0}")] + Decode(#[from] DeserializeError), + /// A captured message failed to dispatch. + #[error(transparent)] + Dispatch(#[from] DispatchError), + /// A fork-choice operation failed. + #[error("fork choice failed: {0}")] + ForkChoice(#[from] ForkChoiceError), + /// The store left a broken invariant after a message. + #[error("store invariant broken")] + Invariant { + /// Store invariant reported by the core. + source: StoreInvariant, + }, + /// The replayed head did not match the captured expectation. + #[error( + "head mismatch: expected root {expected_root:?} slot {expected_slot:?}, got root {got_root:?} slot {got_slot:?}" + )] + HeadMismatch { + /// Expected head block root. + expected_root: Root, + /// Expected head slot. + expected_slot: Slot, + /// Actual head block root. + got_root: Root, + /// Actual head slot, looked up from the head root. + got_slot: Option, + }, + /// The head was the expected block but on the wrong payload branch. + #[error("head payload status mismatch: expected {expected:?}, got {got:?}")] + HeadPayloadStatusMismatch { + /// Expected payload branch. + expected: PayloadStatus, + /// Actual payload branch. + got: PayloadStatus, + }, +} + +/// Replay `capture` through a fresh engine and verify the resulting head. +/// +/// Each message advances the store clock to its arrival time, is handled, and +/// leaves the store invariants intact, mirroring the fork-choice reference +/// runner. +/// Returns [`ReplayError`] when replay cannot reproduce the expected head. +pub fn drive(capture: &Capture) -> Result { + let anchor_state = BeaconState::deserialize(&capture.anchor_state_ssz)?; + let anchor_block = BeaconBlock::deserialize(&capture.anchor_block_ssz)?; + let mut engine = FollowEngine::new( + &anchor_state, + &anchor_block, + capture.genesis_validators_root, + )?; + + for message in &capture.messages { + engine.advance_to(message.recv_unix_time)?; + engine.handle_gossip(message.kind, &message.ssz_bytes)?; + engine + .store() + .check_invariants() + .map_err(|source| ReplayError::Invariant { source })?; + } + + let head = engine.get_head()?; + let head_slot = engine + .store() + .blocks + .get(&head.root) + .map(|block| block.slot); + if head.root != capture.expected_head_root || head_slot != Some(capture.expected_head_slot) { + return Err(ReplayError::HeadMismatch { + expected_root: capture.expected_head_root, + expected_slot: capture.expected_head_slot, + got_root: head.root, + got_slot: head_slot, + }); + } + if let Some(expected_status) = capture.expected_head_payload_status + && head.payload_status != expected_status + { + return Err(ReplayError::HeadPayloadStatusMismatch { + expected: expected_status, + got: head.payload_status, + }); + } + Ok(head) +} diff --git a/moonglass-node/src/follower/session.rs b/moonglass-node/src/follower/session.rs new file mode 100644 index 0000000..bc63042 --- /dev/null +++ b/moonglass-node/src/follower/session.rs @@ -0,0 +1,26 @@ +//! Static configuration for a live follower session. +//! +//! [`FollowerConfig`] mirrors the positional command line of the runnable +//! follower: where to find the chain configuration and genesis state, which +//! consensus client to anchor from, where to listen, which discovery port to +//! bind, and which peers to dial. The fields match the launch inputs read once +//! at start. + +use std::path::PathBuf; + +/// Inputs a follower reads once when it starts. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FollowerConfig { + /// Path to the launcher `config.yaml`. + pub config_yaml_path: PathBuf, + /// Path to the genesis state SSZ. + pub genesis_ssz_path: PathBuf, + /// Base URL of the consensus client REST endpoint to anchor from. + pub cl_url: String, + /// Address to listen on for peer connections. + pub listen: String, + /// UDP port for discv5 discovery. + pub udp_port: u16, + /// Bootnode ENRs or multiaddrs to dial. + pub bootnodes: Vec, +} diff --git a/moonglass-node/src/follower/topics.rs b/moonglass-node/src/follower/topics.rs new file mode 100644 index 0000000..0cac2aa --- /dev/null +++ b/moonglass-node/src/follower/topics.rs @@ -0,0 +1,91 @@ +//! The gossip topic set a follower subscribes to. +//! +//! The fixed consensus topics and all column-sidecar subnets are derived from +//! the configured digest for the epoch. Subscribing all column subnets matches +//! the core store's full data-availability gate for blob-bearing payloads. + +use std::collections::BTreeSet; + +use moonglass_core::constants::{DATA_COLUMN_SIDECAR_SUBNET_COUNT, NUMBER_OF_COLUMNS}; +use moonglass_core::containers::compute_subnet_for_data_column_sidecar; +use moonglass_core::error::TransitionError; +use moonglass_core::networking::{ + beacon_aggregate_and_proof_topic, beacon_block_topic, data_column_sidecar_topic, + execution_payload_bid_topic, execution_payload_topic, payload_attestation_message_topic, + proposer_preferences_topic, +}; +use moonglass_core::primitives::{ColumnIndex, Epoch, ForkDigest, Root, SubnetId}; + +use crate::config::ChainConfig; + +/// The gossip topics a follower subscribes to for one fork digest. +pub struct TopicTable { + /// Full gossip topic strings to subscribe to. + pub subscribe: Vec, +} + +impl TopicTable { + /// Build the subscribe set from a configured epoch digest. + /// Returns a [`TransitionError`] when the fork digest cannot be computed. + pub fn for_config( + chain_config: &ChainConfig, + genesis_validators_root: Root, + epoch: Epoch, + ) -> Result { + let fork_digest = chain_config.compute_fork_digest(genesis_validators_root, epoch)?; + Ok(Self::for_fork_digest(fork_digest)) + } + + /// Build the full devnet-ready subscribe set for a known fork digest. + pub fn for_fork_digest(fork_digest: ForkDigest) -> Self { + let mut subscribe = fixed_topics(fork_digest); + subscribe.extend(all_column_subnet_topics(fork_digest)); + Self { subscribe } + } + + /// Build a custody-only subscribe set for experiments that do not need full DA. + pub fn for_custody_columns(fork_digest: ForkDigest, custody_columns: &[ColumnIndex]) -> Self { + let mut subscribe = fixed_topics(fork_digest); + subscribe.extend(custody_column_subnet_topics(fork_digest, custody_columns)); + Self { subscribe } + } +} + +/// Fixed consensus topics every follower subscribes to. +pub fn fixed_topics(fork_digest: ForkDigest) -> Vec { + vec![ + beacon_block_topic(fork_digest), + beacon_aggregate_and_proof_topic(fork_digest), + execution_payload_topic(fork_digest), + execution_payload_bid_topic(fork_digest), + payload_attestation_message_topic(fork_digest), + proposer_preferences_topic(fork_digest), + ] +} + +/// Column-sidecar topics for every subnet required by full data availability. +pub fn all_column_subnet_topics(fork_digest: ForkDigest) -> Vec { + (0..DATA_COLUMN_SIDECAR_SUBNET_COUNT) + .map(|subnet| data_column_sidecar_topic(fork_digest, SubnetId::new(subnet))) + .collect() +} + +/// Column-sidecar topics for a supplied custody column set. +/// +/// Columns at or beyond `NUMBER_OF_COLUMNS` are out of range and ignored. +pub fn custody_column_subnet_topics( + fork_digest: ForkDigest, + custody_columns: &[ColumnIndex], +) -> Vec { + let subnets: BTreeSet = custody_columns + .iter() + .copied() + .filter(|column| column.as_u64() < NUMBER_OF_COLUMNS as u64) + .map(|column| compute_subnet_for_data_column_sidecar(column).as_u64()) + .collect(); + + subnets + .into_iter() + .map(|subnet| data_column_sidecar_topic(fork_digest, SubnetId::new(subnet))) + .collect() +} diff --git a/moonglass-node/src/genesis.rs b/moonglass-node/src/genesis.rs new file mode 100644 index 0000000..d324cad --- /dev/null +++ b/moonglass-node/src/genesis.rs @@ -0,0 +1,113 @@ +//! Genesis bundle consumed by externally driven core integrations. + +use moonglass_core::ssz::{Deserialize as _, Merkleized as _}; + +use moonglass_core::constants::GENESIS_EPOCH; +use moonglass_core::containers::BeaconState; +use moonglass_core::primitives::Root; + +use crate::config::ChainConfig; +use crate::error::GenesisError; + +/// Parsed consensus configuration plus decoded genesis state. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GenesisBundle { + /// Chain configuration read from `config.yaml`. + pub chain_config: ChainConfig, + /// Genesis state read from `genesis.ssz`. + pub genesis_state: BeaconState, + /// Root of the validator registry at genesis. + pub genesis_validators_root: Root, +} + +impl GenesisBundle { + /// Decode a consensus configuration and genesis state. + pub fn from_parts(config_yaml: &[u8], genesis_ssz: &[u8]) -> Result { + let chain_config = ChainConfig::from_yaml_slice(config_yaml)?; + let genesis_state = decode_genesis_state(genesis_ssz)?; + Self::from_config_and_state(chain_config, genesis_state) + } + + /// Build a bundle from already decoded pieces. + pub fn from_config_and_state( + chain_config: ChainConfig, + genesis_state: BeaconState, + ) -> Result { + let genesis_validators_root = compute_genesis_validators_root(&genesis_state)?; + if genesis_state.genesis_validators_root != genesis_validators_root { + return Err(GenesisError::GenesisValidatorsRootMismatch { + got: genesis_state.genesis_validators_root, + want: genesis_validators_root, + }); + } + Ok(Self { + chain_config, + genesis_state, + genesis_validators_root, + }) + } + + /// Check that the decoded state is inside the active single-fork range. + pub fn ensure_single_live_fork_anchor(&self) -> Result<(), GenesisError> { + ensure_single_live_fork_anchor(&self.chain_config, &self.genesis_state) + } + + /// Whether the decoded state satisfies the configured genesis trigger. + pub fn is_valid_genesis_state(&self) -> bool { + is_valid_genesis_state(&self.chain_config, &self.genesis_state) + } +} + +/// Decode a genesis state from SSZ bytes. +pub fn decode_genesis_state(genesis_ssz: &[u8]) -> Result { + BeaconState::deserialize(genesis_ssz).map_err(|source| GenesisError::Ssz { source }) +} + +/// Read the genesis validators root from a genesis state SSZ without decoding it. +/// +/// Every fork's `BeaconState` begins with `genesis_time` (8 bytes) followed by +/// `genesis_validators_root` (32 bytes), both fixed size, so the root sits at a +/// stable offset. This lets a follower read it from a genesis state predating +/// the active fork, which the full decoder cannot represent. +pub fn read_genesis_validators_root(genesis_ssz: &[u8]) -> Result { + let array: [u8; 32] = genesis_ssz + .get(8..40) + .and_then(|field| field.try_into().ok()) + .ok_or(GenesisError::GenesisStateTooShort)?; + Ok(Root(array)) +} + +/// Compute the validator-registry root recorded in the genesis state. +pub fn compute_genesis_validators_root(state: &BeaconState) -> Result { + state + .validators + .hash_tree_root() + .map(Root::from) + .map_err(|source| GenesisError::Merkleization { source }) +} + +/// Check that `state` is inside the active single-fork range. +pub fn ensure_single_live_fork_anchor( + chain_config: &ChainConfig, + state: &BeaconState, +) -> Result<(), GenesisError> { + let state_epoch = state.slot.epoch(); + let activation_epoch = chain_config.forks.gloas_epoch; + if state_epoch < activation_epoch { + return Err(GenesisError::SingleForkNotActive { + state_epoch, + activation_epoch, + }); + } + Ok(()) +} + +/// Whether `state` satisfies the configured genesis trigger. +pub fn is_valid_genesis_state(chain_config: &ChainConfig, state: &BeaconState) -> bool { + if state.genesis_time < chain_config.min_genesis_time { + return false; + } + let active_count = + u64::try_from(state.get_active_validator_indices(GENESIS_EPOCH).len()).unwrap_or(u64::MAX); + active_count >= chain_config.min_genesis_active_validator_count +} diff --git a/moonglass-node/src/lib.rs b/moonglass-node/src/lib.rs new file mode 100644 index 0000000..ad84fd4 --- /dev/null +++ b/moonglass-node/src/lib.rs @@ -0,0 +1,35 @@ +#![allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + +//! Devnet-facing wrapper around the plain moonglass consensus core. +//! +//! This crate owns launcher inputs such as consensus YAML and genesis SSZ +//! bundles, and a read-only devnet follower behind the `follower` feature. The +//! `node` feature adds a live libp2p and discv5 transport plus a runnable +//! binary that drives the follower against a real network. The core +//! `moonglass-core` crate remains a spec-shaped library selected by Cargo +//! features. + +#[cfg(not(any(feature = "mainnet", feature = "minimal")))] +compile_error!("crate must be built with exactly one of the `mainnet` or `minimal` features"); + +#[cfg(all(feature = "mainnet", feature = "minimal"))] +compile_error!( + "crate cannot be built with both `mainnet` and `minimal` features (cargo features are additive)" +); + +pub mod config; +pub mod error; +pub mod genesis; + +#[cfg(feature = "follower")] +pub mod follower; + +#[cfg(feature = "node")] +pub mod node; + +pub use config::{ + ACTIVE_PRESET, BlobSchedule, ChainConfig, ForkSchedule, MAINNET_PRESET_NAME, + MINIMAL_PRESET_NAME, NetworkConfig, PresetBase, TimingConfig, +}; +pub use error::{ConfigError, GenesisError}; +pub use genesis::{GenesisBundle, ensure_single_live_fork_anchor}; diff --git a/moonglass-node/src/node/api.rs b/moonglass-node/src/node/api.rs new file mode 100644 index 0000000..a6289eb --- /dev/null +++ b/moonglass-node/src/node/api.rs @@ -0,0 +1,276 @@ +//! A small read-only beacon REST API for endpoint-based explorers. +//! +//! The follow loop owns and mutates the fork-choice engine, so this server never +//! touches it directly. Instead the loop publishes an [`ApiSnapshot`] on a +//! [`tokio::sync::watch`] channel after each head computation, and the handlers +//! clone the latest snapshot to answer a request. The server therefore stays +//! lock-free against the engine and always serves a consistent view. +//! +//! Only the subset of endpoints an explorer needs to render the follower as a +//! client and track its head is served. Full block JSON, validator endpoints, +//! and the events stream are deferred. + +use std::net::{Ipv4Addr, SocketAddr}; + +use axum::Router; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; +use libp2p::PeerId; +use serde_json::{Value, json}; +use tokio::sync::watch; + +use moonglass_core::constants::{ + EPOCHS_PER_SYNC_COMMITTEE_PERIOD, MAX_COMMITTEES_PER_SLOT, MAX_VALIDATORS_PER_COMMITTEE, + SLOT_DURATION_MS, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT, SYNC_COMMITTEE_SIZE, +}; +use moonglass_core::primitives::{Root, Version}; + +use crate::config::ChainConfig; + +/// Zero-filled byte signature returned in place of a real block signature. +/// +/// The follower stores a [`BeaconBlock`](moonglass_core::containers::BeaconBlock) +/// in its fork-choice store and does not retain the proposer signature, so header +/// responses report an all-zero signature. +const ZERO_SIGNATURE_HEX: &str = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + +/// An owned, cheap-to-clone view of the chain state the endpoints report. +/// +/// The follow loop rebuilds this from the engine after each head computation and +/// publishes it on the watch channel. Every field is owned so a handler can clone +/// the whole snapshot and answer without borrowing the engine. +#[derive(Debug, Clone)] +pub struct ApiSnapshot { + /// Root of the current head block. + pub head_root: Root, + /// Slot of the head block. + pub head_slot: u64, + /// Proposer index of the head block. + pub head_proposer_index: u64, + /// Parent root of the head block. + pub head_parent_root: Root, + /// Post-state root of the head block. + pub head_state_root: Root, + /// Body root of the head block. + pub head_body_root: Root, + /// Epoch of the finalized checkpoint. + pub finalized_epoch: u64, + /// Root of the finalized checkpoint. + pub finalized_root: Root, + /// Epoch of the current justified checkpoint. + pub justified_epoch: u64, + /// Root of the current justified checkpoint. + pub justified_root: Root, + /// Genesis time in Unix seconds. + pub genesis_time: u64, + /// Genesis validators root. + pub genesis_validators_root: Root, + /// Fork version stamped on the genesis state. + pub genesis_fork_version: Version, + /// Current slot from the wall clock, used to derive the sync distance. + pub current_slot: u64, +} + +/// Static data shared by the handlers alongside the live snapshot receiver. +/// +/// These values never change for the life of the process, so they live beside +/// the watch receiver rather than inside each published snapshot. +#[derive(Clone)] +pub struct ApiState { + /// Receiver for the latest chain snapshot. + pub snapshot: watch::Receiver, + /// Version string reported by the node endpoints. + pub version: String, + /// Local libp2p peer identity. + pub peer_id: PeerId, + /// Chain configuration reported by the spec endpoint. + pub chain_config: ChainConfig, +} + +/// Serve the read-only beacon REST API on `0.0.0.0:port` until the task is dropped. +/// +/// Returns an [`std::io::Error`] when the listener cannot bind the port. +pub async fn serve(port: u16, state: ApiState) -> Result<(), std::io::Error> { + let app = Router::new() + .route("/eth/v1/node/version", get(node_version)) + .route("/eth/v1/node/syncing", get(node_syncing)) + .route("/eth/v1/node/identity", get(node_identity)) + .route("/eth/v1/beacon/genesis", get(beacon_genesis)) + .route("/eth/v1/config/spec", get(config_spec)) + .route("/eth/v1/beacon/headers", get(beacon_headers)) + .route("/eth/v1/beacon/headers/head", get(beacon_headers_head)) + .route( + "/eth/v1/beacon/states/:state_id/finality_checkpoints", + get(finality_checkpoints), + ) + .fallback(not_found) + .with_state(state); + + let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, port)); + let listener = tokio::net::TcpListener::bind(address).await?; + axum::serve(listener, app).await +} + +/// Lowercase `0x`-prefixed hex encoding of a fixed-size byte array. +fn hex(bytes: [u8; N]) -> String { + let mut out = String::with_capacity(2 + N * 2); + out.push_str("0x"); + for byte in bytes { + out.push(char::from_digit(u32::from(byte >> 4), 16).unwrap_or('0')); + out.push(char::from_digit(u32::from(byte & 0x0f), 16).unwrap_or('0')); + } + out +} + +/// Wrap a JSON value in a `200 OK` response with a JSON content type. +fn json_ok(value: Value) -> Response { + (StatusCode::OK, axum::Json(value)).into_response() +} + +/// The header object reported for the head block. +/// +/// The store keeps a block without a signature, so the signature field is a +/// zero-filled placeholder. +fn head_header(snapshot: &ApiSnapshot) -> Value { + json!({ + "root": hex(snapshot.head_root.0), + "canonical": true, + "header": { + "message": { + "slot": snapshot.head_slot.to_string(), + "proposer_index": snapshot.head_proposer_index.to_string(), + "parent_root": hex(snapshot.head_parent_root.0), + "state_root": hex(snapshot.head_state_root.0), + "body_root": hex(snapshot.head_body_root.0), + }, + "signature": ZERO_SIGNATURE_HEX, + }, + }) +} + +/// `GET /eth/v1/node/version`. +async fn node_version(State(state): State) -> Response { + json_ok(json!({ "data": { "version": state.version } })) +} + +/// `GET /eth/v1/node/syncing`. +async fn node_syncing(State(state): State) -> Response { + let snapshot = state.snapshot.borrow().clone(); + let sync_distance = snapshot.current_slot.saturating_sub(snapshot.head_slot); + json_ok(json!({ + "data": { + "head_slot": snapshot.head_slot.to_string(), + "sync_distance": sync_distance.to_string(), + "is_syncing": sync_distance > 0, + "is_optimistic": false, + "el_offline": true, + } + })) +} + +/// `GET /eth/v1/node/identity`. +async fn node_identity(State(state): State) -> Response { + json_ok(json!({ + "data": { + "peer_id": state.peer_id.to_string(), + "enr": "", + "p2p_addresses": [], + "discovery_addresses": [], + "metadata": { + "seq_number": "0", + "attnets": "0x0000000000000000", + "syncnets": "0x00", + }, + } + })) +} + +/// `GET /eth/v1/beacon/genesis`. +async fn beacon_genesis(State(state): State) -> Response { + let snapshot = state.snapshot.borrow().clone(); + json_ok(json!({ + "data": { + "genesis_time": snapshot.genesis_time.to_string(), + "genesis_validators_root": hex(snapshot.genesis_validators_root.0), + "genesis_fork_version": hex(snapshot.genesis_fork_version.0), + } + })) +} + +/// `GET /eth/v1/config/spec`. +/// +/// Reports the timing and limit constants an explorer reads to compute slot and +/// epoch geometry. At minimum the slot duration and epoch length are exact, since +/// explorers derive their clocks from them. +async fn config_spec(State(state): State) -> Response { + let config = &state.chain_config; + json_ok(json!({ + "data": { + "SECONDS_PER_SLOT": (SLOT_DURATION_MS / 1_000).to_string(), + "SLOTS_PER_EPOCH": SLOTS_PER_EPOCH.to_string(), + "SLOTS_PER_HISTORICAL_ROOT": SLOTS_PER_HISTORICAL_ROOT.to_string(), + "EPOCHS_PER_SYNC_COMMITTEE_PERIOD": EPOCHS_PER_SYNC_COMMITTEE_PERIOD.to_string(), + "SYNC_COMMITTEE_SIZE": SYNC_COMMITTEE_SIZE.to_string(), + "MAX_COMMITTEES_PER_SLOT": MAX_COMMITTEES_PER_SLOT.to_string(), + "MAX_VALIDATORS_PER_COMMITTEE": MAX_VALIDATORS_PER_COMMITTEE.to_string(), + "GENESIS_DELAY": config.timing.genesis_delay.to_string(), + "MIN_GENESIS_TIME": config.min_genesis_time.to_string(), + "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT": config.min_genesis_active_validator_count.to_string(), + "DEPOSIT_CHAIN_ID": config.network.deposit_chain_id.to_string(), + "DEPOSIT_NETWORK_ID": config.network.deposit_network_id.to_string(), + "DEPOSIT_CONTRACT_ADDRESS": hex(config.network.deposit_contract_address.0), + "GENESIS_FORK_VERSION": hex(config.forks.genesis_version.0), + } + })) +} + +/// `GET /eth/v1/beacon/headers`. +/// +/// The follower tracks one head, so the list always holds a single entry. +async fn beacon_headers(State(state): State) -> Response { + let snapshot = state.snapshot.borrow().clone(); + json_ok(json!({ "data": [head_header(&snapshot)] })) +} + +/// `GET /eth/v1/beacon/headers/head`. +async fn beacon_headers_head(State(state): State) -> Response { + let snapshot = state.snapshot.borrow().clone(); + json_ok(json!({ "data": head_header(&snapshot) })) +} + +/// `GET /eth/v1/beacon/states/:state_id/finality_checkpoints`. +/// +/// The store keeps a single justified checkpoint, so it is reported for both the +/// previous and current justified fields. The `state_id` is accepted but not +/// resolved, since the follower exposes only its current view. +async fn finality_checkpoints( + State(state): State, + Path(_state_id): Path, +) -> Response { + let snapshot = state.snapshot.borrow().clone(); + let justified = json!({ + "epoch": snapshot.justified_epoch.to_string(), + "root": hex(snapshot.justified_root.0), + }); + json_ok(json!({ + "data": { + "previous_justified": justified, + "current_justified": justified, + "finalized": { + "epoch": snapshot.finalized_epoch.to_string(), + "root": hex(snapshot.finalized_root.0), + }, + } + })) +} + +/// Fallback handler returning a JSON `404 Not Found` for any unserved path. +async fn not_found() -> Response { + ( + StatusCode::NOT_FOUND, + axum::Json(json!({ "code": 404, "message": "Not Found" })), + ) + .into_response() +} diff --git a/moonglass-node/src/node/discovery.rs b/moonglass-node/src/node/discovery.rs new file mode 100644 index 0000000..1af5e0e --- /dev/null +++ b/moonglass-node/src/node/discovery.rs @@ -0,0 +1,110 @@ +//! discv5 peer discovery feeding dial targets to the swarm. +//! +//! A fresh-identity discv5 service is seeded with bootnode ENRs, then a +//! background task periodically queries for peers and forwards each discovered +//! node's TCP endpoint as a libp2p dial target over a channel, preferring the +//! IPv4 endpoint and falling back to IPv6. The address is sent without a peer +//! id, so libp2p learns the identity during the handshake. + +use std::net::Ipv4Addr; +use std::time::Duration; + +use discv5::enr::{CombinedKey, NodeId}; +use discv5::{ConfigBuilder, Discv5, Enr, ListenConfig}; +use libp2p::Multiaddr; +use libp2p::multiaddr::Protocol; +use tokio::sync::mpsc; + +/// Inputs for the discovery service. +pub struct DiscoveryConfig { + /// UDP port discv5 listens on. + pub udp_port: u16, + /// SSZ `ENRForkID` advertised in the local ENR `eth2` field, so consensus + /// peers see a matching fork digest. + pub eth2_field: Vec, + /// Bootnode ENR strings used to seed the routing table. + pub bootnodes: Vec, +} + +/// A discovery setup failure. +#[derive(Debug, thiserror::Error)] +pub enum DiscoveryError { + /// The local ENR or discv5 service could not be built or started. + #[error("discv5 setup failed: {0}")] + Setup(String), + /// A bootnode ENR string was invalid or could not be added. + #[error("invalid bootnode enr: {0}")] + Bootnode(String), +} + +/// Start discv5 and return a receiver of dial targets it discovers. +pub async fn spawn(config: DiscoveryConfig) -> Result, DiscoveryError> { + let key = CombinedKey::generate_secp256k1(); + let local_enr = Enr::builder() + .add_value("eth2", &config.eth2_field) + .build(&key) + .map_err(|source| DiscoveryError::Setup(format!("{source:?}")))?; + let listen = ListenConfig::Ipv4 { + ip: Ipv4Addr::UNSPECIFIED, + port: config.udp_port, + }; + let discv5_config = ConfigBuilder::new(listen).build(); + let mut discv5 = Discv5::new(local_enr, key, discv5_config) + .map_err(|source| DiscoveryError::Setup(source.to_string()))?; + + for bootnode in &config.bootnodes { + let enr: Enr = bootnode + .parse() + .map_err(|source| DiscoveryError::Bootnode(format!("{source:?}")))?; + discv5 + .add_enr(enr) + .map_err(|source| DiscoveryError::Bootnode(source.to_string()))?; + } + + discv5 + .start() + .await + .map_err(|source| DiscoveryError::Setup(source.to_string()))?; + + let (sender, receiver) = mpsc::channel(64); + tokio::spawn(query_loop(discv5, sender)); + Ok(receiver) +} + +/// Periodically query discv5 and forward discovered dial targets. +async fn query_loop(discv5: Discv5, sender: mpsc::Sender) { + let mut ticker = tokio::time::interval(Duration::from_secs(30)); + loop { + ticker.tick().await; + match discv5.find_node(NodeId::random()).await { + Ok(peers) => { + for enr in peers { + if let Some(address) = enr_to_multiaddr(&enr) + && sender.send(address).await.is_err() + { + return; + } + } + } + Err(error) => tracing::debug!(?error, "discv5 query failed"), + } + } +} + +/// Convert an ENR's TCP endpoint into a libp2p dial address. +/// +/// Prefers the IPv4 endpoint and falls back to IPv6 when no IPv4 endpoint is +/// advertised. A node that advertises neither yields no dial target. +fn enr_to_multiaddr(enr: &Enr) -> Option { + let mut address = Multiaddr::empty(); + if let (Some(ip), Some(port)) = (enr.ip4(), enr.tcp4()) { + address.push(Protocol::Ip4(ip)); + address.push(Protocol::Tcp(port)); + } else if let (Some(ip), Some(port)) = (enr.ip6(), enr.tcp6()) { + address.push(Protocol::Ip6(ip)); + address.push(Protocol::Tcp(port)); + } else { + return None; + } + Some(address) +} diff --git a/moonglass-node/src/node/mod.rs b/moonglass-node/src/node/mod.rs new file mode 100644 index 0000000..d30c071 --- /dev/null +++ b/moonglass-node/src/node/mod.rs @@ -0,0 +1,13 @@ +//! Live devnet transport for the follower, behind the `node` feature. +//! +//! This wires the follower engine seam to a real network: a libp2p swarm +//! subscribes to the consensus gossip topics, and each inbound message is +//! decompressed, classified, and fed to fork choice while a slot clock advances +//! the store. The transport adds the heavy async dependencies (libp2p, tokio) +//! that the default crate avoids. + +pub mod api; +pub mod discovery; +pub mod network; +pub mod reqresp; +pub mod run; diff --git a/moonglass-node/src/node/network.rs b/moonglass-node/src/node/network.rs new file mode 100644 index 0000000..67906c7 --- /dev/null +++ b/moonglass-node/src/node/network.rs @@ -0,0 +1,139 @@ +//! The libp2p swarm and gossip behaviour the follower runs. +//! +//! Gossip uses anonymous message authenticity, matching consensus gossip, which +//! signs at the application layer rather than the libp2p layer. The gossipsub +//! configuration mirrors the consensus network: a large transmit limit, a +//! content-addressed message id over the decompressed payload, and anonymous +//! validation, so the follower agrees with real nodes on message identity. + +pub use behaviour::{Behaviour, BehaviourEvent}; + +use std::time::Duration; + +use sha2::{Digest, Sha256}; + +use libp2p::{ + StreamProtocol, Swarm, SwarmBuilder, gossipsub, identify, noise, ping, request_response, tcp, + yamux, +}; + +use moonglass_core::networking::BEACON_BLOCKS_BY_RANGE_V2_PROTOCOL_ID; + +use crate::follower::codec::decompress_raw; +use crate::node::reqresp::BlocksByRangeCodec; + +/// Domain mixed into the message id for a valid snappy payload. +const MESSAGE_DOMAIN_VALID_SNAPPY: [u8; 4] = [0x01, 0x00, 0x00, 0x00]; +/// Domain mixed into the message id for an undecodable snappy payload. +const MESSAGE_DOMAIN_INVALID_SNAPPY: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; +/// Maximum gossip payload size accepted, matching the consensus gossip limit. +const GOSSIP_MAX_SIZE: usize = 10 * 1024 * 1024; + +/// The `NetworkBehaviour` derive generates an event enum whose variants cannot +/// carry doc comments, so it is isolated here behind a documentation-lint allow +/// while the rest of the module stays fully linted. +mod behaviour { + #![allow(missing_docs, clippy::missing_docs_in_private_items)] + + use libp2p::swarm::NetworkBehaviour; + use libp2p::{gossipsub, identify, ping, request_response}; + + use crate::node::reqresp::BlocksByRangeCodec; + + /// The network behaviours the follower runs. + #[derive(NetworkBehaviour)] + pub struct Behaviour { + /// Gossipsub, carrying the consensus topic subscriptions. + pub gossipsub: gossipsub::Behaviour, + /// Outbound block range backfill over request/response. + pub blocks_by_range: request_response::Behaviour, + /// Peer identification, which consensus peers expect before engaging. + pub identify: identify::Behaviour, + /// Liveness pings that keep an otherwise idle connection open. + pub ping: ping::Behaviour, + } +} + +/// A failure building the network stack. +#[derive(Debug, thiserror::Error)] +pub enum NetworkError { + /// Transport construction failed. + #[error("transport setup failed: {0}")] + Transport(String), + /// Gossipsub construction failed. + #[error("gossipsub setup failed: {0}")] + Gossipsub(String), +} + +/// The consensus gossip message id: the first twenty bytes of a SHA256 over a +/// domain, the topic, and the snappy-decompressed payload. +/// +/// Two nodes derive the same id for the same payload even though gossip is +/// anonymous, which is how gossipsub deduplicates consensus messages. +fn message_id(message: &gossipsub::Message) -> gossipsub::MessageId { + let topic = message.topic.as_str(); + let (domain, payload) = if let Ok(decompressed) = decompress_raw(&message.data) { + (MESSAGE_DOMAIN_VALID_SNAPPY, decompressed) + } else { + (MESSAGE_DOMAIN_INVALID_SNAPPY, message.data.clone()) + }; + let mut hasher = Sha256::new(); + hasher.update(domain); + hasher.update((topic.len() as u64).to_le_bytes()); + hasher.update(topic.as_bytes()); + hasher.update(&payload); + let digest = hasher.finalize(); + gossipsub::MessageId::from(&digest[..20]) +} + +/// Build a tokio-backed swarm with tcp, noise, yamux, and consensus gossipsub. +pub fn build_swarm() -> Result, NetworkError> { + let swarm = SwarmBuilder::with_new_identity() + .with_tokio() + .with_tcp( + tcp::Config::default(), + noise::Config::new, + yamux::Config::default, + ) + .map_err(|source| NetworkError::Transport(source.to_string()))? + .with_behaviour(|key| { + let config = gossipsub::ConfigBuilder::default() + .max_transmit_size(GOSSIP_MAX_SIZE) + .validation_mode(gossipsub::ValidationMode::Anonymous) + .message_id_fn(message_id) + .mesh_n(8) + .mesh_n_low(6) + .mesh_n_high(12) + .heartbeat_interval(Duration::from_millis(700)) + .history_length(6) + .history_gossip(3) + .build() + .map_err(|source| NetworkError::Gossipsub(source.to_string()))?; + let gossipsub = + gossipsub::Behaviour::new(gossipsub::MessageAuthenticity::Anonymous, config) + .map_err(|source| NetworkError::Gossipsub(source.to_string()))?; + let blocks_by_range = request_response::Behaviour::with_codec( + BlocksByRangeCodec, + std::iter::once(( + StreamProtocol::new(BEACON_BLOCKS_BY_RANGE_V2_PROTOCOL_ID), + request_response::ProtocolSupport::Outbound, + )), + request_response::Config::default(), + ); + let identify = identify::Behaviour::new( + identify::Config::new("eth2/1.0.0".to_owned(), key.public()).with_agent_version( + concat!("moonglass/", env!("CARGO_PKG_VERSION")).to_owned(), + ), + ); + let ping = ping::Behaviour::default(); + Ok(Behaviour { + gossipsub, + blocks_by_range, + identify, + ping, + }) + }) + .map_err(|source| NetworkError::Transport(source.to_string()))? + .build(); + Ok(swarm) +} diff --git a/moonglass-node/src/node/reqresp.rs b/moonglass-node/src/node/reqresp.rs new file mode 100644 index 0000000..f4eb7cf --- /dev/null +++ b/moonglass-node/src/node/reqresp.rs @@ -0,0 +1,271 @@ +//! Outbound `beacon_blocks_by_range` request/response client. +//! +//! The follower anchors at a finalized checkpoint, so the blocks between that +//! anchor and the gossip head are missing and gossiped blocks cannot connect to +//! the store. This module fetches that range over the consensus request/response +//! protocol so the head can walk forward to the live tip without shadowing a +//! consensus client over HTTP. +//! +//! Only the outbound side is implemented: the follower sends a range request and +//! reads the streamed response. The wire form per the consensus p2p interface is +//! a varint of the uncompressed length followed by snappy frame bytes for the +//! request, and a stream of chunks for the response, each a one byte result code, +//! a four byte fork digest context, a varint length, and snappy frame bytes. + +use std::io; +use std::io::Read as _; + +use async_trait::async_trait; +use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use libp2p::StreamProtocol; +use libp2p::request_response::Codec; +use snap::read::FrameDecoder; +use snap::write::FrameEncoder; + +/// Largest response stream accepted, bounding memory for a hostile peer. +const MAX_RESPONSE_BYTES: u64 = 64 * 1024 * 1024; +/// Largest single decompressed block accepted. +const MAX_CHUNK_BYTES: usize = 16 * 1024 * 1024; +/// Length of the fork digest context preceding each success chunk. +const CONTEXT_BYTES: usize = 4; +/// Success result code that introduces a block chunk. +const RESULT_SUCCESS: u8 = 0; +/// Deprecated block range stride required by the consensus request container. +const BLOCKS_BY_RANGE_STEP: u64 = 1; + +/// A request for a contiguous run of blocks starting at a slot. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlocksByRangeRequest { + /// First slot requested. + pub start_slot: u64, + /// Number of slots requested. + pub count: u64, + /// Deprecated stride field. Consensus peers expect this to be `1`. + pub step: u64, +} + +impl BlocksByRangeRequest { + /// Build a spec-shaped contiguous range request. + pub fn new(start_slot: u64, count: u64) -> Self { + Self { + start_slot, + count, + step: BLOCKS_BY_RANGE_STEP, + } + } +} + +/// Raw SSZ `SignedBeaconBlock` payloads returned in slot order. +pub type BlocksByRangeResponse = Vec>; + +/// Codec for the outbound `beacon_blocks_by_range` protocol. +#[derive(Debug, Clone, Default)] +pub struct BlocksByRangeCodec; + +#[async_trait] +impl Codec for BlocksByRangeCodec { + type Protocol = StreamProtocol; + type Request = BlocksByRangeRequest; + type Response = BlocksByRangeResponse; + + async fn read_request( + &mut self, + _protocol: &StreamProtocol, + _io: &mut T, + ) -> io::Result + where + T: AsyncRead + Unpin + Send, + { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "follower does not serve range requests", + )) + } + + async fn write_request( + &mut self, + _protocol: &StreamProtocol, + io: &mut T, + request: Self::Request, + ) -> io::Result<()> + where + T: AsyncWrite + Unpin + Send, + { + let mut ssz = Vec::with_capacity(24); + ssz.extend_from_slice(&request.start_slot.to_le_bytes()); + ssz.extend_from_slice(&request.count.to_le_bytes()); + ssz.extend_from_slice(&request.step.to_le_bytes()); + + let mut framed = Vec::new(); + write_varint(&mut framed, ssz.len() as u64); + framed.extend_from_slice(&snappy_frame_compress(&ssz)?); + io.write_all(&framed).await?; + io.close().await?; + Ok(()) + } + + async fn read_response( + &mut self, + _protocol: &StreamProtocol, + io: &mut T, + ) -> io::Result + where + T: AsyncRead + Unpin + Send, + { + let mut buffer = Vec::new(); + io.take(MAX_RESPONSE_BYTES).read_to_end(&mut buffer).await?; + let blocks = parse_chunks(&buffer)?; + tracing::debug!( + bytes = buffer.len(), + result = ?buffer.first(), + blocks = blocks.len(), + "blocks_by_range response" + ); + Ok(blocks) + } + + async fn write_response( + &mut self, + _protocol: &StreamProtocol, + _io: &mut T, + _response: Self::Response, + ) -> io::Result<()> + where + T: AsyncWrite + Unpin + Send, + { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "follower does not serve range responses", + )) + } +} + +/// Split a response stream into raw SSZ block payloads. +/// +/// Parsing stops at the first non success chunk and returns the blocks gathered +/// so far, so a peer that serves a partial range still makes progress. +fn parse_chunks(buffer: &[u8]) -> io::Result { + let mut blocks = Vec::new(); + let mut cursor = 0; + while cursor < buffer.len() { + let result = buffer[cursor]; + cursor += 1; + if result != RESULT_SUCCESS { + break; + } + if cursor + CONTEXT_BYTES > buffer.len() { + break; + } + cursor += CONTEXT_BYTES; + let (length, varint_len) = read_varint(&buffer[cursor..])?; + cursor += varint_len; + let length = usize::try_from(length) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "chunk length overflow"))?; + if length > MAX_CHUNK_BYTES { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "chunk exceeds the accepted size", + )); + } + let (block, consumed) = snappy_frame_prefix(&buffer[cursor..], length)?; + cursor += consumed; + blocks.push(block); + } + Ok(blocks) +} + +/// Snappy frame compress `data`. +fn snappy_frame_compress(data: &[u8]) -> io::Result> { + use std::io::Write as _; + let mut encoder = FrameEncoder::new(Vec::new()); + encoder.write_all(data)?; + encoder + .into_inner() + .map_err(|error| io::Error::other(error.to_string())) +} + +/// Decompress exactly `length` bytes of snappy frame data from the front of +/// `data`, returning the bytes and the number of compressed bytes consumed. +fn snappy_frame_prefix(data: &[u8], length: usize) -> io::Result<(Vec, usize)> { + let mut reader = io::Cursor::new(data); + let mut out = vec![0_u8; length]; + { + let mut decoder = FrameDecoder::new(&mut reader); + decoder.read_exact(&mut out)?; + } + let consumed = usize::try_from(reader.position()) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "chunk offset overflow"))?; + Ok((out, consumed)) +} + +/// Append `value` as an unsigned LEB128 varint. +fn write_varint(out: &mut Vec, mut value: u64) { + loop { + let mut byte = (value & 0x7f) as u8; + value >>= 7; + if value != 0 { + byte |= 0x80; + } + out.push(byte); + if value == 0 { + break; + } + } +} + +/// Read an unsigned LEB128 varint, returning the value and bytes consumed. +fn read_varint(buffer: &[u8]) -> io::Result<(u64, usize)> { + let mut value = 0_u64; + let mut shift = 0_u32; + for (index, &byte) in buffer.iter().enumerate() { + if shift >= 64 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "varint too long", + )); + } + value |= u64::from(byte & 0x7f) << shift; + if byte & 0x80 == 0 { + return Ok((value, index + 1)); + } + shift += 7; + } + Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "varint is incomplete", + )) +} + +#[cfg(test)] +mod tests { + use futures::io::Cursor; + use libp2p::StreamProtocol; + + use super::*; + + #[tokio::test] + async fn write_request_encodes_spec_container_with_step() { + let mut codec = BlocksByRangeCodec; + let mut io = Cursor::new(Vec::new()); + codec + .write_request( + &StreamProtocol::new("/eth2/beacon_chain/req/beacon_blocks_by_range/2/ssz_snappy"), + &mut io, + BlocksByRangeRequest::new(12, 34), + ) + .await + .expect("write request"); + + let wire = io.into_inner(); + let (length, offset) = read_varint(&wire).expect("request length"); + assert_eq!(length, 24); + + let length = usize::try_from(length).expect("test request length fits usize"); + let (ssz, consumed) = + snappy_frame_prefix(&wire[offset..], length).expect("request payload"); + assert_eq!(offset + consumed, wire.len()); + assert_eq!(&ssz[0..8], &12_u64.to_le_bytes()); + assert_eq!(&ssz[8..16], &34_u64.to_le_bytes()); + assert_eq!(&ssz[16..24], &BLOCKS_BY_RANGE_STEP.to_le_bytes()); + } +} diff --git a/moonglass-node/src/node/run.rs b/moonglass-node/src/node/run.rs new file mode 100644 index 0000000..e9a024d --- /dev/null +++ b/moonglass-node/src/node/run.rs @@ -0,0 +1,399 @@ +//! The live follow loop: subscribe to gossip and feed messages to fork choice. +//! +//! The loop advances the store clock once per second and routes every inbound +//! gossip message through the follower seam (decompress, classify, dispatch), +//! logging the head as it moves. It runs until the process is stopped. + +use std::time::Duration; + +use futures::StreamExt; +use libp2p::gossipsub::IdentTopic; +use libp2p::swarm::SwarmEvent; +use libp2p::{Multiaddr, PeerId, Swarm, gossipsub, request_response}; +use tokio::sync::{mpsc, watch}; + +use moonglass_core::containers::BeaconBlock; +use moonglass_core::primitives::{Root, Version}; +use moonglass_core::ssz::Merkleized; + +use crate::config::ChainConfig; +use crate::follower::FollowEngine; +use crate::follower::clock::{self, SlotClock}; +use crate::follower::codec::decompress_raw; +use crate::follower::dispatch::{GossipKind, GossipOutcome, classify}; +use crate::follower::topics::TopicTable; + +use super::api::{self, ApiSnapshot, ApiState}; +use super::network::{Behaviour, BehaviourEvent, NetworkError, build_swarm}; +use super::reqresp::BlocksByRangeRequest; + +/// A failure starting or running the live follow loop. +#[derive(Debug, thiserror::Error)] +pub enum RunError { + /// The network stack could not be built. + #[error(transparent)] + Network(#[from] NetworkError), + /// A swarm listen or dial address was rejected. + #[error("swarm transport error: {0}")] + Transport(String), + /// A gossip topic subscription failed. + #[error("gossip subscription failed: {0}")] + Subscribe(String), +} + +/// Where the follower listens, what it subscribes to, and how it finds peers. +pub struct RunConfig { + /// Address the swarm listens on. + pub listen: Multiaddr, + /// Gossip topics to subscribe to. + pub topics: TopicTable, + /// Dial targets discovered by discv5. + pub discovered: mpsc::Receiver, + /// Port the read-only beacon REST API binds on. + pub api_port: u16, + /// Chain configuration reported by the spec endpoint. + pub chain_config: ChainConfig, +} + +/// Run the follow loop until the process is stopped. +/// +/// Subscribes to the configured gossip topics, advances the store clock once per +/// second, and feeds every inbound gossip message through fork choice, logging +/// the head as it moves. +pub async fn run(mut engine: FollowEngine, config: RunConfig) -> Result<(), RunError> { + let RunConfig { + listen, + topics, + mut discovered, + api_port, + chain_config, + } = config; + let clock = SlotClock::new(engine.store().genesis_time); + let genesis_fork_version = chain_config.forks.genesis_version; + let mut swarm = build_swarm()?; + + let snapshot_tx = spawn_api( + &engine, + clock, + genesis_fork_version, + api_port, + *swarm.local_peer_id(), + chain_config, + ); + + for topic in &topics.subscribe { + swarm + .behaviour_mut() + .gossipsub + .subscribe(&IdentTopic::new(topic.clone())) + .map_err(|source| RunError::Subscribe(format!("{source:?}")))?; + } + + swarm + .listen_on(listen) + .map_err(|source| RunError::Transport(source.to_string()))?; + + let mut ticker = tokio::time::interval(Duration::from_secs(1)); + let mut backfill = BackfillState::default(); + loop { + tokio::select! { + event = swarm.select_next_some() => { + match event { + SwarmEvent::Behaviour(BehaviourEvent::Gossipsub( + gossipsub::Event::Message { message, .. }, + )) => { + handle_message(&mut engine, message.topic.as_str(), &message.data); + snapshot_tx.send_replace(build_snapshot(&engine, clock, genesis_fork_version)); + } + SwarmEvent::Behaviour(BehaviourEvent::BlocksByRange( + request_response::Event::Message { + peer, + message: request_response::Message::Response { response, .. }, + }, + )) => { + backfill.complete(&peer); + let applied = apply_backfill(&mut engine, &response); + tracing::info!(received = response.len(), applied, "backfill applied"); + if !response.is_empty() && applied == 0 { + backfill.remove_peer(&peer); + } + snapshot_tx.send_replace(build_snapshot(&engine, clock, genesis_fork_version)); + if !response.is_empty() { + request_backfill(&mut swarm, &mut backfill, &engine, clock); + } + } + SwarmEvent::Behaviour(BehaviourEvent::BlocksByRange( + request_response::Event::OutboundFailure { peer, error, .. }, + )) => { + backfill.complete(&peer); + backfill.remove_peer(&peer); + tracing::warn!(?error, "backfill request failed"); + request_backfill(&mut swarm, &mut backfill, &engine, clock); + } + SwarmEvent::ConnectionEstablished { peer_id, .. } => { + backfill.add_peer(peer_id); + request_backfill(&mut swarm, &mut backfill, &engine, clock); + } + SwarmEvent::ConnectionClosed { + peer_id, + num_established: 0, + .. + } => { + backfill.remove_peer(&peer_id); + request_backfill(&mut swarm, &mut backfill, &engine, clock); + } + _ => {} + } + } + Some(address) = discovered.recv() => { + if let Err(error) = swarm.dial(address) { + tracing::debug!(%error, "dial failed"); + } + } + _ = ticker.tick() => { + if tick_engine(&mut engine, clock) { + snapshot_tx.send_replace(build_snapshot(&engine, clock, genesis_fork_version)); + request_backfill(&mut swarm, &mut backfill, &engine, clock); + } + } + } + } +} + +/// Advance the store clock for a timer tick and log the current head. +fn tick_engine(engine: &mut FollowEngine, clock: SlotClock) -> bool { + let now = clock::unix_now(); + if let Err(error) = engine.advance_to(now) { + tracing::warn!(%error, "clock advance failed"); + return false; + } + match engine.get_head() { + Ok(head) => tracing::info!(slot = clock.slot_at(now), head = ?head.root, "head"), + Err(error) => tracing::warn!(%error, "head selection failed"), + } + true +} + +/// Publish the initial chain view and spawn the read-only REST API. +fn spawn_api( + engine: &FollowEngine, + clock: SlotClock, + genesis_fork_version: Version, + api_port: u16, + peer_id: PeerId, + chain_config: ChainConfig, +) -> watch::Sender { + let (snapshot_tx, snapshot_rx) = + watch::channel(build_snapshot(engine, clock, genesis_fork_version)); + let api_state = ApiState { + snapshot: snapshot_rx, + version: concat!("moonglass/", env!("CARGO_PKG_VERSION")).to_owned(), + peer_id, + chain_config, + }; + tokio::spawn(async move { + if let Err(error) = api::serve(api_port, api_state).await { + tracing::error!(%error, "rest api stopped"); + } + }); + snapshot_tx +} + +/// Build the REST API snapshot from the current engine and wall clock. +/// +/// Reads the head root, looks up the head block to fill the header fields, and +/// copies the finalized and justified checkpoints and the genesis identity. The +/// body root is the head body's hash-tree-root. When head selection or the block +/// lookup fails, the header fields stay zero so the snapshot is always +/// well-defined. `genesis_fork_version` is the configured version stamped on the +/// genesis state, fixed for the run. +fn build_snapshot( + engine: &FollowEngine, + clock: SlotClock, + genesis_fork_version: Version, +) -> ApiSnapshot { + let store = engine.store(); + let head_root = engine + .get_head() + .map_or(store.finalized_checkpoint.root, |head| head.root); + let head = store.blocks.get(&head_root); + let head_body_root = head + .and_then(|block| block.body.hash_tree_root().ok()) + .map_or(Root::ZERO, Root::from); + ApiSnapshot { + head_root, + head_slot: head.map_or(0, |block| block.slot.as_u64()), + head_proposer_index: head.map_or(0, |block| block.proposer_index.as_u64()), + head_parent_root: head.map_or(Root::ZERO, |block: &BeaconBlock| block.parent_root), + head_state_root: head.map_or(Root::ZERO, |block| block.state_root), + head_body_root, + finalized_epoch: store.finalized_checkpoint.epoch.as_u64(), + finalized_root: store.finalized_checkpoint.root, + justified_epoch: store.justified_checkpoint.epoch.as_u64(), + justified_root: store.justified_checkpoint.root, + genesis_time: store.genesis_time, + genesis_validators_root: engine.genesis_validators_root(), + genesis_fork_version, + current_slot: clock.slot_at(clock::unix_now()), + } +} + +/// Largest block run requested in one backfill, under the spec request cap. +const MAX_BACKFILL_BLOCKS: u64 = 900; + +/// Connected peers and the single in-flight backfill request. +#[derive(Debug, Default)] +struct BackfillState { + /// Peers that can be asked for block ranges. + peers: Vec, + /// Peer currently serving a range request, if any. + in_flight: Option, +} + +impl BackfillState { + /// Add a newly connected peer as a backfill candidate. + fn add_peer(&mut self, peer: PeerId) { + if !self.peers.contains(&peer) { + self.peers.push(peer); + } + } + + /// Stop using a peer for backfill and clear its in-flight request. + fn remove_peer(&mut self, peer: &PeerId) { + self.peers.retain(|candidate| candidate != peer); + self.complete(peer); + } + + /// Mark a peer as serving the current request. + fn start(&mut self, peer: PeerId) { + self.in_flight = Some(peer); + } + + /// Clear the current request when its peer responds or fails. + fn complete(&mut self, peer: &PeerId) { + if self.in_flight.as_ref() == Some(peer) { + self.in_flight = None; + } + } + + /// Return the next peer to ask for a range. + fn next_peer(&self) -> Option { + (self.in_flight.is_none()) + .then(|| self.peers.last().copied()) + .flatten() + } +} + +/// Send the next backfill request to a connected peer when a gap remains. +fn request_backfill( + swarm: &mut Swarm, + backfill: &mut BackfillState, + engine: &FollowEngine, + clock: SlotClock, +) { + let Some(peer) = backfill.next_peer() else { + return; + }; + let Some(request) = backfill_request(engine, clock) else { + return; + }; + let start = request.start_slot; + let count = request.count; + swarm + .behaviour_mut() + .blocks_by_range + .send_request(&peer, request); + tracing::info!(%peer, start, count, "requested block backfill"); + backfill.start(peer); +} + +/// Build a request for the blocks between the anchored head and the current slot. +/// +/// Returns `None` when the head already reaches the current slot, so no backfill +/// is needed. +fn backfill_request(engine: &FollowEngine, clock: SlotClock) -> Option { + let head = engine.get_head().ok()?; + let anchor_slot = engine.store().blocks.get(&head.root)?.slot.as_u64(); + let current_slot = clock.slot_at(clock::unix_now()); + let count = current_slot.checked_sub(anchor_slot)?; + if count == 0 { + return None; + } + Some(BlocksByRangeRequest::new( + anchor_slot + 1, + count.min(MAX_BACKFILL_BLOCKS), + )) +} + +/// Apply backfilled blocks in slot order, returning how many the store accepted. +fn apply_backfill(engine: &mut FollowEngine, blocks: &[Vec]) -> usize { + if let Err(error) = engine.advance_to(clock::unix_now()) { + tracing::warn!(%error, "clock advance before backfill failed"); + } + let mut applied = 0; + for block in blocks { + match engine.handle_gossip(GossipKind::BeaconBlock, block) { + Ok(_) => applied += 1, + Err(error) => tracing::debug!(%error, "backfill block rejected"), + } + } + applied +} + +/// Decompress, classify, and apply a single gossip message, logging the result. +fn handle_message(engine: &mut FollowEngine, topic: &str, data: &[u8]) { + let Some(kind) = classify(topic) else { + tracing::debug!(topic, "ignoring unknown gossip topic"); + return; + }; + let ssz = match decompress_raw(data) { + Ok(bytes) => bytes, + Err(error) => { + tracing::warn!(topic, %error, "snappy decompression failed"); + return; + } + }; + // Advance the store clock to this message's arrival time before dispatching, + // matching the replay path and the handle_gossip contract. Without this the + // clock lags up to one tick behind real time, so a block gossiped at the + // start of its slot is rejected as from the future and never redelivered. + let now = clock::unix_now(); + if let Err(error) = engine.advance_to(now) { + tracing::warn!(topic, %error, "clock advance failed"); + return; + } + match engine.handle_gossip(kind, &ssz) { + Ok(GossipOutcome::Applied) => tracing::debug!(topic, "applied"), + Ok(GossipOutcome::LoggedOnly) => tracing::trace!(topic, "logged only"), + Err(error) => tracing::debug!(topic, %error, "rejected gossip message"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn backfill_state_deduplicates_and_clears_in_flight_peers() { + let first = PeerId::random(); + let second = PeerId::random(); + let mut state = BackfillState::default(); + + state.add_peer(first); + state.add_peer(first); + state.add_peer(second); + + assert_eq!(state.peers, vec![first, second]); + assert_eq!(state.next_peer(), Some(second)); + + state.start(second); + assert_eq!(state.next_peer(), None); + + state.complete(&first); + assert_eq!(state.in_flight, Some(second)); + + state.remove_peer(&second); + assert_eq!(state.in_flight, None); + assert_eq!(state.next_peer(), Some(first)); + } +} diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/anchor_block.ssz_snappy b/moonglass-node/tests/assets/anchor_block.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/anchor_block.ssz_snappy rename to moonglass-node/tests/assets/anchor_block.ssz_snappy diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/anchor_state.ssz_snappy b/moonglass-node/tests/assets/anchor_state.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/anchor_state.ssz_snappy rename to moonglass-node/tests/assets/anchor_state.ssz_snappy diff --git a/moonglass-node/tests/assets/signed_block.ssz_snappy b/moonglass-node/tests/assets/signed_block.ssz_snappy new file mode 100644 index 0000000..2bb8347 Binary files /dev/null and b/moonglass-node/tests/assets/signed_block.ssz_snappy differ diff --git a/moonglass-node/tests/assets/signed_block_alt.ssz_snappy b/moonglass-node/tests/assets/signed_block_alt.ssz_snappy new file mode 100644 index 0000000..c52483e Binary files /dev/null and b/moonglass-node/tests/assets/signed_block_alt.ssz_snappy differ diff --git a/moonglass-node/tests/follower.rs b/moonglass-node/tests/follower.rs new file mode 100644 index 0000000..10c51f3 --- /dev/null +++ b/moonglass-node/tests/follower.rs @@ -0,0 +1,202 @@ +//! Integration coverage for the read-only follower seam. +//! +//! These drive the public follower API with the committed anchor and block +//! fixtures, so the fork-choice wiring (including a block's embedded votes), the +//! topic classifier, and the anchor guards stay verified without inline tests. +#![cfg(all(feature = "minimal", feature = "follower"))] + +use moonglass_core::constants::SLOT_DURATION_MS; +use moonglass_core::containers::{BeaconBlock, BeaconState, SignedBeaconBlock}; +use moonglass_core::networking::{ + beacon_aggregate_and_proof_topic, beacon_block_topic, data_column_sidecar_topic, + execution_payload_bid_topic, execution_payload_topic, payload_attestation_message_topic, + proposer_preferences_topic, +}; +use moonglass_core::primitives::{Epoch, ForkDigest, Root, SubnetId}; +use moonglass_core::ssz::{Deserialize, Merkleized}; + +use moonglass_node::config::{ACTIVE_PRESET, ChainConfig}; +use moonglass_node::error::GenesisError; +use moonglass_node::follower::PayloadStatus; +use moonglass_node::follower::anchor::{ + AnchorContext, AnchorError, adopt_checkpoint, load_context, +}; +use moonglass_node::follower::codec::decompress_raw; +use moonglass_node::follower::dispatch::{GossipKind, classify}; +use moonglass_node::follower::replay::{Capture, CapturedMessage, ReplayError, drive}; + +fn asset(name: &str) -> Vec { + let raw: &[u8] = match name { + "anchor_state" => &include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/anchor_state.ssz_snappy" + ))[..], + "anchor_block" => &include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/anchor_block.ssz_snappy" + ))[..], + "signed_block" => &include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/signed_block.ssz_snappy" + ))[..], + "signed_block_alt" => &include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/assets/signed_block_alt.ssz_snappy" + ))[..], + other => panic!("unknown asset {other}"), + }; + decompress_raw(raw).expect("decompress fixture") +} + +fn block_root(block: &SignedBeaconBlock) -> Root { + Root::from(block.message.hash_tree_root().expect("block root")) +} + +#[test] +fn replay_drives_anchor_plus_block_to_expected_head() { + let anchor_state_ssz = asset("anchor_state"); + let anchor_block_ssz = asset("anchor_block"); + let state = BeaconState::deserialize(&anchor_state_ssz).unwrap(); + let anchor = BeaconBlock::deserialize(&anchor_block_ssz).unwrap(); + let gvr = state.genesis_validators_root; + let genesis_time = state.genesis_time; + let anchor_root = Root::from(anchor.hash_tree_root().expect("anchor root")); + + // Pick the fixture block that builds directly on the anchor. + let candidates = [asset("signed_block"), asset("signed_block_alt")]; + let (child, child_ssz) = candidates + .iter() + .find_map(|ssz| { + let block = SignedBeaconBlock::deserialize(ssz).ok()?; + (block.message.parent_root == anchor_root).then(|| (block, ssz.clone())) + }) + .expect("a fixture block builds on the anchor"); + let child_slot = child.message.slot; + let child_root = block_root(&child); + + // Match the store clock: slot_start_time(slot) = genesis + slot*SLOT_DURATION_MS/1000. + let recv_unix_time = genesis_time + child_slot.as_u64() * (SLOT_DURATION_MS / 1000); + let mut capture = Capture { + anchor_state_ssz, + anchor_block_ssz, + genesis_validators_root: gvr, + messages: vec![CapturedMessage { + recv_unix_time, + kind: GossipKind::BeaconBlock, + ssz_bytes: child_ssz, + }], + expected_head_root: child_root, + expected_head_slot: child_slot, + expected_head_payload_status: None, + }; + + let head = drive(&capture).expect("replay reaches the expected head"); + assert_eq!(head.root, child_root); + + // The oracle also pins the head's payload branch when asked. + capture.expected_head_payload_status = Some(head.payload_status); + assert!(drive(&capture).is_ok()); + let other = match head.payload_status { + PayloadStatus::Empty => PayloadStatus::Full, + _ => PayloadStatus::Empty, + }; + capture.expected_head_payload_status = Some(other); + assert!(matches!( + drive(&capture), + Err(ReplayError::HeadPayloadStatusMismatch { .. }) + )); +} + +#[test] +fn classify_maps_each_topic_and_rejects_unknown() { + let digest = ForkDigest([1, 2, 3, 4]); + assert_eq!( + classify(&beacon_block_topic(digest)), + Some(GossipKind::BeaconBlock) + ); + assert_eq!( + classify(&beacon_aggregate_and_proof_topic(digest)), + Some(GossipKind::AggregateAndProof) + ); + assert_eq!( + classify(&execution_payload_topic(digest)), + Some(GossipKind::ExecutionPayload) + ); + assert_eq!( + classify(&execution_payload_bid_topic(digest)), + Some(GossipKind::ExecutionPayloadBid) + ); + assert_eq!( + classify(&payload_attestation_message_topic(digest)), + Some(GossipKind::PayloadAttestation) + ); + assert_eq!( + classify(&proposer_preferences_topic(digest)), + Some(GossipKind::ProposerPreferences) + ); + assert_eq!( + classify(&data_column_sidecar_topic(digest, SubnetId::new(7))), + Some(GossipKind::DataColumnSidecar) + ); + assert_eq!(classify("/eth2/01020304/voluntary_exit/ssz_snappy"), None); +} + +#[test] +fn adopt_checkpoint_enforces_fork_and_chain_guards() { + let state = BeaconState::deserialize(&asset("anchor_state")).unwrap(); + let block = BeaconBlock::deserialize(&asset("anchor_block")).unwrap(); + let anchor_epoch = state.slot.epoch().as_u64(); + let gvr = state.genesis_validators_root; + + // A fork epoch after the anchor's epoch rejects it as pre-fork. + let mut too_late = ChainConfig::preset(); + too_late.forks.gloas_epoch = Epoch::new(anchor_epoch + 1); + let pre_fork = AnchorContext { + chain_config: too_late, + genesis_validators_root: gvr, + }; + assert!(matches!( + adopt_checkpoint(&pre_fork, &state, &block).map(|_| ()), + Err(AnchorError::Genesis( + GenesisError::SingleForkNotActive { .. } + )) + )); + + // A mismatched genesis validators root rejects the wrong-chain checkpoint. + let mut active = ChainConfig::preset(); + active.forks.gloas_epoch = Epoch::new(anchor_epoch); + let wrong_chain = AnchorContext { + chain_config: active.clone(), + genesis_validators_root: Root::ZERO, + }; + assert!(matches!( + adopt_checkpoint(&wrong_chain, &state, &block).map(|_| ()), + Err(AnchorError::GenesisValidatorsRootMismatch { .. }) + )); + + // A matching context adopts the checkpoint and tracks a head. + let valid = AnchorContext { + chain_config: active, + genesis_validators_root: gvr, + }; + let engine = adopt_checkpoint(&valid, &state, &block).expect("valid checkpoint adopted"); + let head = engine.get_head().expect("select head"); + assert!(engine.store().blocks.contains_key(&head.root)); +} + +#[test] +fn load_context_reads_config_and_genesis_root() { + let state_bytes = asset("anchor_state"); + let state = BeaconState::deserialize(&state_bytes).unwrap(); + // The launcher config names the build preset so it parses on both lanes. + let yaml = format!( + "PRESET_BASE: {}\nGLOAS_FORK_EPOCH: 1\n", + ACTIVE_PRESET.as_str() + ); + let context = load_context(yaml.as_bytes(), &state_bytes).expect("load context"); + assert_eq!(context.chain_config.forks.gloas_epoch, Epoch::new(1)); + assert_eq!( + context.genesis_validators_root, + state.genesis_validators_root + ); +} diff --git a/moonglass/src/constants/chain.rs b/moonglass/src/constants/chain.rs deleted file mode 100644 index ee6de6e..0000000 --- a/moonglass/src/constants/chain.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! Chain identity: genesis trigger parameters, fork versions, and the -//! deposit-contract pointer. -//! -//! Fork versions are mixed into signing domains before a validator signs. This -//! module keeps the Ethereum consensus-spec names for constants consumed by the -//! implemented paths, without exporting every unused historical config entry. - -use crate::primitives::{Epoch, ExecutionAddress, Slot, Version}; - -/// Minimum active validator count required to trigger genesis. -#[cfg(feature = "mainnet")] -pub const MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: u64 = 16_384; - -/// Earliest Unix timestamp at which genesis may occur. -#[cfg(feature = "mainnet")] -pub const MIN_GENESIS_TIME: u64 = 1_606_824_000; - -/// Delay, in seconds, between the genesis trigger and the genesis slot. -#[cfg(feature = "mainnet")] -pub const GENESIS_DELAY: u64 = 604_800; - -/// First epoch number. -pub const GENESIS_EPOCH: Epoch = Epoch(0); - -/// First slot number. -pub const GENESIS_SLOT: Slot = Slot(0); - -/// Fork version stamped on the genesis state. -#[cfg(feature = "mainnet")] -pub const GENESIS_FORK_VERSION: Version = Version([0x00, 0x00, 0x00, 0x00]); - -/// Fork version used by voluntary-exit signatures. -/// -/// The consensus rules intentionally pin this domain to the same version used -/// when BLS-to-execution credential changes were introduced, so old voluntary -/// exits remain verifiable after later network upgrades. -#[cfg(feature = "mainnet")] -pub const CAPELLA_FORK_VERSION: Version = Version([0x03, 0x00, 0x00, 0x00]); - -/// Depth of the deposit-contract Merkle tree. -pub const DEPOSIT_CONTRACT_TREE_DEPTH: usize = 32; - -/// Length of a deposit proof: Merkle path plus root chunk. -pub const DEPOSIT_PROOF_LEN: usize = DEPOSIT_CONTRACT_TREE_DEPTH + 1; - -/// Sentinel for `deposit_requests_start_index` meaning no start index has been -/// assigned yet, because no execution-layer deposit request has been processed. -pub const UNSET_DEPOSIT_REQUESTS_START_INDEX: u64 = u64::MAX; - -/// Chain ID of the network the deposit contract lives on. -#[cfg(feature = "mainnet")] -pub const DEPOSIT_CHAIN_ID: u64 = 1; - -/// Network ID of the network the deposit contract lives on. -#[cfg(feature = "mainnet")] -pub const DEPOSIT_NETWORK_ID: u64 = 1; - -/// Execution-layer address of the canonical deposit contract. -#[cfg(feature = "mainnet")] -pub const DEPOSIT_CONTRACT_ADDRESS: ExecutionAddress = ExecutionAddress([ - 0x00, 0x00, 0x00, 0x00, 0x21, 0x9a, 0xb5, 0x40, 0x35, 0x6c, 0xbb, 0x83, 0x9c, 0xbe, 0x05, 0x30, - 0x3d, 0x77, 0x05, 0xfa, -]); - -// Minimal-preset values from the consensus-spec minimal configuration. - -/// Minimal-preset active validator count required to trigger genesis. -#[cfg(feature = "minimal")] -pub const MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: u64 = 64; - -/// Minimal-preset earliest Unix timestamp at which genesis may occur. -#[cfg(feature = "minimal")] -pub const MIN_GENESIS_TIME: u64 = 1_578_009_600; - -/// Minimal-preset delay, in seconds, between genesis trigger and genesis slot. -#[cfg(feature = "minimal")] -pub const GENESIS_DELAY: u64 = 300; - -/// Minimal-preset fork version stamped on the genesis state. -#[cfg(feature = "minimal")] -pub const GENESIS_FORK_VERSION: Version = Version([0x00, 0x00, 0x00, 0x01]); - -/// Minimal-preset fork version used by voluntary-exit signatures. -#[cfg(feature = "minimal")] -pub const CAPELLA_FORK_VERSION: Version = Version([0x03, 0x00, 0x00, 0x01]); - -/// Minimal-preset deposit-contract chain ID. -#[cfg(feature = "minimal")] -pub const DEPOSIT_CHAIN_ID: u64 = 5; - -/// Minimal-preset deposit-contract network ID. -#[cfg(feature = "minimal")] -pub const DEPOSIT_NETWORK_ID: u64 = 5; - -/// Minimal-preset execution-layer address of the deposit contract. -#[cfg(feature = "minimal")] -pub const DEPOSIT_CONTRACT_ADDRESS: ExecutionAddress = ExecutionAddress([ - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, - 0x34, 0x56, 0x78, 0x90, -]); diff --git a/moonglass/src/containers/attestation.rs b/moonglass/src/containers/attestation.rs deleted file mode 100644 index 5aa0755..0000000 --- a/moonglass/src/containers/attestation.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Validator vote containers and slashing evidence. -//! -//! An attestation is a validator vote for the current head block and for the -//! source/target checkpoints used by Casper finality. Conflicting votes can -//! become slashing evidence. - -use crate::constants::{MAX_ATTESTING_INDICES, MAX_COMMITTEES_PER_SLOT}; -use crate::containers::{Checkpoint, SignedBeaconBlockHeader}; -use crate::primitives::{BLSSignature, CommitteeIndex, Root, Slot, ValidatorIndex}; -use ssz_rs::prelude::*; - -/// The vote payload an attester signs. -/// -/// It names the slot, spec index field, head block, and finality checkpoints. -/// Aggregate attestations use `committee_bits` to identify participating -/// committees. The meaning of `index` depends on the attestation form being -/// evaluated. In payload-branch checks, a vote for a block whose slot equals -/// `data.slot` must use `index == 0` and remains pending for fork-choice -/// scoring. A vote naming an older head for `data.slot` uses `index == 0` for -/// the empty/no-payload branch and `index == 1` for the full/payload branch. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct AttestationData { - /// Slot the attestation is for. - pub slot: Slot, - /// Spec index field. - /// - /// For aggregate attestations this is not the sole committee selector, so use - /// `committee_bits` to identify participating committees. For payload - /// branch voting, a vote for a block at `data.slot` must use `0`. A vote for - /// an older head uses `0` for the empty branch and `1` for the full branch. - pub index: CommitteeIndex, - /// Head block root being voted for by chain-head selection. - pub beacon_block_root: Root, - /// Source checkpoint used by finality. - pub source: Checkpoint, - /// Target checkpoint used by finality. - pub target: Checkpoint, -} - -/// Attestation expanded into sorted attester indices for signature verification. -/// -/// `attesting_indices` is sorted and deduplicated, the form [`BeaconState::validate_indexed_attestation`](crate::containers::BeaconState::validate_indexed_attestation) -/// requires before checking the aggregate `signature` over `data`. Unlike -/// [`crate::containers::IndexedPayloadAttestation`], duplicate indices are invalid here because -/// each validator votes at most once. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct IndexedAttestation { - /// Sorted, deduplicated indices of validators that attested. - pub attesting_indices: List, - /// The vote shared by all listed attesters. - pub data: AttestationData, - /// Aggregate BLS signature of the listed attesters. - pub signature: BLSSignature, -} - -/// Wire-form attestation: per-committee participation bitfield + aggregate signature. -/// -/// The state transition consumes block-body attestations through -/// [`BeaconState::process_attestation`](crate::containers::BeaconState::process_attestation). Fork choice consumes both block and -/// gossip attestations through [`crate::fork_choice::Store::on_attestation()`] to update -/// latest messages for LMD-GHOST. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct Attestation { - /// Per-attester participation, packed across all committees of the slot. - pub aggregation_bits: Bitlist, - /// The vote shared by all participants. - pub data: AttestationData, - /// Aggregate signature over `data` from the participating attesters. - pub signature: BLSSignature, - /// Bitfield selecting which committees within the slot participated. - pub committee_bits: Bitvector, -} - -/// Single-attester attestation form used on gossip before aggregation. -/// -/// It names one `attester_index` and its `committee_index` directly rather than packing -/// participation into bitfields, so an individual vote can travel the network before it is -/// folded into an aggregate [`Attestation`]. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct SingleAttestation { - /// Committee the attester belongs to. - pub committee_index: CommitteeIndex, - /// Validator that produced the attestation. - pub attester_index: ValidatorIndex, - /// The vote. - pub data: AttestationData, - /// Attester's signature over `data`. - pub signature: BLSSignature, -} - -/// Evidence of two contradictory attestations by overlapping validator sets. -/// -/// Block processing validates and applies this through -/// [`BeaconState::process_attester_slashing`](crate::containers::BeaconState::process_attester_slashing). Fork choice records the -/// equivocating validators through [`crate::fork_choice::Store::on_attester_slashing()`]. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct AttesterSlashing { - /// First conflicting attestation. - pub attestation_1: IndexedAttestation, - /// Second conflicting attestation. - /// - /// Must share at least one attester with `attestation_1`. - pub attestation_2: IndexedAttestation, -} - -/// Evidence that a proposer signed two distinct block headers for the same slot. -/// -/// Block processing validates and applies this through -/// [`BeaconState::process_proposer_slashing`](crate::containers::BeaconState::process_proposer_slashing). -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct ProposerSlashing { - /// First signed header by the proposer. - pub signed_header_1: SignedBeaconBlockHeader, - /// Conflicting signed header by the same proposer for the same slot. - pub signed_header_2: SignedBeaconBlockHeader, -} diff --git a/moonglass/src/containers/block.rs b/moonglass/src/containers/block.rs deleted file mode 100644 index 4e506b4..0000000 --- a/moonglass/src/containers/block.rs +++ /dev/null @@ -1,153 +0,0 @@ -//! Block-shaped containers: header, body, block, and their signed envelopes. - -use ssz_rs::prelude::*; - -use crate::constants::{ - MAX_ATTESTATIONS, MAX_ATTESTER_SLASHINGS, MAX_BLS_TO_EXECUTION_CHANGES, MAX_DEPOSITS, - MAX_PAYLOAD_ATTESTATIONS, MAX_PROPOSER_SLASHINGS, MAX_VOLUNTARY_EXITS, -}; -use crate::containers::{ - Attestation, AttesterSlashing, Deposit, Eth1Data, ExecutionRequests, PayloadAttestation, - ProposerSlashing, SignedBLSToExecutionChange, SignedExecutionPayloadBid, SignedVoluntaryExit, - SyncAggregate, -}; -use crate::primitives::{BLSSignature, Bytes32, Root, Slot, ValidatorIndex}; - -/// Compact block summary stored in state and signed by proposers. -/// -/// It carries the roots needed to identify a block without storing the full -/// body, and it is reused as proposer-slashing evidence. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct BeaconBlockHeader { - /// Slot the block is proposed for. - pub slot: Slot, - /// Validator that proposed the block. - pub proposer_index: ValidatorIndex, - /// Root of the parent block. - pub parent_root: Root, - /// Root of the post-state after applying the block. - pub state_root: Root, - /// Root of [`BeaconBlockBody`]. - pub body_root: Root, -} - -impl BeaconBlockHeader { - /// Return this header with `state_root` set. - #[must_use] - pub fn with_state_root(mut self, state_root: Root) -> Self { - self.state_root = state_root; - self - } -} - -/// Header plus the proposer's signature. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct SignedBeaconBlockHeader { - /// The header being signed. - pub message: BeaconBlockHeader, - /// Proposer's signature over the domain-separated signing root of `message`. - pub signature: BLSSignature, -} - -/// All operations the proposer chose to include in this block. -/// -/// Parent-payload requests and withdrawals are processed around this body. The -/// body itself carries randomness, votes, slashings, lifecycle operations, -/// payload-timeliness votes, and sync-committee participation. -/// Consumed by [`BeaconState::process_block`](crate::containers::BeaconState::process_block): parent payload commitment is -/// handled before the current-slot bid, then operations are handled by -/// [`BeaconState::process_operations`](crate::containers::BeaconState::process_operations). -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct BeaconBlockBody { - /// Proposer's RANDAO reveal, mixed into committee-shuffling randomness. - pub randao_reveal: BLSSignature, - /// Proposer's deposit-chain vote used to track new deposits. - pub eth1_data: Eth1Data, - /// Proposer-supplied freeform 32-byte tag, ignored by consensus. - pub graffiti: Bytes32, - /// Evidence of duplicate block proposals. - pub proposer_slashings: List, - /// Evidence of double-vote or surround-vote misbehavior. - pub attester_slashings: List, - /// Validator votes for the head block and finality checkpoints. - pub attestations: List, - /// Legacy block-body deposits from the spec shape. - /// - /// Non-empty lists are rejected here. Active deposit application arrives - /// through parent-payload [`ExecutionRequests`]. - pub deposits: List, - /// Validator-signed requests to leave the active set. - pub voluntary_exits: List, - /// Aggregate sync-committee signature over the previous-slot block root. - pub sync_aggregate: SyncAggregate, - /// Requests to swap BLS withdrawal credentials for execution addresses. - pub bls_to_execution_changes: List, - /// Builder bid the proposer committed to for this slot. - /// - /// Accepted by [`BeaconState::process_execution_payload_bid`](crate::containers::BeaconState::process_execution_payload_bid), then matched - /// by [`BeaconState::verify_execution_payload_envelope`](crate::containers::BeaconState::verify_execution_payload_envelope) when the matching - /// payload envelope is delivered for this block. - pub signed_execution_payload_bid: SignedExecutionPayloadBid, - /// Payload-timeliness committee votes for the parent slot's payload. - /// - /// The state transition validates these with - /// [`BeaconState::process_payload_attestation`](crate::containers::BeaconState::process_payload_attestation). Fork choice replays the - /// aggregate through [`crate::fork_choice::Store::on_block()`], expanding participants - /// before writing local PTC vote evidence for the attested parent root. - pub payload_attestations: List, - /// Execution-to-consensus requests from the parent slot's payload. - /// - /// The block proves these requests by matching the accepted parent bid's - /// `execution_requests_root` before applying them in - /// [`BeaconState::accept_parent_payload_commitment`](crate::containers::BeaconState::accept_parent_payload_commitment). - pub parent_execution_requests: ExecutionRequests, -} - -/// Proposed beacon block with its slot identity, claimed post-state root, and -/// operations. -/// In state transition this is applied by -/// [`crate::containers::BeaconState::apply_signed_block`]. In fork choice it is -/// accepted by [`crate::fork_choice::Store::on_block()`] and stored in -/// [`crate::fork_choice::Store::blocks`]. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct BeaconBlock { - /// Slot the block is for. - pub slot: Slot, - /// Validator that proposed the block. - pub proposer_index: ValidatorIndex, - /// Root of the parent block. - pub parent_root: Root, - /// Root of the post-state produced by applying this block. - pub state_root: Root, - /// Block operations. - pub body: BeaconBlockBody, -} - -impl BeaconBlock { - /// Header corresponding to this block and the supplied body/state roots. - #[must_use] - pub fn header(&self, body_root: Root, state_root: Root) -> BeaconBlockHeader { - BeaconBlockHeader { - slot: self.slot, - proposer_index: self.proposer_index, - parent_root: self.parent_root, - state_root, - body_root, - } - } -} - -/// Beacon block plus the proposer's signature. -/// -/// This is the entry object for the block transition: -/// [`crate::containers::BeaconState::apply_signed_block`] advances slots, -/// checks the proposer signature, processes the block, and verifies the claimed -/// post-state root. Fork choice passes the same object to -/// [`crate::fork_choice::Store::on_block()`] before caching the resulting post-state. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct SignedBeaconBlock { - /// The block being signed. - pub message: BeaconBlock, - /// Proposer's signature over the domain-separated signing root of `message`. - pub signature: BLSSignature, -} diff --git a/moonglass/src/containers/builder.rs b/moonglass/src/containers/builder.rs deleted file mode 100644 index 8aa57cd..0000000 --- a/moonglass/src/containers/builder.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Builder-market containers. -//! -//! Builders supply execution payloads for proposed beacon blocks. They bid for -//! slots, beacon attestations add the state-transition quorum weight used to -//! release builder payments, and payload-timeliness committee votes feed fork -//! choice's local payload-status evidence. This module models the consensus -//! objects for that builder-supplied payload path. - -use ssz_rs::prelude::*; - -use crate::constants::PTC_SIZE; -use crate::primitives::{ - BLSPubkey, BLSSignature, BuilderIndex, Epoch, ExecutionAddress, Gwei, Root, Slot, - ValidatorIndex, -}; - -/// A single builder entry in the builder registry, indexed by [`BuilderIndex`]. -/// -/// The `pubkey` verifies a builder's bid signatures, and `balance` is the stake that backs -/// accepted bids and funds the payments owed by them. A builder stays active until -/// `withdrawable_epoch`, which holds `FAR_FUTURE_EPOCH` while the builder is live. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct Builder { - /// Builder's BLS public key used to verify bid signatures. - pub pubkey: BLSPubkey, - /// Builder registry record version. - pub version: u8, - /// Execution-layer address that receives builder withdrawals. - pub execution_address: ExecutionAddress, - /// Builder's stake balance backing accepted bids. - pub balance: Gwei, - /// Epoch at which the builder was added to the registry. - pub deposit_epoch: Epoch, - /// Epoch the balance becomes withdrawable, or `FAR_FUTURE_EPOCH` while active. - pub withdrawable_epoch: Epoch, -} - -/// Outbound payment a builder owes for an accepted bid, queued for the withdrawal sweep. -/// -/// Accepting a bid does not transfer funds immediately. It opens this obligation against the -/// builder's balance, payable to `fee_recipient`, which the withdrawal sweep settles once the -/// payment becomes due. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct BuilderPendingWithdrawal { - /// Execution-layer address the payment is destined for. - pub fee_recipient: ExecutionAddress, - /// Amount the builder owes. - pub amount: Gwei, - /// Builder making the payment. - pub builder_index: BuilderIndex, -} - -/// Builder payment accumulator entry for one accepted bid. -/// -/// A bid opens the `withdrawal` obligation with zero `weight`. Beacon -/// attestations for that proposal slot add effective balance only when they set -/// a new participation flag for the attester. The entry can be queued by -/// parent-payload handoff, queued at epoch aging if `weight` clears the quorum, -/// dropped when it ages out below quorum, or cleared by proposer slashing while -/// still in the two-epoch payment window. PTC votes are separate fork-choice -/// evidence about payload timeliness and blob data availability. They do not add -/// this payment weight. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct BuilderPendingPayment { - /// Sum of committee members' effective balance weighting toward payment quorum. - pub weight: Gwei, - /// The payment obligation queued by parent-payload handoff or quorum release. - pub withdrawal: BuilderPendingWithdrawal, - /// Proposer that opened this payment, so proposer slashing only clears the - /// payment when it slashes that same proposer. - pub proposer_index: ValidatorIndex, -} - -/// The vote a payload-timeliness committee member signs for a slot. -/// -/// It records whether the payload for `beacon_block_root` at `slot` was seen -/// (`payload_present`) and whether its blob data arrived alongside it (`blob_data_available`). -/// The same data appears inside block aggregates ([`PayloadAttestation`]) and gossip messages -/// ([`PayloadAttestationMessage`]), so a committee member's verdict travels unchanged whether -/// it is broadcast individually or folded into an aggregate. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct PayloadAttestationData { - /// Beacon block root the payload is associated with. - pub beacon_block_root: Root, - /// Slot the attestation is for. - pub slot: Slot, - /// True if the payload was seen on time, before its due time in the slot. - pub payload_present: bool, - /// True if blob data was observed alongside the payload. - pub blob_data_available: bool, -} - -/// Aggregated payload-timeliness vote carried in the block body: per-position bitfield plus -/// aggregate signature. -/// `aggregation_bits` is indexed by committee position, not by validator index, -/// so a set bit means the validator occupying that position attested. -/// [`BeaconState::process_payload_attestation`](crate::containers::BeaconState::process_payload_attestation) -/// validates this aggregate form. The current fork-choice replay path expands -/// those bits to validator indices and then reuses -/// [`crate::fork_choice::Store::on_payload_attestation_message()`], whose local store -/// write expands each validator back to every PTC position it occupies. Contrast -/// [`PayloadAttestationMessage`], which names a single validator index directly. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct PayloadAttestation { - /// Bit per committee position, set to 1 if that position's signature is included. - pub aggregation_bits: Bitvector, - /// The shared vote. - pub data: PayloadAttestationData, - /// Aggregate signature over `data`. - pub signature: BLSSignature, -} - -/// Single-member payload-timeliness vote used on gossip before aggregation. -/// -/// Handled by [`crate::fork_choice::Store::on_payload_attestation_message()`]. This form -/// names a validator index, and fork choice expands it to every PTC position the -/// validator occupies before updating payload vote vectors. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct PayloadAttestationMessage { - /// Committee member that produced the vote. - pub validator_index: ValidatorIndex, - /// The vote. - pub data: PayloadAttestationData, - /// Member's signature over `data`. - pub signature: BLSSignature, -} - -/// Payload-timeliness vote expanded into sorted participant indices. -/// -/// Built by [`BeaconState::indexed_payload_attestation`](crate::containers::BeaconState::indexed_payload_attestation) and validated by -/// [`BeaconState::validate_indexed_payload_attestation`](crate::containers::BeaconState::validate_indexed_payload_attestation). Unlike ordinary -/// indexed beacon attestations, duplicate validator indices are valid here -/// because PTC membership is position-based and a validator may occupy multiple -/// positions. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct IndexedPayloadAttestation { - /// Sorted indices of committee members that attested. Duplicates are valid - /// because a validator may occupy multiple PTC positions. - pub attesting_indices: List, - /// The shared vote. - pub data: PayloadAttestationData, - /// Aggregate signature. - pub signature: BLSSignature, -} diff --git a/moonglass/src/containers/chain.rs b/moonglass/src/containers/chain.rs deleted file mode 100644 index e92a1bf..0000000 --- a/moonglass/src/containers/chain.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Chain metadata containers. -//! -//! Covers signing-version records, Casper finality checkpoints, historical -//! summaries, deposit-vote data, and signing-domain data. `Eth1Data` keeps the -//! historical spec name for the execution-layer deposit-chain vote. - -use ssz_rs::prelude::*; - -use crate::primitives::{Domain, Epoch, Hash32, Root, Version}; - -/// Tracks the active signing version and the version it succeeded. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct Fork { - /// Signing version active before `epoch`. - pub previous_version: Version, - /// Signing version active from `epoch` onward. - pub current_version: Version, - /// Epoch at which the current version became active. - pub epoch: Epoch, -} - -/// Domain-separation tuple fed into signing-domain construction. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct ForkData { - /// Signing version used for domain separation. - pub current_version: Version, - /// Root of the validator set at genesis, tying the signature to this chain. - pub genesis_validators_root: Root, -} - -/// Casper finality checkpoint: the `(epoch, block_root)` pair finality refers to. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, SimpleSerialize)] -pub struct Checkpoint { - /// Epoch the checkpoint is at. - pub epoch: Epoch, - /// Block root at the start of `epoch`. - pub root: Root, -} - -/// Per-historical-period roots stored in `BeaconState.historical_summaries`. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct HistoricalSummary { - /// Merkle root of the block-roots ring buffer for the period. - pub block_summary_root: Root, - /// Merkle root of the state-roots ring buffer for the period. - pub state_summary_root: Root, -} - -/// Deposit-voting data observed in a block and aggregated across the voting period. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct Eth1Data { - /// Root of the deposit-contract Merkle tree at the voted deposit-chain block. - pub deposit_root: Root, - /// Total deposits observed up to and including the voted deposit-chain block. - pub deposit_count: u64, - /// Hash of the voted deposit-chain block. - pub block_hash: Hash32, -} - -/// Helper tuple whose tree root is the BLS signing message. -/// -/// This is not a network message. It exists so signatures bind an object root -/// to the domain that identifies its message kind and signing context. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct SigningData { - /// Tree root of the object being signed. - pub object_root: Root, - /// Signing domain combining the domain type and signing-version root. - pub domain: Domain, -} diff --git a/moonglass/src/containers/execution.rs b/moonglass/src/containers/execution.rs deleted file mode 100644 index 0e8636c..0000000 --- a/moonglass/src/containers/execution.rs +++ /dev/null @@ -1,166 +0,0 @@ -//! Execution-layer data carried through consensus. -//! -//! Execution payloads contain opaque execution transactions and block-access -//! list bytes. Consensus does not inspect their internal execution semantics. -//! It tracks roots, hashes, requests, and builder commitments needed by the -//! beacon-state transition. -//! -//! Inclusion-list bid extensions are not represented in the current -//! `ExecutionPayloadBid` shape. - -use crate::constants::{ - BYTES_PER_LOGS_BLOOM, MAX_BLOB_COMMITMENTS_PER_BLOCK, MAX_BYTES_PER_TRANSACTION, - MAX_EXTRA_DATA_BYTES, MAX_TRANSACTIONS_PER_PAYLOAD, MAX_WITHDRAWALS_PER_PAYLOAD, -}; -use crate::containers::{ExecutionRequests, Withdrawal}; -use crate::primitives::{ - BLSSignature, BuilderIndex, Bytes32, ExecutionAddress, Gwei, Hash32, KZGCommitment, Root, Slot, - Uint256, -}; -use ssz_rs::prelude::*; - -/// Opaque RLP-encoded block access list. Layout is not unpacked by consensus. -pub type BlockAccessList = List; - -/// A single execution-layer transaction as an opaque byte list. -pub type Transaction = List; - -/// The list of transactions an `ExecutionPayload` carries. -pub type Transactions = List; - -/// Execution-layer block payload delivered for a beacon block. -/// -/// Consensus treats transactions and block-access lists as opaque bytes here. -/// [`BeaconState::verify_execution_payload_envelope`](crate::containers::BeaconState::verify_execution_payload_envelope) checks the payload against the -/// accepted builder bid and expected consensus-side commitments. Execution -/// engine validity is outside the current boundary. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct ExecutionPayload { - /// Parent execution block hash. - pub parent_hash: Hash32, - /// Address that receives priority fees and builder payments. - pub fee_recipient: ExecutionAddress, - /// State root of the execution-layer trie after applying this payload. - pub state_root: Bytes32, - /// Root of the transaction-receipts trie. - pub receipts_root: Bytes32, - /// Bloom filter over event logs emitted by transactions in the payload. - pub logs_bloom: Vector, - /// RANDAO mix carried from the consensus layer for execution-layer use. - pub prev_randao: Bytes32, - /// Execution-layer block height. - pub block_number: u64, - /// Maximum gas budget for the block. - pub gas_limit: u64, - /// Gas actually consumed by the block's transactions. - pub gas_used: u64, - /// Unix timestamp the payload was produced for. - pub timestamp: u64, - /// Proposer-supplied freeform bytes (capped to 32). - pub extra_data: List, - /// Base fee per gas (little-endian 256-bit integer). - pub base_fee_per_gas: Uint256, - /// Execution-layer block hash. - pub block_hash: Hash32, - /// Opaque transactions, each itself a length-prefixed byte list. - pub transactions: Transactions, - /// Withdrawals applied at the execution layer (capped per payload). - pub withdrawals: List, - /// Blob gas consumed by this payload. - pub blob_gas_used: u64, - /// Excess blob gas used to price the next payload's blobs. - pub excess_blob_gas: u64, - /// Block-access list as opaque RLP-encoded bytes. - pub block_access_list: BlockAccessList, - /// Slot tag echoed back to the consensus layer. - pub slot_number: u64, -} - -/// Builder's bid for the proposer's slot. -/// -/// Non-self-build bids are signed by the builder. Self-build bids carry the -/// self-build sentinel and rely on the beacon proposer's block signature -/// instead. The proposer commits to the chosen bid by including it in the signed -/// beacon block body. -/// Consumed in the current block by -/// [`BeaconState::process_execution_payload_bid`](crate::containers::BeaconState::process_execution_payload_bid), which updates -/// `BeaconState::latest_execution_payload_bid` and the active -/// builder-payment accumulator. The next child block uses the bid's -/// `execution_requests_root` to prove the parent payload handoff. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct ExecutionPayloadBid { - /// Execution-layer parent block hash the bid is conditioned on. - pub parent_block_hash: Hash32, - /// Consensus-layer parent block root the bid is conditioned on. - pub parent_block_root: Root, - /// Hash the builder commits to producing as the next execution block. - pub block_hash: Hash32, - /// RANDAO value the builder expects the proposer to reveal for the slot. - pub prev_randao: Bytes32, - /// Address receiving priority fees and the accepted bid value. - pub fee_recipient: ExecutionAddress, - /// Gas budget the builder commits to honoring. - pub gas_limit: u64, - /// Builder offering the bid, or `BUILDER_INDEX_SELF_BUILD` for self-builds. - pub builder_index: BuilderIndex, - /// Slot the bid is for, which must equal the proposer's slot at inclusion time. - pub slot: Slot, - /// Trustless Gwei amount the builder will pay the proposer if accepted. - pub value: Gwei, - /// Trusted execution-layer payment marker, zero for broadcast bids. - pub execution_payment: Gwei, - /// KZG commitments the builder pre-commits to including blobs for. - pub blob_kzg_commitments: List, - /// Tree root of the execution-to-consensus requests the builder commits to. - pub execution_requests_root: Root, -} - -/// Builder bid plus its signature field. -/// -/// Included in [`crate::containers::BeaconBlockBody`] and verified by -/// [`BeaconState::process_execution_payload_bid`](crate::containers::BeaconState::process_execution_payload_bid). -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct SignedExecutionPayloadBid { - /// The bid being signed. - pub message: ExecutionPayloadBid, - /// Builder signature under `DOMAIN_BEACON_BUILDER`, or the point at infinity - /// for self-build bids. - pub signature: BLSSignature, -} - -/// Delivered payload plus execution-to-consensus requests and provenance roots. -/// -/// Checked by [`BeaconState::verify_execution_payload_envelope`](crate::containers::BeaconState::verify_execution_payload_envelope). Fork choice records a -/// checked envelope through [`crate::fork_choice::Store::on_execution_payload_envelope()`] -/// in [`crate::fork_choice::Store::payloads`]. That store entry means -/// "recorded after the current consensus-side envelope checks", not a complete -/// execution-engine or blob-availability verdict. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct ExecutionPayloadEnvelope { - /// The execution payload delivered for the bid. - pub payload: ExecutionPayload, - /// Execution-to-consensus requests carried by the payload. - pub execution_requests: ExecutionRequests, - /// Accepted bid's builder index, or the self-build sentinel. - pub builder_index: BuilderIndex, - /// Root of the beacon block this envelope is bound to. - pub beacon_block_root: Root, - /// Root of the parent beacon block. - pub parent_beacon_block_root: Root, -} - -/// Envelope plus the signature from the required envelope signer. -/// -/// Network-facing entry object for -/// [`crate::fork_choice::Store::on_execution_payload_envelope()`]. The state transition -/// validates the signer and bid commitments, and fork choice records the -/// envelope only after those checks pass. Non-self-build envelopes are signed by -/// the registered builder. Self-build envelopes are signed by the beacon -/// proposer. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct SignedExecutionPayloadEnvelope { - /// The envelope being signed. - pub message: ExecutionPayloadEnvelope, - /// Signature under `DOMAIN_BEACON_BUILDER` from the required envelope signer. - pub signature: BLSSignature, -} diff --git a/moonglass/src/containers/state.rs b/moonglass/src/containers/state.rs deleted file mode 100644 index f18c8fe..0000000 --- a/moonglass/src/containers/state.rs +++ /dev/null @@ -1,253 +0,0 @@ -//! The full consensus state. -//! -//! `BeaconState` combines the validator registry, historical ring buffers, -//! processing queues, builder state, and per-slot working data. -//! Conceptually, it is the chain clock and identity, recent block/state roots, -//! randomness, validator registry and balances, participation/finality records, -//! execution and withdrawal handoff, lifecycle queues, proposer lookahead, and -//! builder/payload-timeliness state. -//! -//! # Field groups -//! -//! - Clock and history: `genesis_time`, `slot`, `latest_block_header`, -//! `block_roots`, `state_roots`, historical roots and summaries. Mutated by -//! [`BeaconState::process_slots`](crate::containers::BeaconState::process_slots), [`BeaconState::process_slot`](crate::containers::BeaconState::process_slot), and [`BeaconState::process_block_header`](crate::containers::BeaconState::process_block_header). -//! - Validator registry and balances: `validators`, `balances`, slashing totals, -//! participation flags, inactivity scores, sync committees, proposer -//! lookahead. Mutated by operation and epoch-processing phases. -//! - Finality: justification checkpoints, finalization checkpoints, and -//! `justification_bits`. Mutated during epoch processing from accumulated -//! participation. -//! - Execution and withdrawal handoff: `latest_block_hash`, -//! withdrawal cursors, expected withdrawals, and execution-request queues. -//! Mutated by withdrawals and by the parent-payload handoff. -//! - Registry queues and churn: pending deposits, partial withdrawals, -//! consolidations, and balance budgets. Mutated by execution requests and -//! epoch queue processing. -//! - Builder registry and payments: `builders`, builder withdrawal cursor, -//! pending payments, pending withdrawals, and `latest_execution_payload_bid`. -//! Mutated by builder bids, beacon attestations for proposal slots, and the -//! parent payload handoff. -//! - Payload availability and PTC: `execution_payload_availability` and -//! `ptc_window`. Mutated by slot processing, parent-payload acceptance, and -//! PTC window updates. - -use crate::constants::{ - BUILDER_PAYMENT_WINDOW_LEN, BUILDER_PENDING_WITHDRAWALS_LIMIT, BUILDER_REGISTRY_LIMIT, - EPOCHS_PER_HISTORICAL_VECTOR, EPOCHS_PER_SLASHINGS_VECTOR, ETH1_DATA_VOTES_LEN, - HISTORICAL_ROOTS_LIMIT, JUSTIFICATION_BITS_LENGTH, MAX_WITHDRAWALS_PER_PAYLOAD, - PENDING_CONSOLIDATIONS_LIMIT, PENDING_DEPOSITS_LIMIT, PENDING_PARTIAL_WITHDRAWALS_LIMIT, - PROPOSER_LOOKAHEAD_LEN, PTC_SIZE, PTC_WINDOW_LEN, SLOTS_PER_HISTORICAL_ROOT, - UNSET_DEPOSIT_REQUESTS_START_INDEX, VALIDATOR_REGISTRY_LIMIT, -}; -use crate::containers::{ - BeaconBlockHeader, Builder, BuilderPendingPayment, BuilderPendingWithdrawal, Checkpoint, - Eth1Data, ExecutionPayloadBid, Fork, HistoricalSummary, PendingConsolidation, PendingDeposit, - PendingPartialWithdrawal, SyncCommittee, Validator, Withdrawal, -}; -use crate::primitives::{ - BuilderIndex, Bytes32, Epoch, Gwei, Hash32, ParticipationFlags, Root, Slot, ValidatorIndex, - WithdrawalIndex, -}; -use ssz_rs::prelude::*; - -/// Complete consensus snapshot needed to validate the next slot or block. -/// -/// Consensus is convergence on this object: validators that apply the same -/// valid slots and blocks should arrive at the same `BeaconState`. -/// The state transition replays this structure forward one block at a time. A -/// fork-choice [`Store`](crate::fork_choice::Store) may cache many post-states, -/// but only the transition mutates a `BeaconState`. When reading a handler, ask -/// whether it writes this object (consensus state) or writes the store (local -/// node view). -/// The default value is the SSZ zero state. It is useful for construction, but -/// it is not a valid initialized chain state on its own. Upgrade routines may -/// set fields such as `execution_payload_availability` to non-zero values. -#[derive(Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct BeaconState { - /// Unix timestamp the chain started at. - pub genesis_time: u64, - /// Root of the validator set at genesis. Tags this chain in domain separators. - pub genesis_validators_root: Root, - /// Current slot of the state. - pub slot: Slot, - /// Active signing-version pair. - pub fork: Fork, - /// Header of the most recent block applied to this state. - pub latest_block_header: BeaconBlockHeader, - /// Ring buffer of past block roots indexed by `slot % SLOTS_PER_HISTORICAL_ROOT`. - /// - /// Written by slot processing and read by attestation validation to decide - /// whether a vote names the canonical block at a historical slot. - pub block_roots: Vector, - /// Ring buffer of past state roots indexed by `slot % SLOTS_PER_HISTORICAL_ROOT`. - /// - /// Written by slot processing before advancing the clock. - pub state_roots: Vector, - /// Roll-up roots of historical block-root buffers, trimmed for cheap proofs. - pub historical_roots: List, - /// Currently winning deposit-chain vote (`Eth1Data` in the spec). - pub eth1_data: Eth1Data, - /// Deposit-chain votes accumulated over the current voting period. - pub eth1_data_votes: List, - /// Index of the next deposit to be processed from the deposit contract. - pub eth1_deposit_index: u64, - /// The validator registry. - pub validators: List, - /// Per-validator balances before effective-balance rounding. - pub balances: List, - /// Ring buffer of past RANDAO mixes. - pub randao_mixes: Vector, - /// Ring buffer of accumulated slashing totals per epoch. - pub slashings: Vector, - /// Per-validator participation flags from the previous epoch. - pub previous_epoch_participation: List, - /// Per-validator participation flags from the current epoch. - pub current_epoch_participation: List, - /// Rolling Casper finality-justification bitvector. - pub justification_bits: Bitvector, - /// Justified checkpoint from the previous epoch. - pub previous_justified_checkpoint: Checkpoint, - /// Justified checkpoint from the current epoch. - pub current_justified_checkpoint: Checkpoint, - /// Most recent finalized checkpoint. - pub finalized_checkpoint: Checkpoint, - /// Per-validator inactivity-leak scores. - pub inactivity_scores: List, - /// Sync committee active this period. - pub current_sync_committee: SyncCommittee, - /// Sync committee active next period, cached one period in advance. - pub next_sync_committee: SyncCommittee, - /// Hash of the most recently settled execution payload. - /// - /// The parent-payload handoff advances this when a child block proves and - /// applies the parent block's payload effects. The next bid must extend this - /// hash through `ExecutionPayloadBid::parent_block_hash`. - pub latest_block_hash: Hash32, - /// Sequence index of the next withdrawal to emit. - pub next_withdrawal_index: WithdrawalIndex, - /// Sweep cursor over the validator registry for the next withdrawal scan. - pub next_withdrawal_validator_index: ValidatorIndex, - /// Summarized history beyond the live ring buffer. - pub historical_summaries: List, - /// Starting index for processing deposit-contract requests. - pub deposit_requests_start_index: u64, - /// Remaining deposit-balance budget for the current epoch. - pub deposit_balance_to_consume: Gwei, - /// Remaining exit-balance budget for the current epoch. - pub exit_balance_to_consume: Gwei, - /// Earliest epoch a new exit may be scheduled at. - pub earliest_exit_epoch: Epoch, - /// Remaining consolidation-balance budget for the current epoch. - pub consolidation_balance_to_consume: Gwei, - /// Earliest epoch a new consolidation may be scheduled at. - pub earliest_consolidation_epoch: Epoch, - /// Queue of deposits awaiting signature verification and activation. - pub pending_deposits: List, - /// Queue of scheduled partial withdrawals. - pub pending_partial_withdrawals: - List, - /// Queue of scheduled consolidations. - pub pending_consolidations: List, - /// Lookahead proposer assignments for the next few slots. - pub proposer_lookahead: Vector, - /// The builder registry. - pub builders: List, - /// Sweep cursor over the builder registry for the next withdrawal scan. - pub next_withdrawal_builder_index: BuilderIndex, - /// Per-slot bit indicating whether the slot's payload was settled available. - /// - /// Cleared by slot processing for the next slot and set by the child block's - /// parent-payload handoff. Beacon attestations read this bit when deciding - /// whether a historical head vote matches the empty or full branch. - pub execution_payload_availability: Bitvector, - /// Rolling 2-epoch accumulator of builder-payment quorum weights. - /// - /// A bid opens one entry and later-included beacon attestations for that - /// bid's slot add effective balance to its `weight`. If a child block - /// accepts the parent payload, the parent-payload handoff releases the - /// payment unconditionally. Otherwise epoch-boundary aging uses the quorum - /// threshold to decide whether the payment is released or dropped. - pub builder_pending_payments: Vector, - /// Queue of builder payments awaiting the next withdrawal sweep. - pub builder_pending_withdrawals: - List, - /// Most-recently accepted builder bid for the current slot. - /// - /// `process_execution_payload_bid` writes this commitment. The later - /// envelope path must match it, and the child block uses its request root - /// when settling the parent payload. - pub latest_execution_payload_bid: ExecutionPayloadBid, - /// Withdrawals the next payload is expected to include. - /// - /// Withdrawal processing computes this list before bid/envelope validation. - /// envelope validation rejects a payload whose withdrawals do not match. - pub payload_expected_withdrawals: List, - /// Per-slot payload-timeliness committee assignments over the lookahead window. - /// - /// Payload attestation validation and fork-choice PTC gossip both use this - /// window to translate between committee positions and validator indices. - pub ptc_window: Vector, PTC_WINDOW_LEN>, -} - -/// SSZ zero state, field by field. -/// -/// # Warning -/// This is **not** a valid initialized chain state. It is the all-zero SSZ -/// value, useful only as a starting point for construction (genesis seeding, -/// upgrade routines). Treating it as a live state will produce -/// spec-invalid behavior. Genesis state is built by the spec's initialization -/// routine. Later spec upgrade routines may set fields such as -/// `execution_payload_availability` to non-zero starting values. -impl Default for BeaconState { - fn default() -> Self { - Self { - genesis_time: u64::default(), - genesis_validators_root: Root::default(), - slot: Slot::default(), - fork: Fork::default(), - latest_block_header: BeaconBlockHeader::default(), - block_roots: Vector::default(), - state_roots: Vector::default(), - historical_roots: List::default(), - eth1_data: Eth1Data::default(), - eth1_data_votes: List::default(), - eth1_deposit_index: u64::default(), - validators: List::default(), - balances: List::default(), - randao_mixes: Vector::default(), - slashings: Vector::default(), - previous_epoch_participation: List::default(), - current_epoch_participation: List::default(), - justification_bits: Bitvector::default(), - previous_justified_checkpoint: Checkpoint::default(), - current_justified_checkpoint: Checkpoint::default(), - finalized_checkpoint: Checkpoint::default(), - inactivity_scores: List::default(), - current_sync_committee: SyncCommittee::default(), - next_sync_committee: SyncCommittee::default(), - latest_block_hash: Hash32::default(), - next_withdrawal_index: WithdrawalIndex::default(), - next_withdrawal_validator_index: ValidatorIndex::default(), - historical_summaries: List::default(), - deposit_requests_start_index: UNSET_DEPOSIT_REQUESTS_START_INDEX, - deposit_balance_to_consume: Gwei::default(), - exit_balance_to_consume: Gwei::default(), - earliest_exit_epoch: Epoch::default(), - consolidation_balance_to_consume: Gwei::default(), - earliest_consolidation_epoch: Epoch::default(), - pending_deposits: List::default(), - pending_partial_withdrawals: List::default(), - pending_consolidations: List::default(), - proposer_lookahead: Vector::default(), - builders: List::default(), - next_withdrawal_builder_index: BuilderIndex::default(), - execution_payload_availability: Bitvector::default(), - builder_pending_payments: Vector::default(), - builder_pending_withdrawals: List::default(), - latest_execution_payload_bid: ExecutionPayloadBid::default(), - payload_expected_withdrawals: List::default(), - ptc_window: Vector::default(), - } - } -} diff --git a/moonglass/src/containers/sync.rs b/moonglass/src/containers/sync.rs deleted file mode 100644 index ee29c04..0000000 --- a/moonglass/src/containers/sync.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Sync-committee machinery. - -use ssz_rs::prelude::*; - -use crate::constants::SYNC_COMMITTEE_SIZE; -use crate::primitives::{BLSPubkey, BLSSignature}; - -/// Set of validators rotated in to sign sync updates each sync period. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct SyncCommittee { - /// Member public keys, in committee order. - pub pubkeys: Vector, - /// Sum of `pubkeys`, used for fast aggregate verification. - pub aggregate_pubkey: BLSPubkey, -} - -/// Aggregated sync-committee signature over the previous slot's block root. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct SyncAggregate { - /// Bit per committee member, set to 1 if the member's signature is included. - pub sync_committee_bits: Bitvector, - /// Aggregate signature of the participating committee members. - pub sync_committee_signature: BLSSignature, -} diff --git a/moonglass/src/containers/validator.rs b/moonglass/src/containers/validator.rs deleted file mode 100644 index 6380e4a..0000000 --- a/moonglass/src/containers/validator.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Validator registry record and the pending queues that gate registry transitions. - -use ssz_rs::prelude::*; - -use crate::primitives::{BLSPubkey, BLSSignature, Bytes32, Epoch, Gwei, Slot, ValidatorIndex}; - -/// A single validator entry in the registry, indexed by [`ValidatorIndex`]. -/// -/// The lifecycle epochs (`activation_eligibility_epoch`, `activation_epoch`, `exit_epoch`, -/// `withdrawable_epoch`) gate when the validator may act and when its balance leaves the -/// consensus layer, and most hold `FAR_FUTURE_EPOCH` until the corresponding transition is -/// scheduled. The `effective_balance` is the quantized stake that drives weight and rewards, -/// not the spendable `balance` tracked separately in [`crate::containers::BeaconState`]. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct Validator { - /// BLS public key used to verify the validator's signatures. - pub pubkey: BLSPubkey, - /// 32-byte credential whose leading byte selects the withdrawal-credential variant. - pub withdrawal_credentials: Bytes32, - /// Quantized balance contributing to weight, voting power, and base-reward math. - pub effective_balance: Gwei, - /// True once the validator has been successfully slashed. - pub slashed: bool, - /// Epoch at which the validator became eligible to enter the activation queue. - pub activation_eligibility_epoch: Epoch, - /// Epoch at which the validator became active. - pub activation_epoch: Epoch, - /// Epoch at which the validator exited (or `FAR_FUTURE_EPOCH` if not exited). - pub exit_epoch: Epoch, - /// Epoch at which the balance becomes withdrawable. - pub withdrawable_epoch: Epoch, -} - -/// Entry in the deferred-deposit queue awaiting signature verification and activation. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct PendingDeposit { - /// Depositing validator's public key. - pub pubkey: BLSPubkey, - /// Withdrawal credential the deposit binds the validator to. - pub withdrawal_credentials: Bytes32, - /// Deposit amount. - pub amount: Gwei, - /// Signature over the deposit message, checked when the deposit is processed. - pub signature: BLSSignature, - /// Slot the deposit was observed in. - pub slot: Slot, -} - -/// Scheduled partial withdrawal of a validator's surplus balance. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct PendingPartialWithdrawal { - /// Validator the withdrawal applies to. - pub validator_index: ValidatorIndex, - /// Amount to withdraw at `withdrawable_epoch`. - pub amount: Gwei, - /// Earliest epoch the withdrawal becomes due. - pub withdrawable_epoch: Epoch, -} - -/// Pending merge of one validator's balance into another's. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct PendingConsolidation { - /// Source validator (balance is moved out of this one). - pub source_index: ValidatorIndex, - /// Target validator (balance is folded into this one). - pub target_index: ValidatorIndex, -} diff --git a/moonglass/src/containers/withdrawal.rs b/moonglass/src/containers/withdrawal.rs deleted file mode 100644 index ab2588f..0000000 --- a/moonglass/src/containers/withdrawal.rs +++ /dev/null @@ -1,197 +0,0 @@ -//! Validator lifecycle and execution-to-consensus request containers. -//! -//! These containers cover deposits, exits, withdrawals, credential changes, and -//! requests created by the execution layer for the consensus transition to -//! consume. -//! -//! The lifecycle path is: a deposit or deposit request enters a pending queue, -//! the validator becomes active, performs duties, may change withdrawal -//! credentials, may request voluntary exit, waits through churn scheduling, and -//! eventually becomes withdrawable. Consensus-layer withdrawals move balances -//! out. Execution-layer withdrawal requests ask consensus to schedule that -//! movement. -//! -//! Validator balances are consensus-layer accounting. Deposits and withdrawals -//! are controlled movements between that accounting and execution-layer ETH. - -use crate::constants::{ - DEPOSIT_PROOF_LEN, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, - MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, - MAX_DEPOSIT_REQUESTS_PER_PAYLOAD, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, -}; -use crate::primitives::{ - BLSPubkey, BLSSignature, Bytes32, Epoch, ExecutionAddress, Gwei, ValidatorIndex, - WithdrawalIndex, -}; -use ssz_rs::prelude::*; - -/// Atomic balance movement from the consensus layer to an execution-layer address. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct Withdrawal { - /// Monotonic index uniquely identifying this withdrawal across history. - pub index: WithdrawalIndex, - /// Validator whose balance is being withdrawn. - pub validator_index: ValidatorIndex, - /// Execution-layer destination address. - pub address: ExecutionAddress, - /// Withdrawn amount. - pub amount: Gwei, -} - -/// Request to swap a BLS withdrawal credential for an execution address. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct BLSToExecutionChange { - /// Validator initiating the credential change. - pub validator_index: ValidatorIndex, - /// BLS key authorising the change (must match the current withdrawal credential). - pub from_bls_pubkey: BLSPubkey, - /// New execution address for future withdrawals. - pub to_execution_address: ExecutionAddress, -} - -/// Credential-change request plus the BLS signature authorising it. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct SignedBLSToExecutionChange { - /// The credential-change being signed. - pub message: BLSToExecutionChange, - /// Signature over the domain-separated signing root of `message`. - pub signature: BLSSignature, -} - -/// Validator-initiated request to leave the active set. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct VoluntaryExit { - /// Epoch the exit is being signed at. The exit cannot take effect earlier. - pub epoch: Epoch, - /// Validator requesting to exit. - pub validator_index: ValidatorIndex, -} - -/// Voluntary exit plus the validator's signature authorising it. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct SignedVoluntaryExit { - /// The voluntary exit being signed. - pub message: VoluntaryExit, - /// Signature over the domain-separated signing root of `message`. - pub signature: BLSSignature, -} - -/// Deposit payload as written to the execution-layer deposit contract. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct DepositData { - /// Depositing validator's public key. - pub pubkey: BLSPubkey, - /// Withdrawal credential the deposit binds the validator to. - pub withdrawal_credentials: Bytes32, - /// Deposit amount. - pub amount: Gwei, - /// Signature over the deposit message (the unsigned tuple). - pub signature: BLSSignature, -} - -/// A deposit-contract event packaged with its Merkle inclusion proof. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct Deposit { - /// Merkle proof of inclusion in the deposit-contract tree (depth + 1 nodes). - pub proof: Vector, - /// The deposit payload being proven. - pub data: DepositData, -} - -/// Execution-layer deposit request that supersedes deposit-vote-driven deposits. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct DepositRequest { - /// Depositing validator's public key. - pub pubkey: BLSPubkey, - /// Withdrawal credential the deposit binds the validator to. - pub withdrawal_credentials: Bytes32, - /// Deposit amount. - pub amount: Gwei, - /// Signature over the deposit message. - pub signature: BLSSignature, - /// Sequence index assigned by the deposit contract. - pub index: u64, -} - -/// Execution-layer request to withdraw or fully exit a validator. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct WithdrawalRequest { - /// Execution-layer address authorising the request (must match the credential). - pub source_address: ExecutionAddress, - /// Validator targeted by the request. - pub validator_pubkey: BLSPubkey, - /// Amount to withdraw, or `FULL_EXIT_REQUEST_AMOUNT` for a full exit. - pub amount: Gwei, -} - -/// Execution-layer request to consolidate one validator's balance into another. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct ConsolidationRequest { - /// Execution-layer address authorising the consolidation. - pub source_address: ExecutionAddress, - /// Source validator's public key (balance moved out). - pub source_pubkey: BLSPubkey, - /// Target validator's public key (balance folded in). - pub target_pubkey: BLSPubkey, -} - -/// Execution-layer request to deposit stake for a builder. -/// -/// A builder deposit either registers a new builder or tops up an existing one. -/// Registering a new builder checks the signature under a builder-specific -/// domain, so it cannot be confused with a validator deposit. A top-up to an -/// existing builder skips the signature check. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct BuilderDepositRequest { - /// Builder's public key. - pub pubkey: BLSPubkey, - /// Withdrawal credential the deposit binds the builder to. - pub withdrawal_credentials: Bytes32, - /// Deposit amount. - pub amount: Gwei, - /// Signature over the deposit message under the builder-deposit domain. - pub signature: BLSSignature, -} - -/// Execution-layer request to exit a builder from the registry. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, SimpleSerialize)] -pub struct BuilderExitRequest { - /// Execution-layer address authorising the exit (must match the builder's). - pub source_address: ExecutionAddress, - /// Builder's public key. - pub pubkey: BLSPubkey, -} - -/// All execution-to-consensus requests delivered by a payload, grouped by kind. -/// -/// The delivered envelope carries these requests with the payload. The child -/// block carries the same requests in -/// [`crate::containers::BeaconBlockBody::parent_execution_requests`], where -/// [`BeaconState::accept_parent_payload_commitment`](crate::containers::BeaconState::accept_parent_payload_commitment) checks their root against -/// the accepted parent bid before dispatching deposit, withdrawal, consolidation, -/// and builder handlers. -#[derive(Default, Debug, Clone, PartialEq, Eq, SimpleSerialize)] -pub struct ExecutionRequests { - /// Execution-layer deposit requests. - pub deposits: List, - /// Execution-layer partial-withdrawal and full-exit requests. - pub withdrawals: List, - /// Execution-layer consolidation requests. - pub consolidations: List, - /// Execution-layer builder deposit requests. - pub builder_deposits: List, - /// Execution-layer builder exit requests. - pub builder_exits: List, -} - -impl ExecutionRequests { - /// True when the payload carried no execution-to-consensus requests. - #[must_use] - pub fn is_empty(&self) -> bool { - self.deposits.is_empty() - && self.withdrawals.is_empty() - && self.consolidations.is_empty() - && self.builder_deposits.is_empty() - && self.builder_exits.is_empty() - } -} diff --git a/moonglass/src/crypto/kzg.rs b/moonglass/src/crypto/kzg.rs deleted file mode 100644 index 4106f78..0000000 --- a/moonglass/src/crypto/kzg.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! KZG polynomial commitments and FK batch openings over arkworks pairings. -//! -//! The implementation keeps the equations visible where the terms are built: -//! - commitment: `C = [p(tau)]_1 = sum_i p_i [tau^i]_1` -//! - opening: `q(X) = (p(X) - p(z)) / (X - z)` and `pi = [q(tau)]_1` -//! - verification: `e(C - [p(z)]_1, [1]_2) = e(pi, [tau - z]_2)` -//! -//! Blob wrappers on top of these primitives are not yet exposed: -//! `blob_to_kzg_commitment`, `compute_kzg_proof`, `compute_blob_kzg_proof`, -//! `verify_kzg_proof`, `verify_blob_kzg_proof`, `verify_blob_kzg_proof_batch`. - -mod error; -mod fk; -mod opening; -mod setup; -mod trusted_setup; - -pub use error::KzgError; -pub use fk::open_fk; -pub use opening::{commit, open, verify}; -pub use setup::{EthereumKzgSetup, KzgSetup}; -pub use trusted_setup::{ - EthereumTrustedSetup, PowersOfTau, SetupFileError, get_powers_from_bytes, get_powers_from_file, - get_powers_from_text, -}; diff --git a/moonglass/src/crypto/kzg/error.rs b/moonglass/src/crypto/kzg/error.rs deleted file mode 100644 index fc4cb1e..0000000 --- a/moonglass/src/crypto/kzg/error.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Error types shared by KZG operations. - -use thiserror::Error; - -/// KZG operation failures. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] -#[non_exhaustive] -pub enum KzgError { - /// Polynomial has more coefficients than the trusted-setup G1 powers cover. - #[error( - "polynomial has {coefficients} coefficients but setup only has {setup_powers} G1 powers" - )] - PolynomialTooLarge { - /// Polynomial coefficient count requested by the operation. - coefficients: usize, - /// Number of G1 powers available in the setup. - setup_powers: usize, - }, - - /// FK opening was requested on an empty polynomial. - #[error("FK opening requires a non-empty polynomial")] - EmptyPolynomial, - - /// FK evaluation domain size does not match the polynomial length. - #[error("FK domain size {domain_size} does not match {coefficients} coefficients")] - DomainSizeMismatch { - /// Polynomial coefficient count. - coefficients: usize, - /// Radix-2 evaluation domain size. - domain_size: usize, - }, - - /// Doubling the polynomial length to derive the FK domain overflowed `usize`. - #[error("FK domain size overflow for {coefficients} coefficients")] - DomainSizeOverflow { - /// Polynomial coefficient count that overflowed the doubled domain. - coefficients: usize, - }, - - /// The scalar field does not support a radix-2 domain of the requested size. - #[error("unsupported radix-2 domain size {0}")] - UnsupportedDomainSize(usize), -} - -/// Ensure the setup has enough G1 powers for `coefficients` polynomial terms. -pub(super) fn ensure_supported(coefficients: usize, setup_powers: usize) -> Result<(), KzgError> { - if coefficients > setup_powers { - return Err(KzgError::PolynomialTooLarge { - coefficients, - setup_powers, - }); - } - Ok(()) -} diff --git a/moonglass/src/fork_choice/on_execution_payload_envelope.rs b/moonglass/src/fork_choice/on_execution_payload_envelope.rs deleted file mode 100644 index 619ece8..0000000 --- a/moonglass/src/fork_choice/on_execution_payload_envelope.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Taking in a delivered execution payload. -//! -//! When a [block's](crate::glossary#beacon-block) payload finally arrives, -//! carried in a signed envelope, this handler checks it and records it. -//! Recording it is what lets the block's *full* branch appear in the fork-choice -//! tree. Three kinds of check run: the consensus-side ones (the signature, and -//! that the payload matches the bid, [slot](crate::glossary#slot), parent hash, -//! timestamp, and withdrawals), a data-availability check, and an -//! execution-engine check. The last two are mocked here and always pass, see -//! [`is_data_available`] and [`verify_and_notify_new_payload`]. Wiring real ones -//! is a TODO. -use crate::containers::SignedExecutionPayloadEnvelope; -use crate::error::ForkChoiceError; -use crate::primitives::Root; - -use super::store::Store; - -impl Store { - /// Check a delivered payload against its block and record it in the store. - /// - /// In order: confirm the block is known, that its data is available - /// ([`is_data_available`]), that the envelope verifies against the block's - /// state - /// ([`verify_execution_payload_envelope`](crate::containers::BeaconState::verify_execution_payload_envelope), - /// run on a throwaway copy), and that the execution engine accepts it - /// ([`verify_and_notify_new_payload`]). Only then is the envelope filed in - /// [`Store::payloads`](super::store::Store::payloads). A payload's lasting - /// effects are applied later, when a child block builds on it. - /// - /// # Errors - /// - /// [`ForkChoiceError::PayloadEnvelopeForUnknownBlock`] if the block is unknown, - /// [`ForkChoiceError::PayloadDataUnavailable`] if the data is missing, a - /// verification error if the consensus checks fail, and - /// [`ForkChoiceError::PayloadExecutionInvalid`] if the engine rejects it. - pub fn on_execution_payload_envelope( - &mut self, - signed_envelope: &SignedExecutionPayloadEnvelope, - ) -> Result<(), ForkChoiceError> { - let envelope = &signed_envelope.message; - let beacon_block_root = envelope.beacon_block_root; - // Prove the block is known, then verify the envelope on a throwaway copy of - // its state so the verification records nothing. - let block_state = self.block_states.get(&beacon_block_root).ok_or( - ForkChoiceError::PayloadEnvelopeForUnknownBlock(beacon_block_root), - )?; - if !is_data_available(beacon_block_root) { - return Err(ForkChoiceError::PayloadDataUnavailable(beacon_block_root)); - } - let mut state = block_state.clone(); - state.verify_execution_payload_envelope(signed_envelope)?; - if !verify_and_notify_new_payload(signed_envelope) { - return Err(ForkChoiceError::PayloadExecutionInvalid(beacon_block_root)); - } - self.payloads.insert(beacon_block_root, envelope.clone()); - Ok(()) - } -} - -/// Whether the block's payload data was available to download. -/// -/// TODO: a real implementation samples the payload's data column sidecars and -/// verifies their KZG proofs. No such verifier is wired in, so this mock always -/// reports the data available. -pub fn is_data_available(_beacon_block_root: Root) -> bool { - true -} - -/// Whether the execution engine accepts the payload as valid. -/// -/// TODO: a real implementation hands the payload to an execution engine, which -/// runs the transactions and reports back. No engine is wired in, so this mock -/// always reports the payload valid. -pub fn verify_and_notify_new_payload(_signed_envelope: &SignedExecutionPayloadEnvelope) -> bool { - true -} diff --git a/moonglass/src/lib.rs b/moonglass/src/lib.rs deleted file mode 100644 index 57b0199..0000000 --- a/moonglass/src/lib.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! A behavior-first guide to the Ethereum consensus specs. -//! -//! Ethereum consensus is the rulebook validators use to agree on chain state. -//! This crate models the data validators agree on, the signed blocks that -//! propose changes to it, and the transition rules that decide whether those -//! changes are valid. -//! -//! This crate is a readable execution map, not a production client architecture. -//! Its data structures and main function shapes stay close to the consensus -//! specs where that helps orientation, and helpers may use clearer Rust shapes when -//! that makes the protocol behavior easier to follow. -//! The covered surface is the currently wired consensus-specs reference-test -//! surface. `mainnet` and `minimal` are spec presets, not claims that every -//! documented branch is live-network behavior or fixture-covered. -//! -//! Start with a protocol object, not with a file tree. Ask which handler owns -//! the object, which fields it reads, which fields it writes, whether the write -//! is durable consensus state or local fork-choice view, where the current -//! implementation deliberately stops, and which fixture family exercises that -//! path. The core route is object -> owning handler -> reads -> writes -> -//! decision -> boundary -> fixture. -//! -//! # Start by object -//! -//! Follow one route at a time. Each route names the object to start from, the -//! handler that owns it, the state/store distinction to watch, and the fixture -//! family that closes the loop. -//! -//! - Block acceptance: -//! start at [`containers::SignedBeaconBlock`], then inspect -//! [`containers::BeaconState::apply_signed_block`] and -//! [`fork_choice::Store::on_block()`]. -//! Watch the state transition mutate a cloned -//! [`BeaconState`](containers::BeaconState) while fork choice caches the -//! post-state in [`fork_choice::Store`]. Fixtures: -//! `sanity/blocks`, `fork_choice/on_block`. -//! - Bid commitment: -//! start at [`containers::ExecutionPayloadBid`], then inspect -//! [`containers::BeaconState::process_execution_payload_bid`]. A bid commits -//! consensus state to payload fields and opens any builder-payment obligation. -//! It is not delivered payload evidence. Fixture: -//! `operations/execution_payload_bid`. -//! - Delivered payload evidence: -//! start at [`containers::SignedExecutionPayloadEnvelope`], then inspect -//! [`fork_choice::Store::on_execution_payload_envelope()`] and -//! [`fork_choice::Store::payloads`]. The envelope passed the implemented -//! consensus-side checks, not full execution-engine or blob-availability -//! verification. Fixture: `fork_choice/on_execution_payload_envelope`. -//! - Parent-payload settlement: -//! start at [`containers::ExecutionPayloadBid`] and -//! [`containers::ExecutionRequests`], then inspect -//! [`containers::BeaconState::accept_parent_payload_commitment`]. The child -//! proves its parent payload handoff, applies the parent payload's requests, -//! and releases the parent builder payment. Fixture: -//! `operations/parent_execution_payload`. -//! - PTC votes: -//! start at [`containers::PayloadAttestation`] and -//! [`containers::PayloadAttestationMessage`], then inspect -//! [`containers::BeaconState::process_payload_attestation`] and -//! [`fork_choice::Store::on_payload_attestation_message()`]. Block aggregates -//! and gossip messages are admitted differently, but the store records local -//! PTC vote evidence by position. Fixtures: `operations/payload_attestation`, -//! `fork_choice/on_payload_attestation_message`. -//! - Beacon attestation branch choice: -//! start at [`containers::Attestation`], then inspect -//! [`containers::BeaconState::process_attestation`] and -//! [`fork_choice::Store::on_attestation()`]. Votes for a block at `data.slot` -//! stay pending. Votes for an older head use `AttestationData::index` as the -//! payload empty/full selector. Fixtures: `operations/attestation`, -//! `fork_choice/on_attestation`. -//! - Head selection: -//! start at [`fork_choice::ForkChoiceNode`], then inspect -//! [`fork_choice::Store::get_head`]. Fork choice returns both a block root and -//! a [`fork_choice::PayloadStatus`]. Fixture: `fork_choice/get_head`. -//! -//! Use [`state_transition`] to follow a block through the rulebook, then use -//! [`containers`] for the data being moved, [`primitives`] and [`constants`] for -//! vocabulary and parameters, [`error`] for the difference between an invalid -//! transition and behavior not yet covered, and [`fork_choice`] for -//! the head-selection rule that reads accepted blocks and attestations to decide -//! which leaf the next block should build on. -//! -//! Build docs with private items when reading this crate as an executable spec: -//! most of the useful phase maps live in private modules because they mirror -//! consensus sub-phases rather than form a public API surface. -//! -//! Coverage boundaries are part of the reading surface. When a consensus branch -//! can be exercised without yet implementing every external verifier, the -//! relevant module docs should name that boundary explicitly. -//! -//! # Hold these distinctions before reading -//! -//! A few distinctions decide whether the rest of the code reads correctly. -//! Hold them before following any route. -//! -//! - [`BeaconState`](containers::BeaconState) is durable consensus state, the -//! snapshot validators agree on and carry forward. The fork-choice -//! [`Store`](fork_choice::Store) is one node's local view, the accepted -//! blocks, attestations, and clock that node has seen. The store is -//! bookkeeping for head selection, not consensus state, and two honest nodes -//! can hold different stores. -//! - A builder's bid is a commitment, not an accepted payload. Recording the bid -//! promises a payload at a hash, but the payload effects settle only when a -//! child block proves and applies the parent payload commitment. -//! - A recorded payload envelope has passed only the consensus-side checks: -//! beacon block roots, required envelope signature, bid-matched payload -//! fields, payload slot, parent execution hash, timestamp, requests root, and -//! withdrawals. -//! Recording it is not an execution-engine validity verdict and not a -//! data-availability verdict. -//! - Payload-timeliness votes are indexed by committee position. A gossip -//! message names one validator and expands to the committee positions that -//! validator holds, so the same vote reads as a validator on the wire and as -//! a set of positions in the aggregate. -//! - A beacon attestation's `index` is a payload-branch selector only when -//! the voted block is older than `attestation.data.slot`. If the voted block is -//! at `data.slot`, the vote must use the empty/pending form. The two rulebooks -//! then read that selector differently. In state transition, a historical -//! vote's `index` is matched against the `BeaconState` payload-availability bit -//! to decide whether the vote earns its head flag, not whether the vote is -//! accepted. In fork choice, an older full-branch vote is admitted only once the -//! local [`fork_choice::Store::payloads`] map holds the recorded envelope. One -//! shapes a head-flag reward from durable consensus state, the other is a -//! node-local admission gate. -//! - A child block applies the parent payload's effects before its own bid. It -//! settles the parent block's promised payload first, then records its own -//! commitment for a later child to prove. -//! -//! # Where this implementation stops -//! -//! The implementation runs the consensus-side rules and stops at the external -//! verifiers a production client would also wire in. Three boundaries are -//! deliberate and not yet implemented. Execution-engine payload validity is not -//! checked, so a recorded payload is consensus-checked, not engine-confirmed. -//! Blob and data-availability verification is not performed. Networking and -//! gossip validation are not wired in, so handlers accept already-deserialized -//! objects rather than messages off the wire. Within those boundaries the code -//! can still exercise the covered consensus branches, including the -//! payload-status branches in fork choice, which is why each affected module -//! names its own boundary in its docs. -#[cfg(not(any(feature = "mainnet", feature = "minimal")))] -compile_error!("crate must be built with exactly one of the `mainnet` or `minimal` features"); - -#[cfg(all(feature = "mainnet", feature = "minimal"))] -compile_error!( - "crate cannot be built with both `mainnet` and `minimal` features (cargo features are additive)" -); - -pub mod constants; -pub mod containers; -pub mod crypto; -pub mod error; -pub mod fork_choice; -pub mod glossary; -pub mod primitives; -pub mod state_transition; diff --git a/moonglass/src/primitives/byte_arrays.rs b/moonglass/src/primitives/byte_arrays.rs deleted file mode 100644 index bde8dd7..0000000 --- a/moonglass/src/primitives/byte_arrays.rs +++ /dev/null @@ -1,360 +0,0 @@ -//! Fixed-size byte newtypes (roots, hashes, BLS pubkeys, signatures, KZG commitments). - -use sha2::{Digest, Sha256}; -use ssz_rs::prelude::*; - -/// SSZ chunk length, in bytes. -const SSZ_CHUNK_BYTES: usize = 32; -/// Compressed BLS12-381 G1 point length, in bytes. -const BLS_G1_COMPRESSED_BYTES: usize = 48; -/// Compressed BLS12-381 G2 point length, in bytes. -const BLS_G2_COMPRESSED_BYTES: usize = 96; -/// KZG commitment length (compressed BLS12-381 G1 point), in bytes. -const KZG_COMMITMENT_BYTES: usize = 48; -/// 256-bit unsigned integer length, in bytes. -const UINT256_BYTES: usize = 32; - -/// 32-byte SSZ hash-tree-root. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, SimpleSerialize)] -#[repr(transparent)] -pub struct Root(pub [u8; 32]); - -impl Root { - /// All-zero root used by the spec as an unset placeholder in block headers. - pub const ZERO: Self = Self([0; 32]); -} - -/// 32-byte execution-layer hash. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, SimpleSerialize)] -#[repr(transparent)] -pub struct Hash32(pub [u8; 32]); - -/// Versioned blob hash (KZG commitment digest with version prefix). -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, SimpleSerialize)] -#[repr(transparent)] -pub struct VersionedHash(pub [u8; 32]); - -/// 20-byte execution-layer address. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, SimpleSerialize)] -#[repr(transparent)] -pub struct ExecutionAddress(pub [u8; 20]); - -/// 4-byte signing version. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, SimpleSerialize)] -#[repr(transparent)] -pub struct Version(pub [u8; 4]); - -/// 4-byte digest of the active signing version. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, SimpleSerialize)] -#[repr(transparent)] -pub struct ForkDigest(pub [u8; 4]); - -/// 32-byte SSZ signing domain. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, SimpleSerialize)] -#[repr(transparent)] -pub struct Domain(pub [u8; 32]); - -/// 4-byte domain-type tag (left half of a [`Domain`]). -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, SimpleSerialize)] -#[repr(transparent)] -pub struct DomainType(pub [u8; 4]); - -/// Compressed BLS public key as SSZ-decoded bytes. -/// -/// Curve validity is checked by the BLS verifier. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(transparent)] -pub struct BLSPubkey(pub [u8; BLS_G1_COMPRESSED_BYTES]); - -impl Default for BLSPubkey { - fn default() -> Self { - Self([0; BLS_G1_COMPRESSED_BYTES]) - } -} - -impl Sized for BLSPubkey { - fn is_variable_size() -> bool { - false - } - fn size_hint() -> usize { - BLS_G1_COMPRESSED_BYTES - } -} - -impl Serialize for BLSPubkey { - fn serialize(&self, buffer: &mut Vec) -> Result { - buffer.extend_from_slice(&self.0); - Ok(BLS_G1_COMPRESSED_BYTES) - } -} - -impl Deserialize for BLSPubkey { - fn deserialize(encoding: &[u8]) -> Result { - match encoding.len() { - n if n == BLS_G1_COMPRESSED_BYTES => { - let mut out = [0u8; BLS_G1_COMPRESSED_BYTES]; - out.copy_from_slice(encoding); - Ok(Self(out)) - } - n if n < BLS_G1_COMPRESSED_BYTES => Err(DeserializeError::ExpectedFurtherInput { - provided: n, - expected: BLS_G1_COMPRESSED_BYTES, - }), - n => Err(DeserializeError::AdditionalInput { - provided: n, - expected: BLS_G1_COMPRESSED_BYTES, - }), - } - } -} - -impl Merkleized for BLSPubkey { - fn hash_tree_root(&mut self) -> Result { - Ok(merkleize_byte_sequence(&self.0)) - } -} - -impl SimpleSerialize for BLSPubkey { - fn is_composite_type() -> bool { - true - } -} - -/// Compressed BLS signature as SSZ-decoded bytes. -/// -/// Curve validity is checked by the BLS verifier. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(transparent)] -pub struct BLSSignature(pub [u8; BLS_G2_COMPRESSED_BYTES]); - -impl Default for BLSSignature { - fn default() -> Self { - Self([0; BLS_G2_COMPRESSED_BYTES]) - } -} - -impl BLSSignature { - /// Serialized BLS12-381 G2 point at infinity used for empty aggregates and - /// self-build placeholders. - pub const G2_POINT_AT_INFINITY: Self = { - let mut bytes = [0; BLS_G2_COMPRESSED_BYTES]; - bytes[0] = 0xC0; - Self(bytes) - }; - - /// True when this signature is the serialized G2 point at infinity. - #[must_use] - pub const fn is_g2_point_at_infinity(&self) -> bool { - let expected = Self::G2_POINT_AT_INFINITY.0; - let mut i = 0; - while i < BLS_G2_COMPRESSED_BYTES { - if self.0[i] != expected[i] { - return false; - } - i += 1; - } - true - } -} - -impl Sized for BLSSignature { - fn is_variable_size() -> bool { - false - } - fn size_hint() -> usize { - BLS_G2_COMPRESSED_BYTES - } -} - -impl Serialize for BLSSignature { - fn serialize(&self, buffer: &mut Vec) -> Result { - buffer.extend_from_slice(&self.0); - Ok(BLS_G2_COMPRESSED_BYTES) - } -} - -impl Deserialize for BLSSignature { - fn deserialize(encoding: &[u8]) -> Result { - match encoding.len() { - n if n == BLS_G2_COMPRESSED_BYTES => { - let mut out = [0u8; BLS_G2_COMPRESSED_BYTES]; - out.copy_from_slice(encoding); - Ok(Self(out)) - } - n if n < BLS_G2_COMPRESSED_BYTES => Err(DeserializeError::ExpectedFurtherInput { - provided: n, - expected: BLS_G2_COMPRESSED_BYTES, - }), - n => Err(DeserializeError::AdditionalInput { - provided: n, - expected: BLS_G2_COMPRESSED_BYTES, - }), - } - } -} - -impl Merkleized for BLSSignature { - fn hash_tree_root(&mut self) -> Result { - Ok(merkleize_byte_sequence(&self.0)) - } -} - -impl SimpleSerialize for BLSSignature { - fn is_composite_type() -> bool { - true - } -} - -/// KZG commitment as SSZ-decoded bytes. -/// -/// Curve and subgroup validity are checked by the KZG verifier. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(transparent)] -pub struct KZGCommitment(pub [u8; KZG_COMMITMENT_BYTES]); - -impl Default for KZGCommitment { - fn default() -> Self { - Self([0; KZG_COMMITMENT_BYTES]) - } -} - -impl Sized for KZGCommitment { - fn is_variable_size() -> bool { - false - } - fn size_hint() -> usize { - KZG_COMMITMENT_BYTES - } -} - -impl Serialize for KZGCommitment { - fn serialize(&self, buffer: &mut Vec) -> Result { - buffer.extend_from_slice(&self.0); - Ok(KZG_COMMITMENT_BYTES) - } -} - -impl Deserialize for KZGCommitment { - fn deserialize(encoding: &[u8]) -> Result { - match encoding.len() { - n if n == KZG_COMMITMENT_BYTES => { - let mut out = [0u8; KZG_COMMITMENT_BYTES]; - out.copy_from_slice(encoding); - Ok(Self(out)) - } - n if n < KZG_COMMITMENT_BYTES => Err(DeserializeError::ExpectedFurtherInput { - provided: n, - expected: KZG_COMMITMENT_BYTES, - }), - n => Err(DeserializeError::AdditionalInput { - provided: n, - expected: KZG_COMMITMENT_BYTES, - }), - } - } -} - -impl Merkleized for KZGCommitment { - fn hash_tree_root(&mut self) -> Result { - Ok(merkleize_byte_sequence(&self.0)) - } -} - -impl SimpleSerialize for KZGCommitment { - fn is_composite_type() -> bool { - true - } -} - -/// 256-bit unsigned integer stored as 32 little-endian bytes per SSZ. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(transparent)] -pub struct Uint256(pub [u8; UINT256_BYTES]); - -impl Sized for Uint256 { - fn is_variable_size() -> bool { - false - } - fn size_hint() -> usize { - UINT256_BYTES - } -} - -impl Serialize for Uint256 { - fn serialize(&self, buffer: &mut Vec) -> Result { - buffer.extend_from_slice(&self.0); - Ok(UINT256_BYTES) - } -} - -impl Deserialize for Uint256 { - fn deserialize(encoding: &[u8]) -> Result { - match encoding.len() { - n if n == UINT256_BYTES => { - let mut out = [0u8; UINT256_BYTES]; - out.copy_from_slice(encoding); - Ok(Self(out)) - } - n if n < UINT256_BYTES => Err(DeserializeError::ExpectedFurtherInput { - provided: n, - expected: UINT256_BYTES, - }), - n => Err(DeserializeError::AdditionalInput { - provided: n, - expected: UINT256_BYTES, - }), - } - } -} - -impl Merkleized for Uint256 { - fn hash_tree_root(&mut self) -> Result { - // Basic uintN values pack into a single chunk. For uint256 the chunk is the value itself. - Ok(Node::try_from(&self.0[..]).expect("32-byte uint fits Node")) - } -} - -impl SimpleSerialize for Uint256 { - fn is_composite_type() -> bool { - false - } -} - -/// 32-byte opaque payload. -pub type Bytes32 = [u8; 32]; - -impl From for Root { - fn from(node: Node) -> Self { - let mut bytes = [0u8; 32]; - bytes.copy_from_slice(node.as_ref()); - Self(bytes) - } -} - -/// Merkleize a fixed-size byte sequence as zero-padded 32-byte SSZ chunks. -fn merkleize_byte_sequence(bytes: &[u8]) -> Node { - let chunk_count = bytes.len().div_ceil(SSZ_CHUNK_BYTES); - let leaf_count = chunk_count.next_power_of_two().max(1); - - let mut leaves = vec![[0u8; SSZ_CHUNK_BYTES]; leaf_count]; - for (i, chunk) in bytes.chunks(SSZ_CHUNK_BYTES).enumerate() { - leaves[i][..chunk.len()].copy_from_slice(chunk); - } - - while leaves.len() > 1 { - leaves = leaves - .chunks(2) - .map(|pair| { - let mut h = Sha256::new(); - h.update(pair[0]); - h.update(pair[1]); - let mut out = [0u8; SSZ_CHUNK_BYTES]; - out.copy_from_slice(&h.finalize()); - out - }) - .collect(); - } - - // `Node::try_from(&[u8])` only fails on length mismatch. `leaves[0]` is always 32 bytes. - Node::try_from(&leaves[0][..]).expect("32-byte chunk fits Node") -} diff --git a/moonglass/src/primitives/numeric_ssz.rs b/moonglass/src/primitives/numeric_ssz.rs deleted file mode 100644 index c7f3f8d..0000000 --- a/moonglass/src/primitives/numeric_ssz.rs +++ /dev/null @@ -1,246 +0,0 @@ -//! SSZ `Serialize`/`Deserialize`/`Merkleized` impls for the numeric newtypes. - -use ssz_rs::prelude::*; - -use super::{ - BuilderIndex, CommitteeIndex, Epoch, Gwei, ParticipationFlags, Slot, ValidatorIndex, - WithdrawalIndex, -}; - -// SSZ impls for uint-wrapping newtypes. We don't derive `SimpleSerialize` on -// these because the derive treats them as containers and reports -// `is_composite_type() == true`, which is wrong for `uintN`-shaped basic -// values inside `List`/`Vector` (the outer collection would merkleize -// per-element instead of packing). - -impl Sized for Slot { - fn is_variable_size() -> bool { - u64::is_variable_size() - } - fn size_hint() -> usize { - u64::size_hint() - } -} -impl Serialize for Slot { - fn serialize(&self, buffer: &mut Vec) -> Result { - self.0.serialize(buffer) - } -} -impl Deserialize for Slot { - fn deserialize(encoding: &[u8]) -> Result { - u64::deserialize(encoding).map(Self) - } -} -impl Merkleized for Slot { - fn hash_tree_root(&mut self) -> Result { - self.0.hash_tree_root() - } -} -impl SimpleSerialize for Slot { - fn is_composite_type() -> bool { - false - } -} - -impl Sized for Epoch { - fn is_variable_size() -> bool { - u64::is_variable_size() - } - fn size_hint() -> usize { - u64::size_hint() - } -} -impl Serialize for Epoch { - fn serialize(&self, buffer: &mut Vec) -> Result { - self.0.serialize(buffer) - } -} -impl Deserialize for Epoch { - fn deserialize(encoding: &[u8]) -> Result { - u64::deserialize(encoding).map(Self) - } -} -impl Merkleized for Epoch { - fn hash_tree_root(&mut self) -> Result { - self.0.hash_tree_root() - } -} -impl SimpleSerialize for Epoch { - fn is_composite_type() -> bool { - false - } -} - -impl Sized for ValidatorIndex { - fn is_variable_size() -> bool { - u64::is_variable_size() - } - fn size_hint() -> usize { - u64::size_hint() - } -} -impl Serialize for ValidatorIndex { - fn serialize(&self, buffer: &mut Vec) -> Result { - self.0.serialize(buffer) - } -} -impl Deserialize for ValidatorIndex { - fn deserialize(encoding: &[u8]) -> Result { - u64::deserialize(encoding).map(Self) - } -} -impl Merkleized for ValidatorIndex { - fn hash_tree_root(&mut self) -> Result { - self.0.hash_tree_root() - } -} -impl SimpleSerialize for ValidatorIndex { - fn is_composite_type() -> bool { - false - } -} - -impl Sized for BuilderIndex { - fn is_variable_size() -> bool { - u64::is_variable_size() - } - fn size_hint() -> usize { - u64::size_hint() - } -} -impl Serialize for BuilderIndex { - fn serialize(&self, buffer: &mut Vec) -> Result { - self.0.serialize(buffer) - } -} -impl Deserialize for BuilderIndex { - fn deserialize(encoding: &[u8]) -> Result { - u64::deserialize(encoding).map(Self) - } -} -impl Merkleized for BuilderIndex { - fn hash_tree_root(&mut self) -> Result { - self.0.hash_tree_root() - } -} -impl SimpleSerialize for BuilderIndex { - fn is_composite_type() -> bool { - false - } -} - -impl Sized for CommitteeIndex { - fn is_variable_size() -> bool { - u64::is_variable_size() - } - fn size_hint() -> usize { - u64::size_hint() - } -} -impl Serialize for CommitteeIndex { - fn serialize(&self, buffer: &mut Vec) -> Result { - self.0.serialize(buffer) - } -} -impl Deserialize for CommitteeIndex { - fn deserialize(encoding: &[u8]) -> Result { - u64::deserialize(encoding).map(Self) - } -} -impl Merkleized for CommitteeIndex { - fn hash_tree_root(&mut self) -> Result { - self.0.hash_tree_root() - } -} -impl SimpleSerialize for CommitteeIndex { - fn is_composite_type() -> bool { - false - } -} - -impl Sized for WithdrawalIndex { - fn is_variable_size() -> bool { - u64::is_variable_size() - } - fn size_hint() -> usize { - u64::size_hint() - } -} -impl Serialize for WithdrawalIndex { - fn serialize(&self, buffer: &mut Vec) -> Result { - self.0.serialize(buffer) - } -} -impl Deserialize for WithdrawalIndex { - fn deserialize(encoding: &[u8]) -> Result { - u64::deserialize(encoding).map(Self) - } -} -impl Merkleized for WithdrawalIndex { - fn hash_tree_root(&mut self) -> Result { - self.0.hash_tree_root() - } -} -impl SimpleSerialize for WithdrawalIndex { - fn is_composite_type() -> bool { - false - } -} - -impl Sized for Gwei { - fn is_variable_size() -> bool { - u64::is_variable_size() - } - fn size_hint() -> usize { - u64::size_hint() - } -} -impl Serialize for Gwei { - fn serialize(&self, buffer: &mut Vec) -> Result { - self.0.serialize(buffer) - } -} -impl Deserialize for Gwei { - fn deserialize(encoding: &[u8]) -> Result { - u64::deserialize(encoding).map(Self) - } -} -impl Merkleized for Gwei { - fn hash_tree_root(&mut self) -> Result { - self.0.hash_tree_root() - } -} -impl SimpleSerialize for Gwei { - fn is_composite_type() -> bool { - false - } -} - -impl Sized for ParticipationFlags { - fn is_variable_size() -> bool { - u8::is_variable_size() - } - fn size_hint() -> usize { - u8::size_hint() - } -} -impl Serialize for ParticipationFlags { - fn serialize(&self, buffer: &mut Vec) -> Result { - self.0.serialize(buffer) - } -} -impl Deserialize for ParticipationFlags { - fn deserialize(encoding: &[u8]) -> Result { - u8::deserialize(encoding).map(Self) - } -} -impl Merkleized for ParticipationFlags { - fn hash_tree_root(&mut self) -> Result { - self.0.hash_tree_root() - } -} -impl SimpleSerialize for ParticipationFlags { - fn is_composite_type() -> bool { - false - } -} diff --git a/moonglass/src/state_transition/balance.rs b/moonglass/src/state_transition/balance.rs deleted file mode 100644 index 984173a..0000000 --- a/moonglass/src/state_transition/balance.rs +++ /dev/null @@ -1,332 +0,0 @@ -//! Balance behavior used by rewards and validator exits. -//! -//! Covers balance mutation (overflow-checked increases, saturating decreases), -//! total active balance, base-reward math, per-flag reward and penalty -//! distribution, the inactivity-leak penalty, and the slashing mutation. - -use crate::constants::{ - BASE_REWARD_FACTOR, EFFECTIVE_BALANCE_INCREMENT, EPOCHS_PER_SLASHINGS_VECTOR, GENESIS_EPOCH, - INACTIVITY_PENALTY_QUOTIENT, INACTIVITY_SCORE_BIAS, MIN_EPOCHS_TO_INACTIVITY_PENALTY, - MIN_SLASHING_PENALTY_QUOTIENT, PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, - TIMELY_HEAD_FLAG_INDEX, VALIDATOR_REGISTRY_LIMIT, WEIGHT_DENOMINATOR, - WHISTLEBLOWER_REWARD_QUOTIENT, -}; -use crate::containers::BeaconState; -use crate::error::{RegistryError, TransitionError}; -use crate::primitives::{Epoch, Gwei, ValidatorIndex}; -use crate::state_transition::BeaconStateLookup; - -/// Per-validator balance changes produced by an epoch accounting phase. -pub(crate) struct BalanceDeltas { - /// Rewards accumulated per validator index. - rewards: Vec, - /// Penalties accumulated per validator index. - penalties: Vec, -} - -impl BalanceDeltas { - /// Construct zeroed reward and penalty vectors for `validator_count` validators. - fn zeroed(validator_count: usize) -> Self { - Self { - rewards: vec![Gwei::ZERO; validator_count], - penalties: vec![Gwei::ZERO; validator_count], - } - } - - /// Apply the accumulated rewards, then penalties, to `balances`. - /// - /// A reward addition that overflows `u64` makes the transition invalid and - /// raises [`TransitionError::BalanceOverflow`]. Penalties saturate at zero, - /// matching the spec's underflow protection on `decrease_balance`. - pub(crate) fn apply_to( - self, - balances: &mut ssz_rs::List, - ) -> Result<(), TransitionError> { - for (i, reward) in self.rewards.iter().enumerate() { - if i < balances.len() { - balances[i] = balances[i] - .checked_add(*reward) - .ok_or(TransitionError::BalanceOverflow)?; - } - } - for (i, penalty) in self.penalties.iter().enumerate() { - if i < balances.len() { - balances[i] = balances[i].saturating_sub(*penalty); - } - } - Ok(()) - } -} - -impl BeaconState { - /// Add `delta` gwei to `index`'s balance. A `u64` overflow is invalid and - /// raises [`TransitionError::BalanceOverflow`]. - pub fn increase_balance( - &mut self, - index: ValidatorIndex, - delta: Gwei, - ) -> Result<(), TransitionError> { - let slot = self.balance_mut(index)?; - *slot = slot - .checked_add(delta) - .ok_or(TransitionError::BalanceOverflow)?; - Ok(()) - } - - /// Subtract `delta` gwei from `index`'s balance. Saturates at `Gwei(0)`. - pub fn decrease_balance( - &mut self, - index: ValidatorIndex, - delta: Gwei, - ) -> Result<(), TransitionError> { - let slot = self.balance_mut(index)?; - *slot = slot.saturating_sub(delta); - Ok(()) - } - - /// Return the mutable balance cell for an existing validator index. - fn balance_mut(&mut self, index: ValidatorIndex) -> Result<&mut Gwei, TransitionError> { - // Existence check: errors if `index` is out of bounds for the validator registry. - let _ = self.validator(index)?; - if index.as_usize() >= self.balances.len() { - return Err(RegistryError::ValidatorIndexOutOfRange(index.as_u64()).into()); - } - Ok(&mut self.balances[index.as_usize()]) - } - - /// Sum of effective balances over the active validator set, floored at - /// `EFFECTIVE_BALANCE_INCREMENT` so downstream divisions stay safe. - #[must_use] - pub fn total_active_balance(&self) -> Gwei { - let epoch = self.slot.epoch(); - let total: Gwei = self - .validators - .iter() - .filter(|v| v.is_active_at(epoch)) - .map(|v| v.effective_balance) - .fold(Gwei(0), Gwei::saturating_add); - total.max(EFFECTIVE_BALANCE_INCREMENT) - } - - /// Sum of effective balances over `indices`, floored at - /// `EFFECTIVE_BALANCE_INCREMENT`. - #[must_use] - pub fn total_balance(&self, indices: &[ValidatorIndex]) -> Gwei { - let total = indices - .iter() - .filter_map(|i| self.validators.get(i.as_usize())) - .map(|v| v.effective_balance) - .fold(Gwei(0), Gwei::saturating_add); - total.max(EFFECTIVE_BALANCE_INCREMENT) - } - - /// Base reward issued per `EFFECTIVE_BALANCE_INCREMENT` of effective balance - /// per epoch. The reciprocal scale knob for total issuance. - #[must_use] - pub fn base_reward_per_increment(&self) -> Gwei { - let total = self.total_active_balance().as_u64(); - let sqrt = isqrt_u64(total).max(1); - Gwei(EFFECTIVE_BALANCE_INCREMENT.as_u64() * BASE_REWARD_FACTOR / sqrt) - } - - /// Base reward for the validator at `index`. Errors if `index` is out of range. - pub fn base_reward(&self, index: ValidatorIndex) -> Result { - let v = self.validator(index)?; - let increments = v.effective_balance.as_u64() / EFFECTIVE_BALANCE_INCREMENT.as_u64(); - Ok(Gwei(increments * self.base_reward_per_increment().as_u64())) - } - - /// Total active-balance, expressed in `EFFECTIVE_BALANCE_INCREMENT` units. - #[must_use] - pub fn active_increments(&self) -> u64 { - self.total_active_balance().as_u64() / EFFECTIVE_BALANCE_INCREMENT.as_u64() - } - - /// Previous epoch from the state's current slot. Saturates at `GENESIS_EPOCH`. - #[must_use] - pub fn previous_epoch(&self) -> Epoch { - let current = self.slot.epoch(); - if current == GENESIS_EPOCH { - GENESIS_EPOCH - } else { - current.saturating_sub(1) - } - } - - /// True if finality has fallen far enough behind to trigger the inactivity leak. - #[must_use] - pub fn is_in_inactivity_leak(&self) -> bool { - let delay = self - .previous_epoch() - .as_u64() - .saturating_sub(self.finalized_checkpoint.epoch.as_u64()); - delay > MIN_EPOCHS_TO_INACTIVITY_PENALTY - } - - /// Validators eligible for per-epoch rewards or penalties. - #[must_use] - pub fn eligible_validator_indices(&self) -> Vec { - let previous = self.previous_epoch(); - self.validators - .iter() - .enumerate() - .filter_map(|(i, v)| { - let active = v.is_active_at(previous); - let post_slash = v.slashed && previous.saturating_add(1) < v.withdrawable_epoch; - (active || post_slash).then_some(ValidatorIndex(i as u64)) - }) - .collect() - } - - /// Indices of validators that were active in `epoch`, not slashed, and earned - /// the participation flag at `flag_index`. - pub fn unslashed_participating_indices( - &self, - flag_index: usize, - epoch: Epoch, - ) -> Result, TransitionError> { - let current = self.slot.epoch(); - let previous = self.previous_epoch(); - let participation = if epoch == current { - &self.current_epoch_participation - } else if epoch == previous { - &self.previous_epoch_participation - } else { - return Ok(Vec::new()); - }; - let mut out = Vec::new(); - for (i, v) in self.validators.iter().enumerate() { - if !v.is_active_at(epoch) || v.slashed { - continue; - } - let flags = participation.get(i).copied().unwrap_or_default(); - if flags.has_flag(flag_index)? { - out.push(ValidatorIndex(i as u64)); - } - } - Ok(out) - } - - /// Per-validator reward and penalty deltas for participation flag `flag_index`. - pub(crate) fn participation_flag_deltas( - &self, - flag_index: usize, - ) -> Result { - let len = self.validators.len(); - let mut deltas = BalanceDeltas::zeroed(len); - let previous = self.previous_epoch(); - let participating = self.unslashed_participating_indices(flag_index, previous)?; - let participating_set: std::collections::HashSet = - participating.iter().map(|i| i.as_u64()).collect(); - let weight = PARTICIPATION_FLAG_WEIGHTS - .get(flag_index) - .copied() - .unwrap_or(0); - let participating_increments = - self.total_balance(&participating).as_u64() / EFFECTIVE_BALANCE_INCREMENT.as_u64(); - let active_increments = self.active_increments().max(1); - let in_leak = self.is_in_inactivity_leak(); - for index in self.eligible_validator_indices() { - let base = self.base_reward(index)?.as_u64(); - if participating_set.contains(&index.as_u64()) { - if !in_leak { - let numerator = base - .saturating_mul(weight) - .saturating_mul(participating_increments); - let denom = active_increments.saturating_mul(WEIGHT_DENOMINATOR); - deltas.rewards[index.as_usize()] = deltas.rewards[index.as_usize()] - .saturating_add(Gwei(numerator / denom.max(1))); - } - } else if flag_index != TIMELY_HEAD_FLAG_INDEX { - let penalty = base.saturating_mul(weight) / WEIGHT_DENOMINATOR.max(1); - deltas.penalties[index.as_usize()] = - deltas.penalties[index.as_usize()].saturating_add(Gwei(penalty)); - } - } - Ok(deltas) - } - - /// Per-validator inactivity-leak deltas. - pub(crate) fn inactivity_penalty_deltas(&self) -> Result { - let len = self.validators.len(); - let mut deltas = BalanceDeltas::zeroed(len); - let previous = self.previous_epoch(); - let matching = self.unslashed_participating_indices( - crate::constants::TIMELY_TARGET_FLAG_INDEX, - previous, - )?; - let matching_set: std::collections::HashSet = - matching.iter().map(|i| i.as_u64()).collect(); - let denominator = INACTIVITY_SCORE_BIAS.saturating_mul(INACTIVITY_PENALTY_QUOTIENT); - for index in self.eligible_validator_indices() { - if matching_set.contains(&index.as_u64()) { - continue; - } - let v = &self.validators[index.as_usize()]; - let score = self - .inactivity_scores - .get(index.as_usize()) - .copied() - .unwrap_or(0); - let numerator = v.effective_balance.as_u64().saturating_mul(score); - deltas.penalties[index.as_usize()] = Gwei(numerator / denominator.max(1)); - } - Ok(deltas) - } - - /// Apply the slashing mutation to `slashed_index`. - pub fn slash_validator( - &mut self, - slashed_index: ValidatorIndex, - whistleblower_index: Option, - ) -> Result<(), TransitionError> { - let current_epoch = self.slot.epoch(); - self.initiate_validator_exit(slashed_index)?; - - let effective_balance = self.validator(slashed_index)?.effective_balance; - let v = &mut self.validators[slashed_index.as_usize()]; - v.slashed = true; - let extended = current_epoch.saturating_add(EPOCHS_PER_SLASHINGS_VECTOR as u64); - if extended > v.withdrawable_epoch { - v.withdrawable_epoch = extended; - } - - let slashings_slot = current_epoch % EPOCHS_PER_SLASHINGS_VECTOR; - let bucket = &mut self.slashings[slashings_slot]; - *bucket = bucket.saturating_add(effective_balance); - - self.decrease_balance( - slashed_index, - Gwei(effective_balance.as_u64() / MIN_SLASHING_PENALTY_QUOTIENT), - )?; - - let proposer_index = self.beacon_proposer_index()?; - let whistleblower = whistleblower_index.unwrap_or(proposer_index); - let whistleblower_reward = Gwei(effective_balance.as_u64() / WHISTLEBLOWER_REWARD_QUOTIENT); - let proposer_reward = - Gwei(whistleblower_reward.as_u64() * PROPOSER_WEIGHT / WEIGHT_DENOMINATOR); - self.increase_balance(proposer_index, proposer_reward)?; - self.increase_balance( - whistleblower, - Gwei(whistleblower_reward.as_u64() - proposer_reward.as_u64()), - )?; - Ok(()) - } -} - -/// Integer square root via Newton's method. -/// -/// Used by attestation reward timing, where timely source credit is bounded by -/// the square root of slots per epoch. Returns the largest `n` with `n*n <= x`. -pub(crate) fn isqrt_u64(x: u64) -> u64 { - if x < 2 { - return x; - } - let mut n = x; - let mut next = u64::midpoint(n, x / n); - while next < n { - n = next; - next = u64::midpoint(n, x / n); - } - n -} diff --git a/moonglass/src/state_transition/builder/lifecycle.rs b/moonglass/src/state_transition/builder/lifecycle.rs deleted file mode 100644 index 2c2a3b5..0000000 --- a/moonglass/src/state_transition/builder/lifecycle.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Builder registry lifecycle: activations, exits, slashings. - -use crate::constants::{ - EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, MIN_BUILDER_WITHDRAWABILITY_DELAY, - MIN_SLASHING_PENALTY_QUOTIENT, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR, - WHISTLEBLOWER_REWARD_QUOTIENT, -}; -use crate::containers::BeaconState; -use crate::error::TransitionError; -use crate::primitives::{BuilderIndex, Gwei, ValidatorIndex}; -use crate::state_transition::BeaconStateLookup; - -impl BeaconState { - /// True if the builder at `builder_index` is in the active set. - /// - /// A builder is active once its `deposit_epoch` is finalized and while its - /// `withdrawable_epoch` is still `FAR_FUTURE_EPOCH`, that is, it has not yet - /// initiated exit. Bid acceptance and builder exit both gate on this, so a - /// builder that has scheduled departure can no longer win a slot. An - /// out-of-range index raises a registry error. - /// Spec: `is_active_builder`. - pub fn is_active_builder(&self, builder_index: BuilderIndex) -> Result { - let builder = self.builder(builder_index)?; - Ok(builder.deposit_epoch < self.finalized_checkpoint.epoch - && builder.withdrawable_epoch == FAR_FUTURE_EPOCH) - } - - /// Schedule a builder's departure from the active set. - /// - /// A builder whose `withdrawable_epoch` is still `FAR_FUTURE_EPOCH` has it set - /// to the current epoch plus `MIN_BUILDER_WITHDRAWABILITY_DELAY`, after which - /// [`BeaconState::is_active_builder`] reports it inactive and its remaining - /// balance is swept. A builder already scheduled to exit is left unchanged, - /// so the call is idempotent. - pub fn initiate_builder_exit( - &mut self, - builder_index: BuilderIndex, - ) -> Result<(), TransitionError> { - let builder = self.builder(builder_index)?; - if builder.withdrawable_epoch != FAR_FUTURE_EPOCH { - return Ok(()); - } - let current_epoch = self.slot.epoch(); - let withdrawable = current_epoch.saturating_add(MIN_BUILDER_WITHDRAWABILITY_DELAY); - self.builders[builder_index.as_usize()].withdrawable_epoch = withdrawable; - Ok(()) - } - - /// Penalize a builder and reward the whistleblower and proposer. - /// - /// The builder is forced to exit, its balance is cut by the minimum slashing - /// penalty, and its `withdrawable_epoch` is pushed out far enough to keep the - /// stake observable through the slashings window. The whistleblower (the - /// block proposer when none is named) receives a reward, with the proposer - /// taking its weighted share. An out-of-range builder, proposer, or - /// whistleblower index raises a registry error. - pub fn slash_builder( - &mut self, - builder_index: BuilderIndex, - whistleblower_index: Option, - ) -> Result<(), TransitionError> { - let current_epoch = self.slot.epoch(); - let proposer_index = self.beacon_proposer_index()?; - let whistleblower = whistleblower_index.unwrap_or(proposer_index); - // Existence checks: error if either index is out of bounds. - let _ = self.validator(proposer_index)?; - let _ = self.validator(whistleblower)?; - let balance = self.builder(builder_index)?.balance; - - self.initiate_builder_exit(builder_index)?; - let penalty = Gwei(balance.as_u64() / MIN_SLASHING_PENALTY_QUOTIENT); - let builder = &mut self.builders[builder_index.as_usize()]; - builder.balance = builder.balance.saturating_sub(penalty); - let extended = current_epoch.saturating_add(EPOCHS_PER_SLASHINGS_VECTOR as u64); - if extended > builder.withdrawable_epoch { - builder.withdrawable_epoch = extended; - } - - let whistleblower_reward = Gwei(balance.as_u64() / WHISTLEBLOWER_REWARD_QUOTIENT); - let proposer_reward = - Gwei(whistleblower_reward.as_u64() * PROPOSER_WEIGHT / WEIGHT_DENOMINATOR); - self.increase_balance(proposer_index, proposer_reward)?; - self.increase_balance( - whistleblower, - Gwei(whistleblower_reward.as_u64() - proposer_reward.as_u64()), - )?; - Ok(()) - } -} diff --git a/moonglass/src/state_transition/committee.rs b/moonglass/src/state_transition/committee.rs deleted file mode 100644 index 58b8663..0000000 --- a/moonglass/src/state_transition/committee.rs +++ /dev/null @@ -1,309 +0,0 @@ -//! Committee and proposer-assignment behavior. -//! -//! Committees distribute validator duties so no small, predictable set controls -//! attestations for a slot. The flow is: collect the active validator set, -//! derive a RANDAO-backed seed, conceptually shuffle the active set, then slice -//! it into committees across the epoch. Proposer sampling uses the same -//! randomness with effective-balance weighting. Per-slot proposer lookup reads -//! the precomputed `state.proposer_lookahead`. - -use sha2::{Digest, Sha256}; - -use crate::constants::{ - DOMAIN_BEACON_ATTESTER, DOMAIN_PTC_ATTESTER, EPOCHS_PER_HISTORICAL_VECTOR, - MAX_COMMITTEES_PER_SLOT, MAX_EFFECTIVE_BALANCE, MIN_SEED_LOOKAHEAD, PTC_SIZE, - SHUFFLE_ROUND_COUNT, SLOTS_PER_EPOCH, TARGET_COMMITTEE_SIZE, -}; -use crate::containers::BeaconState; -use crate::error::{BlockError, TransitionError}; -use crate::primitives::{Bytes32, CommitteeIndex, DomainType, Epoch, Slot, ValidatorIndex}; -use crate::state_transition::BeaconStateLookup; - -/// Random-sample cap of `2**16 - 1` used by effective-balance-weighted -/// validator sampling in [`BeaconState::compute_proposer_index`] and [`BeaconState::next_sync_committee_indices`]. -pub(crate) const MAX_RANDOM_VALUE: u64 = (1 << 16) - 1; - -/// Convert a protocol index into a host slice index after the caller has -/// bounded it by an allocated collection length. -fn u64_to_usize(value: u64) -> usize { - usize::try_from(value).expect("protocol index fits host usize") -} - -/// Convert a host slice index into the protocol's `uint64` index domain. -fn usize_to_u64(value: usize) -> u64 { - u64::try_from(value).expect("host index fits protocol u64") -} - -impl BeaconState { - /// Proposer for the state's current slot. - pub fn beacon_proposer_index(&self) -> Result { - let offset = self.slot % SLOTS_PER_EPOCH; - self.proposer_lookahead - .get(offset) - .copied() - .ok_or_else(|| BlockError::ProposerLookaheadOutOfRange(self.slot).into()) - } - - /// Indices of validators active during `epoch`. - #[must_use] - pub fn active_validator_indices(&self, epoch: Epoch) -> Vec { - self.validators - .iter() - .enumerate() - .filter_map(|(i, v)| v.is_active_at(epoch).then_some(ValidatorIndex(i as u64))) - .collect() - } - - /// Indices of validators active and not slashed during `epoch`. - /// - /// Used for proposer selection: the candidate set excludes slashed validators. - #[must_use] - pub fn active_unslashed_validator_indices(&self, epoch: Epoch) -> Vec { - self.validators - .iter() - .enumerate() - .filter_map(|(i, v)| { - (v.is_active_at(epoch) && !v.slashed).then_some(ValidatorIndex(i as u64)) - }) - .collect() - } - - /// RANDAO ring-buffer slot for `epoch`. - #[must_use] - pub fn randao_mix(&self, epoch: Epoch) -> Bytes32 { - self.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] - } - - /// 32-byte seed mixing the domain tag, the epoch, and the randao value. - #[must_use] - pub fn seed(&self, epoch: Epoch, domain_type: DomainType) -> Bytes32 { - let lookback = EPOCHS_PER_HISTORICAL_VECTOR as u64 - MIN_SEED_LOOKAHEAD as u64 - 1; - let mix = self.randao_mix(epoch.saturating_add(lookback)); - let mut hasher = Sha256::new(); - hasher.update(domain_type.0); - hasher.update(epoch.as_u64().to_le_bytes()); - hasher.update(mix); - hasher.finalize().into() - } - - /// Number of beacon committees produced per slot in `epoch`. - #[must_use] - pub fn committee_count_per_slot(&self, epoch: Epoch) -> u64 { - let active = self.active_validator_indices(epoch).len() as u64; - let raw = active / SLOTS_PER_EPOCH as u64 / TARGET_COMMITTEE_SIZE; - raw.min(MAX_COMMITTEES_PER_SLOT as u64).max(1) - } - - /// Beacon committee at (`slot`, `committee_index`). - pub fn beacon_committee( - &self, - slot: Slot, - committee_index: CommitteeIndex, - ) -> Result, TransitionError> { - let epoch = slot.epoch(); - let committees_per_slot = self.committee_count_per_slot(epoch); - if committee_index.as_u64() >= committees_per_slot { - return Err(BlockError::CommitteeIndexOutOfRange(committee_index).into()); - } - let indices = self.active_validator_indices(epoch); - let seed = self.seed(epoch, DOMAIN_BEACON_ATTESTER); - let count = committees_per_slot * SLOTS_PER_EPOCH as u64; - let index = - (slot % SLOTS_PER_EPOCH as u64) * committees_per_slot + committee_index.as_u64(); - Ok(compute_committee(&indices, seed, index, count)) - } - - /// Effective-balance-weighted random proposer sample from `indices`. - /// - /// Spec: `compute_proposer_index`. - pub fn compute_proposer_index( - &self, - indices: &[ValidatorIndex], - seed: Bytes32, - ) -> Result { - if indices.is_empty() { - return Err(BlockError::EmptyActiveValidatorSet.into()); - } - let total = indices.len() as u64; - let mut i: u64 = 0; - loop { - let candidate_index = u64_to_usize(compute_shuffled_index(i % total, total, seed)); - let candidate = indices[candidate_index]; - let random_bytes = { - let mut hasher = Sha256::new(); - hasher.update(seed); - hasher.update((i / 16).to_le_bytes()); - hasher.finalize() - }; - let offset = u64_to_usize((i % 16) * 2); - let random_value = u64::from(u16::from_le_bytes([ - random_bytes[offset], - random_bytes[offset + 1], - ])); - let effective_balance = self.validator(candidate)?.effective_balance.as_u64(); - if effective_balance.saturating_mul(MAX_RANDOM_VALUE) - >= MAX_EFFECTIVE_BALANCE.as_u64().saturating_mul(random_value) - { - return Ok(candidate); - } - i = i.saturating_add(1); - } - } - - /// Sample `size` indices from `candidates`, weighted by each candidate's - /// effective balance. When `shuffle_indices` is true the candidate ordering - /// is itself permuted through [`compute_shuffled_index`]. Otherwise the - /// candidate list is traversed in order. Duplicates are possible. - pub fn compute_balance_weighted_selection( - &self, - candidates: &[ValidatorIndex], - seed: Bytes32, - size: usize, - shuffle_indices: bool, - ) -> Result, TransitionError> { - if candidates.is_empty() { - return Err(BlockError::EmptyActiveValidatorSet.into()); - } - let total = candidates.len() as u64; - let effective_balances: Vec = candidates - .iter() - .map(|i| self.validator(*i).map(|v| v.effective_balance.as_u64())) - .collect::, _>>()?; - let mut selected: Vec = Vec::with_capacity(size); - let mut i: u64 = 0; - let mut random_bytes = [0_u8; 32]; - while selected.len() < size { - let offset = u64_to_usize((i % 16) * 2); - if offset == 0 { - let mut hasher = Sha256::new(); - hasher.update(seed); - hasher.update((i / 16).to_le_bytes()); - random_bytes = hasher.finalize().into(); - } - let mut next_index = i % total; - if shuffle_indices { - next_index = compute_shuffled_index(next_index, total, seed); - } - let next_index_usize = u64_to_usize(next_index); - let weight = effective_balances[next_index_usize].saturating_mul(MAX_RANDOM_VALUE); - let random_value = u64::from(u16::from_le_bytes([ - random_bytes[offset], - random_bytes[offset + 1], - ])); - let threshold = MAX_EFFECTIVE_BALANCE.as_u64().saturating_mul(random_value); - if weight >= threshold { - selected.push(candidates[next_index_usize]); - } - i = i.saturating_add(1); - } - Ok(selected) - } - - /// Payload-timeliness committee for `slot`. Concatenates every beacon - /// committee at this slot, derives a slot-specific seed, then samples - /// `PTC_SIZE` indices by effective balance without further shuffling. - pub fn compute_ptc(&self, slot: Slot) -> Result, TransitionError> { - let epoch = slot.epoch(); - let base_seed = self.seed(epoch, DOMAIN_PTC_ATTESTER); - let seed: Bytes32 = { - let mut hasher = Sha256::new(); - hasher.update(base_seed); - hasher.update(slot.as_u64().to_le_bytes()); - hasher.finalize().into() - }; - let committees_per_slot = self.committee_count_per_slot(epoch); - let mut indices: Vec = Vec::new(); - for ci in 0..committees_per_slot { - let committee = self.beacon_committee(slot, CommitteeIndex(ci))?; - indices.extend(committee); - } - self.compute_balance_weighted_selection(&indices, seed, PTC_SIZE, false) - } -} - -/// Swap-or-not shuffle locating `index` inside a population of `index_count` -/// -/// elements, seeded by `seed`. -/// The result is deterministic across `SHUFFLE_ROUND_COUNT` rounds. -/// # Panics -/// Panics if `index >= index_count`. Callers must validate the bound. -#[must_use] -pub fn compute_shuffled_index(index: u64, index_count: u64, seed: Bytes32) -> u64 { - assert!(index < index_count, "shuffle index out of range"); - let mut current = index; - for round in 0..SHUFFLE_ROUND_COUNT { - let round_byte = u8::try_from(round).expect("SHUFFLE_ROUND_COUNT fits in u8"); - let pivot = pivot_for_round(seed, round_byte, index_count); - let flip = (pivot + index_count - current) % index_count; - let position = current.max(flip); - let bit = source_bit(seed, round_byte, position); - if bit == 1 { - current = flip; - } - } - current -} - -/// Round pivot: lower 8 bytes of `hash(seed || round)`, mod `index_count`. -fn pivot_for_round(seed: Bytes32, round_byte: u8, index_count: u64) -> u64 { - let mut hasher = Sha256::new(); - hasher.update(seed); - hasher.update([round_byte]); - let digest = hasher.finalize(); - u64::from_le_bytes(digest[..8].try_into().expect("8-byte prefix")) % index_count -} - -/// Bit selector for the swap-or-not source, derived from `hash(seed || round -/// || position/256)`. -fn source_bit(seed: Bytes32, round_byte: u8, position: u64) -> u8 { - let position_chunk = u32::try_from(position / 256).expect("position chunk fits in u32"); - let mut hasher = Sha256::new(); - hasher.update(seed); - hasher.update([round_byte]); - hasher.update(position_chunk.to_le_bytes()); - let source = hasher.finalize(); - let byte_index = ((position % 256) / 8) as usize; - let bit_index = (position % 8) as u8; - (source[byte_index] >> bit_index) & 1 -} - -/// Slice of shuffled indices forming committee `index` of `count`. -/// -/// Example in words: if an epoch has `SLOTS_PER_EPOCH * committees_per_slot` -/// committees, each committee index selects one contiguous slice of the -/// shuffled active set. -#[must_use] -pub fn compute_committee( - indices: &[ValidatorIndex], - seed: Bytes32, - index: u64, - count: u64, -) -> Vec { - let total = indices.len() as u64; - let start = u64_to_usize(total * index / count); - let end = u64_to_usize(total * (index + 1) / count); - (start..end) - .map(|i| { - let shuffled = compute_shuffled_index(usize_to_u64(i), total, seed); - indices[u64_to_usize(shuffled)] - }) - .collect() -} - -/// Set committee indices encoded by a `committee_bits` bitvector. -#[must_use] -pub fn committee_indices( - committee_bits: &ssz_rs::Bitvector, -) -> Vec { - committee_bits - .iter() - .enumerate() - .filter_map(|(i, bit)| { - if *bit { - Some(CommitteeIndex(i as u64)) - } else { - None - } - }) - .collect() -} diff --git a/moonglass/src/state_transition/epoch/sync_committees.rs b/moonglass/src/state_transition/epoch/sync_committees.rs deleted file mode 100644 index 1b8268d..0000000 --- a/moonglass/src/state_transition/epoch/sync_committees.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Next sync-committee selection. - -use sha2::{Digest, Sha256}; - -use crate::constants::{ - DOMAIN_SYNC_COMMITTEE, EPOCHS_PER_SYNC_COMMITTEE_PERIOD, MAX_EFFECTIVE_BALANCE, - SYNC_COMMITTEE_SIZE, -}; -use crate::containers::{BeaconState, SyncCommittee}; -use crate::error::{BlockError, TransitionError}; -use crate::primitives::{BLSPubkey, Epoch, ValidatorIndex}; -use crate::state_transition::committee::MAX_RANDOM_VALUE; -use crate::state_transition::{BeaconStateLookup, aggregate_pubkeys, compute_shuffled_index}; - -impl BeaconState { - /// At a sync-committee period boundary, roll next into current and resample - /// the next sync committee from the active set at the following epoch. - /// Spec: `process_sync_committee_updates` - pub fn process_sync_committee_updates(&mut self) -> Result<(), TransitionError> { - let next_epoch = self.slot.epoch().as_u64() + 1; - if next_epoch.is_multiple_of(EPOCHS_PER_SYNC_COMMITTEE_PERIOD) { - let new_next_sync_committee = self.compute_next_sync_committee()?; - self.current_sync_committee = - std::mem::replace(&mut self.next_sync_committee, new_next_sync_committee); - } - Ok(()) - } - - /// Sample a fresh sync committee for the next sync-committee period. - fn compute_next_sync_committee(&self) -> Result { - let target = Epoch::new(self.slot.epoch().as_u64() + 1); - let indices = self.next_sync_committee_indices(target)?; - let pubkeys: Vec = indices - .iter() - .map(|i| self.validator(*i).map(|v| v.pubkey)) - .collect::>()?; - let aggregate_pubkey = aggregate_pubkeys(&pubkeys)?; - let mut pk_vec = ssz_rs::Vector::::default(); - for (i, pk) in pubkeys.iter().enumerate().take(SYNC_COMMITTEE_SIZE) { - pk_vec[i] = *pk; - } - Ok(SyncCommittee { - pubkeys: pk_vec, - aggregate_pubkey, - }) - } - - /// Effective-balance-weighted sampling of `SYNC_COMMITTEE_SIZE` validators - /// from the active set at `epoch`. - fn next_sync_committee_indices( - &self, - epoch: Epoch, - ) -> Result, TransitionError> { - let active = self.active_validator_indices(epoch); - let active_count = active.len() as u64; - if active_count == 0 { - return Err(BlockError::EmptyActiveValidatorSet.into()); - } - let seed = self.seed(epoch, DOMAIN_SYNC_COMMITTEE); - let mut out: Vec = Vec::with_capacity(SYNC_COMMITTEE_SIZE); - let mut i: u64 = 0; - while out.len() < SYNC_COMMITTEE_SIZE { - let shuffled = compute_shuffled_index(i % active_count, active_count, seed); - let candidate = active[usize::try_from(shuffled).expect("shuffled index fits usize")]; - let random_bytes = { - let mut hasher = Sha256::new(); - hasher.update(seed); - hasher.update((i / 16).to_le_bytes()); - hasher.finalize() - }; - let offset = usize::try_from((i % 16) * 2).expect("random-byte offset fits usize"); - let random_value = u64::from(u16::from_le_bytes([ - random_bytes[offset], - random_bytes[offset + 1], - ])); - let effective = self.validator(candidate)?.effective_balance.as_u64(); - if effective.saturating_mul(MAX_RANDOM_VALUE) - >= MAX_EFFECTIVE_BALANCE.as_u64().saturating_mul(random_value) - { - out.push(candidate); - } - i = i.saturating_add(1); - } - Ok(out) - } -} diff --git a/moonglass/src/state_transition/withdrawal.rs b/moonglass/src/state_transition/withdrawal.rs deleted file mode 100644 index a28b4ad..0000000 --- a/moonglass/src/state_transition/withdrawal.rs +++ /dev/null @@ -1,428 +0,0 @@ -//! Withdrawal-sweep transition phases. -//! -//! Computes the per-slot expected withdrawals (queued builder withdrawals -//! first, then pending partial withdrawals, then a builder sweep, then a -//! validator sweep) and stores them in `state.payload_expected_withdrawals` -//! for the execution-payload path to verify. Validator and builder balances -//! move here. - -use crate::constants::{ - FAR_FUTURE_EPOCH, MAX_BUILDERS_PER_WITHDRAWALS_SWEEP, - MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP, - MAX_WITHDRAWALS_PER_PAYLOAD, MIN_ACTIVATION_BALANCE, -}; -use crate::containers::{BeaconState, Withdrawal}; -use crate::error::{RegistryError, TransitionError}; -use crate::primitives::{BuilderIndex, ExecutionAddress, Gwei, ValidatorIndex, WithdrawalIndex}; - -/// Per-sweep accounting that flows from `expected_withdrawals` into the -/// post-state update step. -pub(crate) struct ExpectedWithdrawals { - /// Ordered withdrawals the next execution payload must include. - withdrawals: Vec, - /// Number of builder pending-withdrawal queue entries consumed. - processed_builder_withdrawals_count: u64, - /// Number of pending partial-withdrawal entries consumed. - processed_partial_withdrawals_count: u64, - /// Number of builders visited by the builder sweep. - processed_builders_sweep_count: u64, -} - -/// Extract the execution withdrawal address from withdrawal credentials. -fn withdrawal_address_from_credentials(credentials: &[u8; 32]) -> ExecutionAddress { - let mut address = [0u8; 20]; - address.copy_from_slice(&credentials[12..]); - ExecutionAddress(address) -} - -/// Encode a builder index into the withdrawal path's validator-index namespace. -fn builder_index_to_validator_index(idx: BuilderIndex) -> ValidatorIndex { - idx.to_validator_index() - .expect("builder index fits builder-index encoding") -} - -impl BeaconState { - /// Sum of in-flight withdrawals already chosen for `validator_index` during - /// the current sweep. Lets later steps in the same sweep observe the - /// post-withdrawal balance without mutating state mid-sweep. - fn balance_after_withdrawals( - &self, - validator_index: ValidatorIndex, - prior: &[Withdrawal], - ) -> Gwei { - let starting = self - .balances - .get(validator_index.as_usize()) - .copied() - .unwrap_or(Gwei::ZERO); - let withdrawn: u64 = prior - .iter() - .filter(|w| w.validator_index == validator_index) - .map(|w| w.amount.as_u64()) - .sum(); - Gwei(starting.as_u64().saturating_sub(withdrawn)) - } - - /// Build withdrawals from the explicit builder pending-withdrawal queue. - /// - /// These are emitted before partial withdrawals and sweeps, capped at one - /// less than the payload limit so later sweep phases can still contribute. - fn get_builder_withdrawals( - &self, - withdrawal_index: WithdrawalIndex, - ) -> Result<(Vec, u64), TransitionError> { - let withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD.saturating_sub(1); - let mut withdrawals: Vec = Vec::new(); - let mut next_index = withdrawal_index; - let mut processed: u64 = 0; - for entry in self.builder_pending_withdrawals.iter() { - if withdrawals.len() >= withdrawals_limit { - break; - } - if entry.builder_index.as_usize() >= self.builders.len() { - return Err( - RegistryError::BuilderIndexOutOfRange(entry.builder_index.as_u64()).into(), - ); - } - withdrawals.push(Withdrawal { - index: next_index, - validator_index: builder_index_to_validator_index(entry.builder_index), - address: entry.fee_recipient, - amount: entry.amount, - }); - next_index = WithdrawalIndex(next_index.as_u64().saturating_add(1)); - processed = processed.saturating_add(1); - } - Ok((withdrawals, processed)) - } - - /// Build withdrawals from finalized pending partial-withdrawal requests. - /// - /// Queue entries that are due but no longer eligible are still counted as - /// processed so the queue can drain. - fn get_pending_partial_withdrawals( - &self, - mut withdrawal_index: WithdrawalIndex, - prior_withdrawals: &[Withdrawal], - ) -> Result<(Vec, u64), TransitionError> { - let epoch = self.slot.epoch(); - let max_pending_partials = usize::try_from(MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP) - .expect("pending-partial sweep limit fits host usize"); - let withdrawals_limit = prior_withdrawals - .len() - .saturating_add(max_pending_partials) - .min(MAX_WITHDRAWALS_PER_PAYLOAD.saturating_sub(1)); - let mut withdrawals: Vec = Vec::new(); - let mut processed: u64 = 0; - for entry in self.pending_partial_withdrawals.iter() { - let all_count = prior_withdrawals.len() + withdrawals.len(); - if entry.withdrawable_epoch > epoch || all_count >= withdrawals_limit { - break; - } - if entry.validator_index.as_usize() >= self.validators.len() { - return Err(RegistryError::ValidatorIndexOutOfRange( - entry.validator_index.as_u64(), - ) - .into()); - } - let validator = &self.validators[entry.validator_index.as_usize()]; - let combined: Vec = prior_withdrawals - .iter() - .copied() - .chain(withdrawals.iter().copied()) - .collect(); - let balance = self.balance_after_withdrawals(entry.validator_index, &combined); - // `is_eligible_for_partial_withdrawals`: validator not yet - // exiting, effective balance at or above the activation floor, - // and a strict excess over `MIN_ACTIVATION_BALANCE`. Queue entries - // that fail eligibility are still consumed so the queue can drain. - let not_exiting = validator.exit_epoch == FAR_FUTURE_EPOCH; - let has_sufficient_effective = validator.effective_balance >= MIN_ACTIVATION_BALANCE; - let has_excess = balance > MIN_ACTIVATION_BALANCE; - if not_exiting && has_sufficient_effective && has_excess { - let max_withdraw = balance - .as_u64() - .saturating_sub(MIN_ACTIVATION_BALANCE.as_u64()); - let amount = Gwei(entry.amount.as_u64().min(max_withdraw)); - withdrawals.push(Withdrawal { - index: withdrawal_index, - validator_index: entry.validator_index, - address: withdrawal_address_from_credentials(&validator.withdrawal_credentials), - amount, - }); - withdrawal_index = WithdrawalIndex(withdrawal_index.as_u64().saturating_add(1)); - } - processed = processed.saturating_add(1); - } - Ok((withdrawals, processed)) - } - - /// Sweep builders from `next_withdrawal_builder_index` for withdrawable balances. - /// - /// Returns both withdrawals and the number of builder records visited so the - /// cursor can be advanced by withdrawal processing during block transition. - fn get_builders_sweep_withdrawals( - &self, - mut withdrawal_index: WithdrawalIndex, - prior_withdrawals: &[Withdrawal], - ) -> Result<(Vec, u64), TransitionError> { - let epoch = self.slot.epoch(); - let builder_len = self.builders.len(); - if builder_len == 0 { - return Ok((Vec::new(), 0)); - } - if self.next_withdrawal_builder_index.as_usize() >= builder_len { - return Err(RegistryError::BuilderIndexOutOfRange( - self.next_withdrawal_builder_index.as_u64(), - ) - .into()); - } - let builders_limit = usize::try_from(MAX_BUILDERS_PER_WITHDRAWALS_SWEEP) - .expect("builder sweep limit fits host usize") - .min(builder_len); - let withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD.saturating_sub(1); - - let mut withdrawals: Vec = Vec::new(); - let mut processed: u64 = 0; - let mut cursor = self.next_withdrawal_builder_index.as_usize(); - for _ in 0..builders_limit { - let all_count = prior_withdrawals.len() + withdrawals.len(); - if all_count >= withdrawals_limit { - break; - } - let builder = &self.builders[cursor]; - if builder.withdrawable_epoch <= epoch && builder.balance.as_u64() > 0 { - withdrawals.push(Withdrawal { - index: withdrawal_index, - validator_index: builder_index_to_validator_index(BuilderIndex(cursor as u64)), - address: builder.execution_address, - amount: builder.balance, - }); - withdrawal_index = WithdrawalIndex(withdrawal_index.as_u64().saturating_add(1)); - } - cursor = (cursor + 1) % builder_len; - processed = processed.saturating_add(1); - } - Ok((withdrawals, processed)) - } - - /// Sweep validators from `next_withdrawal_validator_index` for full or - /// partial withdrawals. - fn get_validators_sweep_withdrawals( - &self, - mut withdrawal_index: WithdrawalIndex, - prior_withdrawals: &[Withdrawal], - ) -> Result, TransitionError> { - let epoch = self.slot.epoch(); - let registry_len = self.validators.len(); - if registry_len == 0 { - return Ok(Vec::new()); - } - if self.next_withdrawal_validator_index.as_usize() >= registry_len { - return Err(RegistryError::ValidatorIndexOutOfRange( - self.next_withdrawal_validator_index.as_u64(), - ) - .into()); - } - let validators_limit = usize::try_from(MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) - .expect("validator sweep limit fits host usize") - .min(registry_len); - let withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD; - - let mut withdrawals: Vec = Vec::new(); - let mut cursor = self.next_withdrawal_validator_index.as_usize(); - for _ in 0..validators_limit { - let all_count = prior_withdrawals.len() + withdrawals.len(); - if all_count >= withdrawals_limit { - break; - } - let validator = &self.validators[cursor]; - let combined: Vec = prior_withdrawals - .iter() - .copied() - .chain(withdrawals.iter().copied()) - .collect(); - let validator_index = ValidatorIndex(cursor as u64); - let balance = self.balance_after_withdrawals(validator_index, &combined); - let address = withdrawal_address_from_credentials(&validator.withdrawal_credentials); - if validator.is_fully_withdrawable_at(balance, epoch) { - withdrawals.push(Withdrawal { - index: withdrawal_index, - validator_index, - address, - amount: balance, - }); - withdrawal_index = WithdrawalIndex(withdrawal_index.as_u64().saturating_add(1)); - } else if validator.is_partially_withdrawable(balance) { - let max = validator.max_effective_balance(); - let amount = balance.saturating_sub(max); - if amount.as_u64() > 0 { - withdrawals.push(Withdrawal { - index: withdrawal_index, - validator_index, - address, - amount, - }); - withdrawal_index = WithdrawalIndex(withdrawal_index.as_u64().saturating_add(1)); - } - } - cursor = (cursor + 1) % registry_len; - } - Ok(withdrawals) - } - - /// Compute the withdrawals expected in the next execution payload and the - /// queue/cursor deltas needed after applying them. - fn expected_withdrawals(&self) -> Result { - let mut withdrawals: Vec = Vec::new(); - let mut next_index = self.next_withdrawal_index; - - let (builder_withdrawals, processed_builder_withdrawals_count) = - self.get_builder_withdrawals(next_index)?; - if let Some(last) = builder_withdrawals.last() { - next_index = WithdrawalIndex(last.index.as_u64().saturating_add(1)); - } - withdrawals.extend(builder_withdrawals); - - let (partial_withdrawals, processed_partial_withdrawals_count) = - self.get_pending_partial_withdrawals(next_index, &withdrawals)?; - if let Some(last) = partial_withdrawals.last() { - next_index = WithdrawalIndex(last.index.as_u64().saturating_add(1)); - } - withdrawals.extend(partial_withdrawals); - - let (builders_sweep_withdrawals, processed_builders_sweep_count) = - self.get_builders_sweep_withdrawals(next_index, &withdrawals)?; - if let Some(last) = builders_sweep_withdrawals.last() { - next_index = WithdrawalIndex(last.index.as_u64().saturating_add(1)); - } - withdrawals.extend(builders_sweep_withdrawals); - - let validators_sweep_withdrawals = - self.get_validators_sweep_withdrawals(next_index, &withdrawals)?; - withdrawals.extend(validators_sweep_withdrawals); - - Ok(ExpectedWithdrawals { - withdrawals, - processed_builder_withdrawals_count, - processed_partial_withdrawals_count, - processed_builders_sweep_count, - }) - } - - /// Compute expected withdrawals for the current payload branch and apply them. - /// - /// If the latest settled execution block hash does not match the latest bid's - /// promised block hash, the payload branch is not settled and this phase is a - /// no-op. Otherwise it drains the builder pending-withdrawals queue and - /// partial-withdrawals queue against their per-sweep limits, mirrors the - /// selected withdrawals into `payload_expected_withdrawals`, then rotates the - /// builder and validator sweep cursors. - /// # Panics - /// Panics on the invariant that any `validator_index` already tagged with - /// `BUILDER_INDEX_FLAG` can be decoded back into a `BuilderIndex`. - /// Spec: `process_withdrawals` - pub fn process_withdrawals(&mut self) -> Result<(), TransitionError> { - if self.latest_block_hash != self.latest_execution_payload_bid.block_hash { - return Ok(()); - } - - let expected = self.expected_withdrawals()?; - - // Apply balance changes. - for w in &expected.withdrawals { - if w.validator_index.is_builder_index() { - let builder_index = w - .validator_index - .to_builder_index() - .expect("builder-index flag set"); - let idx = builder_index.as_usize(); - if idx < self.builders.len() { - let current = self.builders[idx].balance; - let drop = Gwei(w.amount.as_u64().min(current.as_u64())); - self.builders[idx].balance = current.saturating_sub(drop); - } - } else { - self.decrease_balance(w.validator_index, w.amount)?; - } - } - - // Update next_withdrawal_index. - if let Some(last) = expected.withdrawals.last() { - self.next_withdrawal_index = WithdrawalIndex(last.index.as_u64().saturating_add(1)); - } - - // Mirror the chosen withdrawals onto the payload-expected queue. - self.payload_expected_withdrawals = ssz_rs::List::default(); - for w in &expected.withdrawals { - self.payload_expected_withdrawals.push(*w); - } - - // Drain the consumed prefix of the builder pending withdrawals queue. - if expected.processed_builder_withdrawals_count > 0 { - let remaining: Vec<_> = self - .builder_pending_withdrawals - .iter() - .skip( - usize::try_from(expected.processed_builder_withdrawals_count) - .expect("processed builder withdrawal count fits usize"), - ) - .copied() - .collect(); - self.builder_pending_withdrawals = ssz_rs::List::default(); - for item in remaining { - self.builder_pending_withdrawals.push(item); - } - } - - // Drain the consumed prefix of the pending-partial-withdrawals queue. - if expected.processed_partial_withdrawals_count > 0 { - let remaining: Vec<_> = self - .pending_partial_withdrawals - .iter() - .skip( - usize::try_from(expected.processed_partial_withdrawals_count) - .expect("processed partial withdrawal count fits usize"), - ) - .copied() - .collect(); - self.pending_partial_withdrawals = ssz_rs::List::default(); - for item in remaining { - self.pending_partial_withdrawals.push(item); - } - } - - // Rotate the builder sweep cursor. - let builder_len = self.builders.len(); - if builder_len > 0 { - let next = self - .next_withdrawal_builder_index - .as_u64() - .saturating_add(expected.processed_builders_sweep_count) - % builder_len as u64; - self.next_withdrawal_builder_index = BuilderIndex(next); - } - - // Rotate the validator sweep cursor. - // - // Spec: `update_next_withdrawal_validator_index`. The mod takes the - // last withdrawal's `validator_index` as-is, even when the last entry - // came from the builder sweep with the `BUILDER_INDEX_FLAG` bit set. - let registry_len = self.validators.len(); - if registry_len > 0 { - if expected.withdrawals.len() == MAX_WITHDRAWALS_PER_PAYLOAD { - if let Some(last) = expected.withdrawals.last() { - let next = (last.validator_index.as_u64() + 1) % registry_len as u64; - self.next_withdrawal_validator_index = ValidatorIndex(next); - } - } else { - let advance = self.next_withdrawal_validator_index.as_u64() - + MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP; - self.next_withdrawal_validator_index = - ValidatorIndex(advance % registry_len as u64); - } - } - - Ok(()) - } -} diff --git a/reftests/src/harness/report.rs b/reftests/src/harness/report.rs deleted file mode 100644 index cdd4e81..0000000 --- a/reftests/src/harness/report.rs +++ /dev/null @@ -1,295 +0,0 @@ -//! Cargo-style reporting for reference-test runs. -//! -//! Skipped fixtures are reported as ignored cases so unsupported or -//! metadata-excluded coverage remains visible without changing the exit code. - -use std::collections::BTreeMap; -use std::io; -use std::time::Duration; - -use super::color::{Color, Style}; -use crate::adapters::Outcome; -use crate::inventory::Case; - -#[derive(Debug, Clone)] -struct Failure { - case_path: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct IgnoredKey { - path: String, - reason: String, -} - -#[derive(Debug, Default)] -pub(crate) struct Summary { - totals: Totals, - ignored: BTreeMap, - filtered_out: usize, - failures: Vec, -} - -impl Summary { - /// Create an empty run summary. - #[must_use] - pub(crate) fn new() -> Self { - Self::default() - } - - /// Record one executed case outcome. - pub(crate) fn record(&mut self, case: &Case, outcome: &Outcome) { - let case_path = case.display_path(); - match outcome { - Outcome::Pass => self.totals.pass += 1, - Outcome::Fail(_) => { - self.totals.fail += 1; - self.failures.push(Failure { case_path }); - } - } - } - - /// Record one ignored fixture report row. - pub(crate) fn record_ignored_fixture( - &mut self, - path: String, - reason: &'static str, - cases: usize, - ) { - let key = IgnoredKey { - path, - reason: reason.to_owned(), - }; - *self.ignored.entry(key).or_default() += cases; - } - - /// Record runnable cases excluded by libtest-style name-pattern selection. - pub(crate) fn record_filtered_out(&mut self, count: usize) { - self.filtered_out += count; - } - - /// Returns true if any case failed. - #[must_use] - pub(crate) fn has_failures(&self) -> bool { - !self.failures.is_empty() - } - - #[must_use] - /// Return aggregate pass/fail counts, excluding skipped cases. - pub(crate) fn totals(&self) -> Totals { - self.totals - } - - /// Write failure details, final result line, and ignored inventory. - pub(crate) fn write( - &self, - elapsed: Duration, - color: Color, - mut out: impl io::Write, - ) -> io::Result<()> { - let totals = self.totals(); - if !self.failures.is_empty() { - writeln!(out)?; - writeln!(out, "{}:", color.paint(Style::Fail, "failures"))?; - for f in &self.failures { - writeln!(out, " {}", f.case_path)?; - } - } - - writeln!(out)?; - let status = if totals.fail == 0 { "ok" } else { "FAILED" }; - let status_style = if totals.fail == 0 { - Style::Pass - } else { - Style::Fail - }; - let ignored = self.ignored.values().sum::(); - writeln!( - out, - "test result: {status}. {p} passed; {f} failed; {ignored} ignored; 0 measured; {filtered} filtered out; finished in {elapsed}", - status = color.paint(status_style, status), - p = totals.pass, - f = totals.fail, - filtered = self.filtered_out, - elapsed = format_elapsed(elapsed), - )?; - - self.write_ignored(&mut out)?; - Ok(()) - } - - fn write_ignored(&self, mut out: impl io::Write) -> io::Result<()> { - if self.ignored.is_empty() { - return Ok(()); - } - - let mut max_key_len = 0; - let mut rows = Vec::with_capacity(self.ignored.len()); - for (ignored, cases) in &self.ignored { - let key = ignored.path.clone(); - max_key_len = max_key_len.max(key.len()); - rows.push((key, ignored.reason.as_str(), *cases)); - } - - writeln!(out)?; - writeln!(out, "ignored fixture cases/families:")?; - for (key, reason, cases) in &rows { - writeln!( - out, - "{key: String { - format!("{:.2}s", elapsed.as_secs_f64()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::harness::color::Color; - use crate::inventory::{Handler, RunnerName, SkipReason, SkippedFamily, SkippedFixture}; - use crate::testing::{GET_HEAD_GENESIS, KZG_VERIFY_PROOF_0_0}; - - #[test] - fn pass_counts_as_pass_and_does_not_fail() { - let mut summary = Summary::new(); - let case = GET_HEAD_GENESIS.to_case(); - summary.record(&case, &Outcome::Pass); - summary.record(&case, &Outcome::Pass); - - let totals = summary.totals(); - assert_eq!(totals.pass, 2); - assert_eq!(totals.fail, 0); - assert_eq!(totals.pass + totals.fail, 2); - - assert!(!summary.has_failures()); - } - - #[test] - fn skipped_handlers_do_not_affect_pass_fail_totals() { - let mut summary = Summary::new(); - let skipped = KZG_VERIFY_PROOF_0_0; - let skipped_fixture = SkippedFixture::Family(SkippedFamily { - config: skipped.preset.to_owned(), - fork: skipped.fork.to_owned(), - runner: RunnerName::Unknown(skipped.runner.to_owned()), - handler: Handler::new(skipped.handler.to_owned()), - reason: SkipReason::UnsupportedRunner, - cases: 1, - case_paths: vec![format!( - "{}/{}/{}/{}/{}/{}", - skipped.preset, - skipped.fork, - skipped.runner, - skipped.handler, - skipped.suite, - skipped.case - )], - }); - summary.record_ignored_fixture( - skipped_fixture.display_path(), - skipped_fixture.reason().as_str(), - skipped_fixture.cases(), - ); - - let totals = summary.totals(); - assert_eq!(totals.pass, 0); - assert_eq!(totals.fail, 0); - assert_eq!(totals.pass + totals.fail, 0); - assert_eq!(summary.ignored.values().copied().sum::(), 1); - assert!(!summary.has_failures()); - } - - #[test] - fn write_emits_cargo_style_summary_and_ignored_inventory() { - let mut summary = Summary::new(); - let case = GET_HEAD_GENESIS.to_case(); - let skipped = KZG_VERIFY_PROOF_0_0; - - summary.record(&case, &Outcome::Pass); - summary.record_filtered_out(3); - let skipped_fixture = SkippedFixture::Family(SkippedFamily { - config: skipped.preset.to_owned(), - fork: skipped.fork.to_owned(), - runner: RunnerName::Unknown(skipped.runner.to_owned()), - handler: Handler::new(skipped.handler.to_owned()), - reason: SkipReason::UnsupportedRunner, - cases: 1, - case_paths: vec![format!( - "{}/{}/{}/{}/{}/{}", - skipped.preset, - skipped.fork, - skipped.runner, - skipped.handler, - skipped.suite, - skipped.case - )], - }); - summary.record_ignored_fixture( - skipped_fixture.display_path(), - skipped_fixture.reason().as_str(), - skipped_fixture.cases(), - ); - - let mut output = Vec::new(); - summary - .write(Duration::from_millis(17), Color::always(), &mut output) - .expect("write report"); - let output = String::from_utf8(output).expect("report is utf-8"); - - assert!(!output.contains(&case.family_path())); - assert!(output.contains( - "test result: \u{1b}[32mok\u{1b}[0m. 1 passed; 0 failed; 1 ignored; 0 measured; 3 filtered out; finished in 0.02s" - )); - assert!(output.contains("general/deneb/kzg/verify_kzg_proof")); - assert!(output.contains("reason=unsupported runner")); - assert!(output.contains("ignored fixture cases/families:")); - } - - #[test] - fn write_colors_status_when_enabled() { - let mut summary = Summary::new(); - let case = GET_HEAD_GENESIS.to_case(); - summary.record(&case, &Outcome::Pass); - - let mut output = Vec::new(); - summary - .write(Duration::from_millis(17), Color::always(), &mut output) - .expect("write report"); - let output = String::from_utf8(output).expect("report is utf-8"); - - assert!(output.contains("test result: \u{1b}[32mok\u{1b}[0m.")); - } - - #[test] - fn write_failure_lists_case_names_only() { - let mut summary = Summary::new(); - let case = GET_HEAD_GENESIS.to_case(); - summary.record(&case, &Outcome::Fail("head mismatch".to_owned())); - - let mut output = Vec::new(); - summary - .write(Duration::from_millis(17), Color::always(), &mut output) - .expect("write report"); - let output = String::from_utf8(output).expect("report is utf-8"); - - assert!(output.contains("failures")); - assert!(output.contains(" minimal/gloas/fork_choice/get_head/pyspec_tests/genesis")); - assert!(!output.contains("head mismatch")); - assert!(!output.contains("runner:")); - assert!(!output.contains("handler:")); - assert!(!output.contains("rerun:")); - } -} diff --git a/reftests/src/testing.rs b/reftests/src/testing.rs deleted file mode 100644 index 5ef542f..0000000 --- a/reftests/src/testing.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! Test-only utilities shared by module unit tests. - -use std::path::{Path, PathBuf}; - -mod assets; - -use crate::inventory::{Case, CaseKind, Handler, Runner}; - -pub(crate) use assets::{ - ALL_CASES, AssetCase, BLS_AGGREGATE_EMPTY_LIST, BLS_AGGREGATE_VALID_0, - BLS_DISABLED_ATTESTATION, BLS_FAST_AGGREGATE_VERIFY_VALID_0, - EPOCH_EFFECTIVE_BALANCE_HYSTERESIS, GET_CUSTODY_GROUPS_1, GET_HEAD_GENESIS, - KZG_VERIFY_PROOF_0_0, SANITY_BLOCK_INVALID_OLD_STYLE_DEPOSIT_REJECTED, SLOTS_1, - SSZ_STATIC_FORK_RANDOM_0, VOLUNTARY_EXIT_BASIC, asset_path, vector_asset_release, - vector_asset_root, -}; - -/// Temporary directory removed on drop. -/// -/// This intentionally stays tiny instead of depending on `tempfile`, because -/// the reftests crate already has a narrow dependency surface and tests only -/// need unique scratch directories. -pub(crate) struct TempDir { - path: PathBuf, -} - -impl TempDir { - /// Create a unique directory under the system temp directory. - pub(crate) fn new(name: &str) -> Self { - let path = std::env::temp_dir().join(format!( - "moonglass-reftests-{name}-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system clock should be after 1970-01-01T00:00:00Z") - .as_nanos() - )); - std::fs::create_dir_all(&path).expect("create temp dir"); - Self { path } - } - - /// Borrow the temporary directory path. - pub(crate) fn path(&self) -> &Path { - &self.path - } -} - -impl Drop for TempDir { - fn drop(&mut self) { - std::fs::remove_dir_all(&self.path).ok(); - } -} - -impl AssetCase { - /// Build the harness [`Case`] for this checked-in vector case. - pub(crate) fn to_case(self) -> Case { - let runner = Runner::parse(self.runner).expect("test asset should use a supported runner"); - Case { - config: self.preset.to_owned(), - fork: self.fork.to_owned(), - kind: CaseKind { - runner, - handler: Handler::new(self.handler.to_owned()), - }, - suite: self.suite.to_owned(), - id: self.case.to_owned(), - root: self.root(), - } - } -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - - use serde::Deserialize; - - use super::*; - - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - struct AssetManifest { - source: AssetManifestSource, - cases: Vec, - files: BTreeMap, - } - - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - struct AssetManifestSource { - repository: String, - release: String, - asset_root: String, - } - - #[test] - fn checked_in_vector_asset_manifest_matches_files() { - let manifest_path = asset_path("manifest.json"); - let raw = std::fs::read_to_string(&manifest_path).expect("read asset manifest"); - let manifest: AssetManifest = serde_json::from_str(&raw).expect("parse asset manifest"); - - assert_eq!(manifest.source.repository, "ethereum/consensus-specs"); - assert_eq!(manifest.source.release, crate::CONSENSUS_SPECS_TAG); - assert_eq!(manifest.source.asset_root, vector_asset_release()); - - let expected_cases = ALL_CASES - .iter() - .copied() - .map(case_manifest_path) - .collect::>(); - assert_eq!(manifest.cases, expected_cases); - - let root = vector_asset_root(); - let files = release_files(&root); - let manifest_files = manifest.files.keys().cloned().collect::>(); - assert_eq!(manifest_files, files); - - for (relative, expected_hash) in manifest.files { - let got = crate::vectors::sha256_hex(&root.join(&relative)).expect("hash asset"); - assert_eq!(got, expected_hash, "{relative}"); - } - } - - fn case_manifest_path(case: AssetCase) -> String { - [ - "tests", - case.preset, - case.fork, - case.runner, - case.handler, - case.suite, - case.case, - ] - .join("/") - } - - fn release_files(root: &Path) -> Vec { - let mut files = Vec::new(); - collect_release_files(root, &root.join("tests"), &mut files); - files.sort(); - files - } - - fn collect_release_files(root: &Path, dir: &Path, files: &mut Vec) { - for entry in std::fs::read_dir(dir).expect("read asset directory") { - let entry = entry.expect("read asset directory entry"); - let path = entry.path(); - let file_type = entry.file_type().expect("read asset file type"); - if file_type.is_dir() { - collect_release_files(root, &path, files); - } else if file_type.is_file() { - files.push( - path.strip_prefix(root) - .expect("asset path should be under release root") - .to_string_lossy() - .replace('\\', "/"), - ); - } - } - } -} diff --git a/reftests/src/testing/assets.rs b/reftests/src/testing/assets.rs deleted file mode 100644 index 0688e79..0000000 --- a/reftests/src/testing/assets.rs +++ /dev/null @@ -1,166 +0,0 @@ -use std::path::{Path, PathBuf}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct AssetCase { - pub(crate) preset: &'static str, - pub(crate) fork: &'static str, - pub(crate) runner: &'static str, - pub(crate) handler: &'static str, - pub(crate) suite: &'static str, - pub(crate) case: &'static str, -} - -impl AssetCase { - pub(crate) fn root(self) -> PathBuf { - vector_asset_root() - .join("tests") - .join(self.preset) - .join(self.fork) - .join(self.runner) - .join(self.handler) - .join(self.suite) - .join(self.case) - } - - pub(crate) fn file(self, name: impl AsRef) -> PathBuf { - self.root().join(name) - } -} - -pub(crate) fn asset_path(relative: impl AsRef) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("assets") - .join(relative) -} - -pub(crate) fn vector_asset_root() -> PathBuf { - asset_path(vector_asset_release()) -} - -pub(crate) fn vector_asset_release() -> String { - format!("consensus-specs-{}", crate::CONSENSUS_SPECS_TAG) -} - -pub(crate) const BLS_AGGREGATE_EMPTY_LIST: AssetCase = AssetCase { - preset: "general", - fork: "altair", - runner: "bls", - handler: "eth_aggregate_pubkeys", - suite: "bls", - case: "eth_aggregate_pubkeys_empty_list", -}; - -pub(crate) const BLS_AGGREGATE_VALID_0: AssetCase = AssetCase { - preset: "general", - fork: "altair", - runner: "bls", - handler: "eth_aggregate_pubkeys", - suite: "bls", - case: "eth_aggregate_pubkeys_valid_0", -}; - -pub(crate) const BLS_FAST_AGGREGATE_VERIFY_VALID_0: AssetCase = AssetCase { - preset: "general", - fork: "altair", - runner: "bls", - handler: "eth_fast_aggregate_verify", - suite: "bls", - case: "eth_fast_aggregate_verify_valid_0", -}; - -pub(crate) const KZG_VERIFY_PROOF_0_0: AssetCase = AssetCase { - preset: "general", - fork: "deneb", - runner: "kzg", - handler: "verify_kzg_proof", - suite: "kzg-mainnet", - case: "verify_kzg_proof_case_correct_proof_0_0", -}; - -pub(crate) const EPOCH_EFFECTIVE_BALANCE_HYSTERESIS: AssetCase = AssetCase { - preset: "minimal", - fork: crate::TARGET_FORK, - runner: "epoch_processing", - handler: "effective_balance_updates", - suite: "pyspec_tests", - case: "effective_balance_hysteresis", -}; - -pub(crate) const GET_HEAD_GENESIS: AssetCase = AssetCase { - preset: "minimal", - fork: crate::TARGET_FORK, - runner: "fork_choice", - handler: "get_head", - suite: "pyspec_tests", - case: "genesis", -}; - -pub(crate) const GET_CUSTODY_GROUPS_1: AssetCase = AssetCase { - preset: "minimal", - fork: crate::TARGET_FORK, - runner: "networking", - handler: "get_custody_groups", - suite: "pyspec_tests", - case: "get_custody_groups_1", -}; - -pub(crate) const BLS_DISABLED_ATTESTATION: AssetCase = AssetCase { - preset: "minimal", - fork: crate::TARGET_FORK, - runner: "operations", - handler: "attestation", - suite: "pyspec_tests", - case: "invalid_index", -}; - -pub(crate) const VOLUNTARY_EXIT_BASIC: AssetCase = AssetCase { - preset: "minimal", - fork: crate::TARGET_FORK, - runner: "operations", - handler: "voluntary_exit", - suite: "pyspec_tests", - case: "basic", -}; - -pub(crate) const SANITY_BLOCK_INVALID_OLD_STYLE_DEPOSIT_REJECTED: AssetCase = AssetCase { - preset: "minimal", - fork: crate::TARGET_FORK, - runner: "sanity", - handler: "blocks", - suite: "pyspec_tests", - case: "invalid_old_style_deposit_rejected", -}; - -pub(crate) const SLOTS_1: AssetCase = AssetCase { - preset: "minimal", - fork: crate::TARGET_FORK, - runner: "sanity", - handler: "slots", - suite: "pyspec_tests", - case: "slots_1", -}; - -pub(crate) const SSZ_STATIC_FORK_RANDOM_0: AssetCase = AssetCase { - preset: "minimal", - fork: crate::TARGET_FORK, - runner: "ssz_static", - handler: "Fork", - suite: "ssz_random", - case: "case_0", -}; - -pub(crate) const ALL_CASES: &[AssetCase] = &[ - BLS_AGGREGATE_EMPTY_LIST, - BLS_AGGREGATE_VALID_0, - BLS_FAST_AGGREGATE_VERIFY_VALID_0, - KZG_VERIFY_PROOF_0_0, - EPOCH_EFFECTIVE_BALANCE_HYSTERESIS, - GET_HEAD_GENESIS, - GET_CUSTODY_GROUPS_1, - BLS_DISABLED_ATTESTATION, - VOLUNTARY_EXIT_BASIC, - SANITY_BLOCK_INVALID_OLD_STYLE_DEPOSIT_REJECTED, - SLOTS_1, - SSZ_STATIC_FORK_RANDOM_0, -]; diff --git a/reftests/src/vectors/release.rs b/reftests/src/vectors/release.rs deleted file mode 100644 index 84f823f..0000000 --- a/reftests/src/vectors/release.rs +++ /dev/null @@ -1,255 +0,0 @@ -//! Pinned consensus-spec release cache handling. - -use std::path::{Path, PathBuf}; - -use crate::error::{ManifestError, ReleaseError}; -use crate::{CONSENSUS_SPECS_TAG, MAINNET_PRESET, MINIMAL_PRESET, TARGET_FORK}; - -use super::FixtureSet; -use super::manifest::{Manifest, manifest_path}; -use super::{archive, fetch}; - -const VECTORS_DIR: &str = "reftests/vectors"; - -pub(crate) type Result = std::result::Result; - -pub(crate) fn tag_dir(fixtures: FixtureSet) -> Result { - let dest = vectors_root(); - let dir = dest.join(CONSENSUS_SPECS_TAG).join(fixtures.cache_dir()); - if valid_cached_release(&dir, fixtures)? { - return Ok(dir); - } - - let manifest = fetch::fetch_release(CONSENSUS_SPECS_TAG, &dir, fixtures)?; - if !valid_cached_release(&dir, fixtures)? { - return Err(ReleaseError::FetchedReleaseIncomplete { tag: manifest.tag }); - } - - Ok(dir) -} - -fn vectors_root() -> PathBuf { - workspace_root().join(VECTORS_DIR) -} - -fn workspace_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("reftests crate lives inside workspace root") - .to_path_buf() -} - -fn valid_cached_release(dir: &Path, fixtures: FixtureSet) -> Result { - let manifest_path = manifest_path(dir); - let manifest = match Manifest::read(&manifest_path) { - Ok(manifest) => manifest, - Err(ManifestError::Io { source, .. }) if source.kind() == std::io::ErrorKind::NotFound => { - return Ok(false); - } - Err(ManifestError::Json { .. }) => return Ok(false), - Err(err) => return Err(ReleaseError::from(err)), - }; - if manifest.tag != CONSENSUS_SPECS_TAG || manifest.archive_sha256s.is_empty() { - return Ok(false); - } - if tests_path_has_symlink(dir)? { - return Ok(false); - } - if !required_fixture_roots_exist(dir, fixtures) { - return Ok(false); - } - for archive_info in fetch::required_archives(fixtures) { - let Some(cached_hash) = manifest.archive_sha256s.get(archive_info.name) else { - return Ok(false); - }; - if cached_hash != archive_info.sha256 { - return Ok(false); - } - - let path = dir.join(".archives").join(archive_info.name); - if !path.is_file() { - return Ok(false); - } - if path - .metadata() - .map_err(|source| ReleaseError::PathIo { - action: "inspect", - path: path.clone(), - source, - })? - .len() - != archive_info.compressed_bytes - { - return Ok(false); - } - let got = archive::sha256_hex(&path)?; - if got != archive_info.sha256 { - return Ok(false); - } - } - Ok(true) -} - -fn required_fixture_roots_exist(dir: &Path, fixtures: FixtureSet) -> bool { - match fixtures { - FixtureSet::General => dir.join("tests").join("general").is_dir(), - FixtureSet::Mainnet => dir - .join("tests") - .join(MAINNET_PRESET) - .join(TARGET_FORK) - .is_dir(), - FixtureSet::Minimal => dir - .join("tests") - .join(MINIMAL_PRESET) - .join(TARGET_FORK) - .is_dir(), - } -} - -fn tests_path_has_symlink(dir: &Path) -> Result { - let tests = dir.join("tests"); - match std::fs::symlink_metadata(&tests) { - Ok(_) => archive::contains_symlink(&tests).map_err(ReleaseError::from), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false), - Err(e) => Err(ReleaseError::PathIo { - action: "inspect", - path: tests, - source: e, - }), - } -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - - use super::*; - - #[test] - fn invalid_cache_manifest_invalidates_release_cache() { - let dir = crate::testing::TempDir::new("invalid-release-manifest"); - let path = manifest_path(dir.path()); - std::fs::write(&path, "{ not json").expect("write invalid manifest"); - - let valid = valid_cached_release(dir.path(), FixtureSet::Minimal) - .expect("invalid JSON should trigger refetch, not fail"); - - assert!(!valid); - } - - #[test] - fn wrong_manifest_tag_invalidates_release_cache() { - let dir = crate::testing::TempDir::new("wrong-release-tag"); - write_manifest( - dir.path(), - "v0.0.0-wrong", - [(minimal_archive().name, minimal_archive().sha256)], - ); - - let valid = valid_cached_release(dir.path(), FixtureSet::Minimal).expect("validate cache"); - - assert!(!valid); - } - - #[test] - fn missing_lane_root_invalidates_release_cache() { - let dir = crate::testing::TempDir::new("missing-release-root"); - write_manifest( - dir.path(), - CONSENSUS_SPECS_TAG, - [(minimal_archive().name, minimal_archive().sha256)], - ); - - let valid = valid_cached_release(dir.path(), FixtureSet::Minimal).expect("validate cache"); - - assert!(!valid); - } - - #[test] - fn missing_archive_invalidates_release_cache() { - let dir = crate::testing::TempDir::new("missing-release-archive"); - create_minimal_root(dir.path()); - write_manifest( - dir.path(), - CONSENSUS_SPECS_TAG, - [(minimal_archive().name, minimal_archive().sha256)], - ); - - let valid = valid_cached_release(dir.path(), FixtureSet::Minimal).expect("validate cache"); - - assert!(!valid); - } - - #[test] - fn wrong_manifest_archive_hash_invalidates_release_cache() { - let dir = crate::testing::TempDir::new("wrong-release-archive-hash"); - create_minimal_root(dir.path()); - write_manifest( - dir.path(), - CONSENSUS_SPECS_TAG, - [(minimal_archive().name, "not-the-pinned-hash")], - ); - - let valid = valid_cached_release(dir.path(), FixtureSet::Minimal).expect("validate cache"); - - assert!(!valid); - } - - #[test] - fn wrong_archive_size_invalidates_release_cache_before_hashing() { - let dir = crate::testing::TempDir::new("wrong-release-archive-size"); - create_minimal_root(dir.path()); - write_manifest( - dir.path(), - CONSENSUS_SPECS_TAG, - [(minimal_archive().name, minimal_archive().sha256)], - ); - let archive_dir = dir.path().join(".archives"); - std::fs::create_dir_all(&archive_dir).expect("archive dir"); - std::fs::write(archive_dir.join(minimal_archive().name), b"too small").expect("archive"); - - let valid = valid_cached_release(dir.path(), FixtureSet::Minimal).expect("validate cache"); - - assert!(!valid); - } - - #[test] - fn cache_for_one_lane_does_not_validate_another_lane() { - let dir = crate::testing::TempDir::new("cross-lane-release-cache"); - create_minimal_root(dir.path()); - write_manifest( - dir.path(), - CONSENSUS_SPECS_TAG, - [(minimal_archive().name, minimal_archive().sha256)], - ); - - let valid = valid_cached_release(dir.path(), FixtureSet::Mainnet).expect("validate cache"); - - assert!(!valid); - } - - fn minimal_archive() -> fetch::RequiredArchive { - fetch::required_archives(FixtureSet::Minimal)[0] - } - - fn create_minimal_root(dir: &Path) { - std::fs::create_dir_all(dir.join("tests").join(MINIMAL_PRESET).join(TARGET_FORK)) - .expect("minimal root"); - } - - fn write_manifest<'a>( - dir: &Path, - tag: &str, - archives: impl IntoIterator, - ) { - let manifest = Manifest { - tag: tag.to_owned(), - fetched_at: 1, - archive_sha256s: archives - .into_iter() - .map(|(name, hash)| (name.to_owned(), hash.to_owned())) - .collect::>(), - }; - manifest.write(&manifest_path(dir)).expect("write manifest"); - } -} diff --git a/reftests/tests/worker.rs b/reftests/tests/worker.rs deleted file mode 100644 index 4fd48d1..0000000 --- a/reftests/tests/worker.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! Integration tests for the internal per-case worker protocol. - -use std::io::Write as _; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; - -use reftests::CONSENSUS_SPECS_TAG; - -const WORKER_ENV: &str = "MOONGLASS_REFTEST_WORKER"; -const WORKER_TOKEN_ENV: &str = "MOONGLASS_REFTEST_WORKER_TOKEN"; -const WORKER_ARG: &str = "--reftests-internal-case-worker"; -const WORKER_JSON_PREFIX: &str = "\n__MOONGLASS_REFTEST_OUTCOME__"; -#[cfg(feature = "mainnet")] -const WORKER_BIN: &str = env!("CARGO_BIN_EXE_reftests"); -#[cfg(feature = "minimal")] -const WORKER_BIN: &str = env!("CARGO_BIN_EXE_reftests-minimal"); - -#[test] -fn internal_worker_returns_structured_failure() { - let fork = target_fork(); - let request = serde_json::json!({ - "case": { - "config": "minimal", - "fork": fork, - "runner": "sanity", - "handler": "slots", - "suite": "pyspec_tests", - "id": "not_slots_1", - "root": slots_case_root(), - }, - "trace": "full", - }); - - let response = run_worker(&request); - let outcome = response.get("outcome").expect("outcome"); - assert_eq!( - outcome.get("status").and_then(serde_json::Value::as_str), - Some("fail") - ); - let failure = outcome - .get("detail") - .and_then(serde_json::Value::as_str) - .expect("failure outcome"); - assert!( - failure.contains("manifest \"") && failure.contains("manifest.yaml\" case mismatch"), - "{failure}" - ); - assert!( - response.get("trace").is_none(), - "pre-execution manifest failures should not emit execution trace: {response:?}" - ); -} - -#[test] -fn internal_worker_omits_trace_when_trace_mode_is_off() { - let request = serde_json::json!({ - "case": { - "config": "general", - "fork": "altair", - "runner": "bls", - "handler": "eth_aggregate_pubkeys", - "suite": "bls", - "id": "eth_aggregate_pubkeys_empty_list", - "root": bls_aggregate_empty_list_root(), - }, - "trace": "off", - }); - - let response = run_worker(&request); - assert_eq!( - response - .get("outcome") - .and_then(|outcome| outcome.get("status")) - .and_then(serde_json::Value::as_str), - Some("pass") - ); - assert!( - response.get("trace").is_none(), - "trace should be omitted when trace mode is off: {response:?}" - ); -} - -fn run_worker(request: &serde_json::Value) -> serde_json::Value { - let mut child = Command::new(WORKER_BIN) - .arg(WORKER_ARG) - .arg("worker-test-token") - .env(WORKER_ENV, "1") - .env(WORKER_TOKEN_ENV, "worker-test-token") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("spawn worker"); - - { - let mut stdin = child.stdin.take().expect("worker stdin"); - serde_json::to_writer(&mut stdin, &request).expect("write case"); - stdin.flush().expect("flush case"); - } - - let output = child.wait_with_output().expect("worker output"); - assert!( - output.status.success(), - "worker failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - - let payload = worker_payload(&output.stdout).expect("outcome marker"); - serde_json::from_slice(payload).expect("worker response json") -} - -fn slots_case_root() -> PathBuf { - asset_root() - .join("tests") - .join("minimal") - .join(target_fork()) - .join("sanity") - .join("slots") - .join("pyspec_tests") - .join("slots_1") -} - -fn bls_aggregate_empty_list_root() -> PathBuf { - asset_root() - .join("tests") - .join("general") - .join("altair") - .join("bls") - .join("eth_aggregate_pubkeys") - .join("bls") - .join("eth_aggregate_pubkeys_empty_list") -} - -fn target_fork() -> String { - let minimal = asset_root().join("tests").join("minimal"); - let forks = std::fs::read_dir(&minimal) - .expect("read minimal asset presets") - .map(|entry| entry.expect("read minimal fork entry").path()) - .filter(|path| path.is_dir()) - .map(|path| { - path.file_name() - .expect("fork path has file name") - .to_string_lossy() - .into_owned() - }) - .collect::>(); - assert_eq!(forks.len(), 1, "expected one checked-in minimal fork"); - forks.into_iter().next().expect("one minimal fork") -} - -fn asset_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("assets") - .join(vector_asset_release()) -} - -fn vector_asset_release() -> String { - format!("consensus-specs-{CONSENSUS_SPECS_TAG}") -} - -fn worker_payload(stdout: &[u8]) -> Option<&[u8]> { - stdout - .windows(WORKER_JSON_PREFIX.len()) - .rposition(|window| window == WORKER_JSON_PREFIX.as_bytes()) - .map(|pos| &stdout[pos + WORKER_JSON_PREFIX.len()..]) -} diff --git a/reftests/Cargo.toml b/tests/Cargo.toml similarity index 89% rename from reftests/Cargo.toml rename to tests/Cargo.toml index ae8bc97..96871cd 100644 --- a/reftests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "reftests" +name = "tests" version = "0.1.0" edition = "2024" rust-version = "1.96" @@ -25,12 +25,11 @@ required-features = ["minimal"] [features] default = ["mainnet"] -mainnet = ["moonglass/mainnet"] -minimal = ["moonglass/minimal"] +mainnet = ["moonglass-core/mainnet"] +minimal = ["moonglass-core/minimal"] [dependencies] -moonglass = { version = "0.1.0", path = "../moonglass", default-features = false } -ssz_rs = "0.9" +moonglass-core = { version = "0.1.0", path = "../moonglass-core", default-features = false } snap = "1" flate2 = "1" tar = "0.4" diff --git a/reftests/README.md b/tests/README.md similarity index 81% rename from reftests/README.md rename to tests/README.md index 387c12b..2ca7d03 100644 --- a/reftests/README.md +++ b/tests/README.md @@ -1,12 +1,14 @@ -# reftests +# tests -`reftests` is an accessory crate that runs Moonglass against generated +The `tests` crate runs Moonglass against generated [`ethereum/consensus-specs`](https://github.com/ethereum/consensus-specs) -fixtures. It is not part of the `moonglass` library API. +fixtures. It builds two lane runners, `reftests` (mainnet + general) and +`reftests-minimal` (minimal), and is not part of the `moonglass-core` library +API. The crate owns fixture discovery, vector-cache management, adapter dispatch, process isolation, terminal output, and trace rendering. Consensus behavior must -still live in `moonglass`. Adapters translate fixture files into calls on +still live in `moonglass-core`. Adapters translate fixture files into calls on Moonglass and compare the result with the fixture. ## Quick Start @@ -14,24 +16,24 @@ Moonglass and compare the result with the fixture. Run the CI-required minimal lane: ```bash -cargo build --release -p reftests --no-default-features --features minimal +cargo build --release -p tests --no-default-features --features minimal target/release/reftests-minimal ``` Run the mainnet lane, including shared `general` fixtures: ```bash -cargo build --release -p reftests +cargo build --release -p tests target/release/reftests ``` Run both lanes explicitly: ```bash -cargo build --release -p reftests --no-default-features --features minimal +cargo build --release -p tests --no-default-features --features minimal target/release/reftests-minimal -cargo build --release -p reftests +cargo build --release -p tests target/release/reftests ``` @@ -96,8 +98,8 @@ Select the lane with Cargo features at build time, then run the matching binary from `target/release/`. When required fixtures are missing, the runner downloads the configured release -archives into `reftests/vectors/`. The cache is local working data and is not -part of the source tree. +archives into `tests/vectors/`. The cache is local working data and is not part +of the source tree. Unfiltered runs enforce the pinned fixture inventory for the active lane. This catches upstream vector-surface drift before execution. Filtered runs skip that @@ -134,32 +136,32 @@ Adapters are glue code. They should: Adapters should not: -- reimplement consensus algorithms inside `reftests` +- reimplement consensus algorithms inside `tests` - mock cryptography, state transition, or fork-choice behavior - invent synthetic fixture formats when an actual consensus-spec fixture exists - mark a handler supported before the corresponding Moonglass behavior exists If a needed behavior is private in Moonglass, expose the smallest sensible -Moonglass API first, then call it from the adapter. +`moonglass-core` API first, then call it from the adapter. ## Development Checks For harness changes, run: ```bash -cargo test -p reftests -cargo test -p reftests --no-default-features --features minimal -cargo clippy -p reftests --all-targets -- -D warnings -cargo clippy -p reftests --no-default-features --features minimal --all-targets -- -D warnings +cargo test -p tests +cargo test -p tests --no-default-features --features minimal +cargo clippy -p tests --all-targets -- -D warnings +cargo clippy -p tests --no-default-features --features minimal --all-targets -- -D warnings ``` For full fixture validation, run both release lanes: ```bash -cargo build --release -p reftests --no-default-features --features minimal +cargo build --release -p tests --no-default-features --features minimal target/release/reftests-minimal -cargo build --release -p reftests +cargo build --release -p tests target/release/reftests ``` diff --git a/reftests/src/adapters.rs b/tests/src/adapters.rs similarity index 68% rename from reftests/src/adapters.rs rename to tests/src/adapters.rs index 3baf037..52806b6 100644 --- a/reftests/src/adapters.rs +++ b/tests/src/adapters.rs @@ -8,13 +8,17 @@ use std::{ cell::{Cell, RefCell}, - fmt, + fmt, fs, + io::ErrorKind, marker::PhantomData, + mem, + result::Result as StdResult, }; -use moonglass::containers::{BeaconBlock, BeaconState}; -use moonglass::error::TransitionError; +use moonglass_core::containers::{BeaconBlock, BeaconState}; +use moonglass_core::error::TransitionError; use serde::{Deserialize, Serialize}; +use serde_yaml::Value; use crate::error::FixtureError; use crate::fixtures::{self, BlsSetting, CaseFiles, FixtureFile, diff}; @@ -23,8 +27,13 @@ use crate::inventory::{Case, Handler, MetadataSkipReason, Runner}; mod bls; mod epoch_processing; mod fork_choice; +mod kzg; +mod networking; mod operations; +mod rewards; mod sanity; +mod shuffling; +mod ssz_generic; mod ssz_static; mod trace_data; @@ -101,15 +110,129 @@ impl FixtureAdapter for Adapter { } } -static ADAPTERS: &[&dyn FixtureAdapter] = &[ - &ssz_static::ADAPTER, - &bls::ADAPTER, - &fork_choice::ADAPTER, - &operations::ADAPTER, - &epoch_processing::ADAPTER, - &sanity::SANITY_ADAPTER, - &sanity::FINALITY_ADAPTER, - &sanity::RANDOM_ADAPTER, +/// Registry entry for one statically known adapter. +#[derive(Clone, Copy)] +enum AdapterKind { + /// `ssz_static` fixtures. + SszStatic, + /// `ssz_generic` fixtures. + SszGeneric, + /// BLS general fixtures. + Bls, + /// KZG general fixtures. + Kzg, + /// Networking helper fixtures. + Networking, + /// Fork-choice fixtures. + ForkChoice, + /// Operation processing fixtures. + Operations, + /// Reward and penalty delta fixtures. + Rewards, + /// Index-shuffling fixtures. + Shuffling, + /// Epoch-processing fixtures. + EpochProcessing, + /// Sanity block and slot fixtures. + Sanity, + /// Sanity finality fixtures. + Finality, + /// Sanity random fixtures. + Random, +} + +impl AdapterKind { + /// Return the upstream runner directory handled by this adapter. + fn runner(self) -> Runner { + match self { + Self::SszStatic => ssz_static::ADAPTER.runner(), + Self::SszGeneric => ssz_generic::ADAPTER.runner(), + Self::Bls => bls::ADAPTER.runner(), + Self::Kzg => kzg::ADAPTER.runner(), + Self::Networking => networking::ADAPTER.runner(), + Self::ForkChoice => fork_choice::ADAPTER.runner(), + Self::Operations => operations::ADAPTER.runner(), + Self::Rewards => rewards::ADAPTER.runner(), + Self::Shuffling => shuffling::ADAPTER.runner(), + Self::EpochProcessing => epoch_processing::ADAPTER.runner(), + Self::Sanity => sanity::SANITY_ADAPTER.runner(), + Self::Finality => sanity::FINALITY_ADAPTER.runner(), + Self::Random => sanity::RANDOM_ADAPTER.runner(), + } + } + + /// Return the upstream handler names this adapter supports. + fn supported_handlers(self) -> Vec<&'static str> { + match self { + Self::SszStatic => ssz_static::ADAPTER.supported_handlers(), + Self::SszGeneric => ssz_generic::ADAPTER.supported_handlers(), + Self::Bls => bls::ADAPTER.supported_handlers(), + Self::Kzg => kzg::ADAPTER.supported_handlers(), + Self::Networking => networking::ADAPTER.supported_handlers(), + Self::ForkChoice => fork_choice::ADAPTER.supported_handlers(), + Self::Operations => operations::ADAPTER.supported_handlers(), + Self::Rewards => rewards::ADAPTER.supported_handlers(), + Self::Shuffling => shuffling::ADAPTER.supported_handlers(), + Self::EpochProcessing => epoch_processing::ADAPTER.supported_handlers(), + Self::Sanity => sanity::SANITY_ADAPTER.supported_handlers(), + Self::Finality => sanity::FINALITY_ADAPTER.supported_handlers(), + Self::Random => sanity::RANDOM_ADAPTER.supported_handlers(), + } + } + + /// Whether `handler` belongs to this adapter. + fn supports(self, handler: &Handler) -> bool { + match self { + Self::SszStatic => ssz_static::ADAPTER.supports(handler), + Self::SszGeneric => ssz_generic::ADAPTER.supports(handler), + Self::Bls => bls::ADAPTER.supports(handler), + Self::Kzg => kzg::ADAPTER.supports(handler), + Self::Networking => networking::ADAPTER.supports(handler), + Self::ForkChoice => fork_choice::ADAPTER.supports(handler), + Self::Operations => operations::ADAPTER.supports(handler), + Self::Rewards => rewards::ADAPTER.supports(handler), + Self::Shuffling => shuffling::ADAPTER.supports(handler), + Self::EpochProcessing => epoch_processing::ADAPTER.supports(handler), + Self::Sanity => sanity::SANITY_ADAPTER.supports(handler), + Self::Finality => sanity::FINALITY_ADAPTER.supports(handler), + Self::Random => sanity::RANDOM_ADAPTER.supports(handler), + } + } + + /// Dispatch one case to this adapter. + fn run(self, case: &Case) -> Outcome { + match self { + Self::SszStatic => ssz_static::ADAPTER.run(case), + Self::SszGeneric => ssz_generic::ADAPTER.run(case), + Self::Bls => bls::ADAPTER.run(case), + Self::Kzg => kzg::ADAPTER.run(case), + Self::Networking => networking::ADAPTER.run(case), + Self::ForkChoice => fork_choice::ADAPTER.run(case), + Self::Operations => operations::ADAPTER.run(case), + Self::Rewards => rewards::ADAPTER.run(case), + Self::Shuffling => shuffling::ADAPTER.run(case), + Self::EpochProcessing => epoch_processing::ADAPTER.run(case), + Self::Sanity => sanity::SANITY_ADAPTER.run(case), + Self::Finality => sanity::FINALITY_ADAPTER.run(case), + Self::Random => sanity::RANDOM_ADAPTER.run(case), + } + } +} + +static ADAPTERS: &[AdapterKind] = &[ + AdapterKind::SszStatic, + AdapterKind::SszGeneric, + AdapterKind::Bls, + AdapterKind::Kzg, + AdapterKind::Networking, + AdapterKind::ForkChoice, + AdapterKind::Operations, + AdapterKind::Rewards, + AdapterKind::Shuffling, + AdapterKind::EpochProcessing, + AdapterKind::Sanity, + AdapterKind::Finality, + AdapterKind::Random, ]; thread_local! { @@ -221,7 +344,7 @@ pub(crate) fn configure_trace(enabled: bool) { /// Return and clear trace events produced by the current case. pub(crate) fn take_trace() -> Vec { - TRACE.with(|events| std::mem::take(&mut *events.borrow_mut())) + TRACE.with(|events| mem::take(&mut *events.borrow_mut())) } /// Return whether the current case is collecting trace events. @@ -391,7 +514,7 @@ pub(crate) fn trace_step_check_fail( /// Result of applying a state transition before expected-post comparison. pub(super) enum StateTransition { /// Moonglass transition returned a consensus result. - Applied(std::result::Result<(), TransitionError>), + Applied(StdResult<(), TransitionError>), /// The harness could not decode or prepare fixture input. HarnessError(String), } @@ -409,7 +532,6 @@ impl StateTransition { } /// Dispatch a case to its adapter based on `(runner, handler)`. -#[must_use] pub(crate) fn run(case: &Case) -> Outcome { if let Err(e) = fixtures::validate_case_manifest(case) { let detail = format!("validate manifest.yaml: {e:#}"); @@ -424,10 +546,9 @@ pub(crate) fn run(case: &Case) -> Outcome { /// Whether the harness has an adapter for this upstream `(runner, handler)`. /// -/// Unmapped runners include `kzg`, `merkle_proof`, `shuffling`, `genesis`, -/// `transition`, and sync-update fixture families. Those cases need adapters -/// with their own input shape before the harness can compare them honestly. -#[must_use] +/// Unmapped runners include `merkle_proof`, `genesis`, `transition`, and +/// sync-update fixture families. Those cases need adapters with their own input +/// shape before the harness can compare them honestly. pub(crate) fn supports(runner: Runner, handler: &Handler) -> bool { adapter(runner).is_some_and(|adapter| adapter.supports(handler)) } @@ -445,7 +566,7 @@ pub(crate) fn supported_families() -> impl Iterator { }) } -fn adapter(runner: Runner) -> Option<&'static dyn FixtureAdapter> { +fn adapter(runner: Runner) -> Option { ADAPTERS .iter() .copied() @@ -462,19 +583,57 @@ fn unsupported_runner(runner: Runner) -> Outcome { Outcome::Fail(detail) } +/// Whether a discovered case belongs to an aggregated unsupported sub-family. +/// +/// The `ssz_generic/containers` directory mixes the basic test containers with +/// progressive containers the in-house SSZ cannot merkleize. Discovery records +/// the progressive cases as a single skipped sub-family rather than as runnable +/// cases, so the basic-container family stays green. +pub(crate) fn is_aggregated_skip_case(case: &Case) -> Option { + if ssz_generic::is_progressive_container_case(case) { + Some(MetadataSkipReason::ProgressiveSszUnsupported) + } else { + None + } +} + /// Whether an otherwise supported case must be excluded because its metadata /// asks for semantics this harness does not implement. -pub(crate) fn case_skip_reason( - case: &Case, -) -> std::result::Result, FixtureError> { - let meta = CaseFiles::new(case).read_meta()?; - match meta.bls_setting { +pub(crate) fn case_skip_reason(case: &Case) -> StdResult, FixtureError> { + let bls_setting = match CaseFiles::new(case).read_meta() { + Ok(meta) => meta.bls_setting, + Err(FixtureError::Yaml { .. }) => read_bls_setting(case)?, + Err(e) => return Err(e), + }; + match bls_setting { Some(BlsSetting::Disabled) => Ok(Some(MetadataSkipReason::BlsDisabledExecution)), Some(BlsSetting::Optional | BlsSetting::Enabled) | None => Ok(None), } } -fn load_pre_state(case: &Case) -> std::result::Result { +fn read_bls_setting(case: &Case) -> StdResult, FixtureError> { + let path = CaseFiles::new(case).path(FixtureFile::META); + let text = match fs::read_to_string(&path) { + Ok(text) => text, + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None), + Err(source) => return Err(FixtureError::Read { path, source }), + }; + let value: Value = serde_yaml::from_str(&text).map_err(|source| FixtureError::Yaml { + path: path.clone(), + source, + })?; + let Value::Mapping(mapping) = value else { + return Ok(None); + }; + let Some(value) = mapping.get(Value::String("bls_setting".to_owned())) else { + return Ok(None); + }; + let bls_setting: BlsSetting = serde_yaml::from_value(value.clone()) + .map_err(|source| FixtureError::Yaml { path, source })?; + Ok(Some(bls_setting)) +} + +fn load_pre_state(case: &Case) -> StdResult { match CaseFiles::new(case).decode_ssz_snappy::(FixtureFile::PRE_STATE) { Ok(state) => { trace_pass("decode pre", "decoded pre.ssz_snappy"); @@ -492,7 +651,7 @@ fn load_pre_state(case: &Case) -> std::result::Result { fn finish_state( case: &Case, state: &mut BeaconState, - result: std::result::Result<(), TransitionError>, + result: StdResult<(), TransitionError>, subject: &str, ) -> Outcome { finish_state_with_post(case, FixtureFile::POST_STATE, state, result, subject) @@ -502,7 +661,7 @@ pub(super) fn finish_state_with_post( case: &Case, post_file: FixtureFile, state: &mut BeaconState, - result: std::result::Result<(), TransitionError>, + result: StdResult<(), TransitionError>, subject: &str, ) -> Outcome { let post_filename = post_file.as_str(); @@ -601,75 +760,3 @@ fn state_data(state: &BeaconState) -> String { bid.value.as_u64(), ) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn sanity_shape_support_is_exact_to_runner() { - let cases = [ - (Runner::Sanity, "blocks", true), - (Runner::Sanity, "slots", true), - (Runner::Sanity, "finality", false), - (Runner::Sanity, "random", false), - (Runner::Finality, "finality", true), - (Runner::Finality, "blocks", false), - (Runner::Random, "random", true), - (Runner::Random, "blocks", false), - ]; - - for (runner, handler, expected) in cases { - assert_eq!( - supports(runner, &Handler::new(handler.to_owned())), - expected, - "{runner}/{handler}" - ); - } - } - - #[test] - fn bls_disabled_cases_are_not_claimed_as_supported() { - let case = crate::testing::BLS_DISABLED_ATTESTATION.to_case(); - - let reason = case_skip_reason(&case).expect("read meta"); - assert_eq!(reason, Some(MetadataSkipReason::BlsDisabledExecution)); - } - - #[test] - fn checked_in_real_vector_cases_run_through_registered_adapters() { - let cases = [ - crate::testing::BLS_AGGREGATE_EMPTY_LIST, - crate::testing::BLS_AGGREGATE_VALID_0, - crate::testing::BLS_FAST_AGGREGATE_VERIFY_VALID_0, - ]; - for asset in cases { - assert_adapter_passes(asset); - } - - #[cfg(feature = "minimal")] - { - let cases = [ - crate::testing::EPOCH_EFFECTIVE_BALANCE_HYSTERESIS, - crate::testing::GET_HEAD_GENESIS, - crate::testing::VOLUNTARY_EXIT_BASIC, - crate::testing::SANITY_BLOCK_INVALID_OLD_STYLE_DEPOSIT_REJECTED, - crate::testing::SLOTS_1, - crate::testing::SSZ_STATIC_FORK_RANDOM_0, - ]; - for asset in cases { - assert_adapter_passes(asset); - } - } - } - - fn assert_adapter_passes(asset: crate::testing::AssetCase) { - let case = asset.to_case(); - let outcome = run(&case); - assert!( - matches!(outcome, Outcome::Pass), - "{} failed: {outcome:?}", - case.display_path() - ); - } -} diff --git a/reftests/src/adapters/bls.rs b/tests/src/adapters/bls.rs similarity index 94% rename from reftests/src/adapters/bls.rs rename to tests/src/adapters/bls.rs index 61be128..6f8db54 100644 --- a/reftests/src/adapters/bls.rs +++ b/tests/src/adapters/bls.rs @@ -6,9 +6,9 @@ use serde::Deserialize; -use moonglass::error::SignatureError; -use moonglass::primitives::{BLSPubkey, BLSSignature, Root}; -use moonglass::state_transition::{aggregate_pubkeys, fast_aggregate_verify}; +use moonglass_core::error::SignatureError; +use moonglass_core::primitives::{BLSPubkey, BLSSignature, Root}; +use moonglass_core::state_transition::{aggregate_pubkeys, fast_aggregate_verify}; use crate::adapters::{Adapter, CaseRunner, Outcome, SupportedHandler, trace_fail, trace_pass}; use crate::fixtures::{CaseFiles, FixtureFile, decode_fixed_hex, encode_hex}; @@ -267,15 +267,3 @@ fn parse_signature(hex: &str) -> Result { fn parse_root(hex: &str) -> Result { decode_fixed_hex(hex).map(Root).map_err(|e| e.to_string()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn aggregate_pubkeys_expected_failure_counts_as_pass() { - let case = crate::testing::BLS_AGGREGATE_EMPTY_LIST.to_case(); - let outcome = Bls::run(&case, BlsHandler::AggregatePubkeys); - assert!(matches!(outcome, Outcome::Pass), "{outcome:?}"); - } -} diff --git a/reftests/src/adapters/epoch_processing.rs b/tests/src/adapters/epoch_processing.rs similarity index 98% rename from reftests/src/adapters/epoch_processing.rs rename to tests/src/adapters/epoch_processing.rs index 828e763..9c43663 100644 --- a/reftests/src/adapters/epoch_processing.rs +++ b/tests/src/adapters/epoch_processing.rs @@ -6,8 +6,8 @@ //! adapter reports either comparison independently, but a failure in either one //! fails the case. -use moonglass::containers::BeaconState; -use moonglass::error::TransitionError; +use moonglass_core::containers::BeaconState; +use moonglass_core::error::TransitionError; use super::{ Adapter, CaseRunner, Outcome, SupportedHandler, finish_state, finish_state_with_post, @@ -145,8 +145,6 @@ impl CaseRunner for EpochProcessing { run_epoch_case(case, handler) } } - -#[must_use] fn run_epoch_case(case: &Case, handler: EpochHandler) -> Outcome { let mut state = match load_pre_state(case) { Ok(state) => state, diff --git a/reftests/src/adapters/fork_choice.rs b/tests/src/adapters/fork_choice.rs similarity index 100% rename from reftests/src/adapters/fork_choice.rs rename to tests/src/adapters/fork_choice.rs diff --git a/reftests/src/adapters/fork_choice/checks.rs b/tests/src/adapters/fork_choice/checks.rs similarity index 91% rename from reftests/src/adapters/fork_choice/checks.rs rename to tests/src/adapters/fork_choice/checks.rs index 17964b0..9485188 100644 --- a/reftests/src/adapters/fork_choice/checks.rs +++ b/tests/src/adapters/fork_choice/checks.rs @@ -4,9 +4,11 @@ //! fork-choice checks call back into Moonglass public read APIs, so the //! harness owns only fixture decoding and assertion formatting. -use moonglass::containers::Checkpoint; -use moonglass::fork_choice::{PayloadStatus, Store, diagnostics::get_viable_for_head_nodes}; -use moonglass::primitives::{Epoch, Root}; +use std::collections::HashMap; + +use moonglass_core::containers::Checkpoint; +use moonglass_core::fork_choice::{PayloadStatus, Store, diagnostics::get_viable_for_head_nodes}; +use moonglass_core::primitives::{Epoch, Root}; use super::steps::{ CheckpointHex, Checks, HeadCheck, PayloadStatusCode, PayloadVoteCheck, ProposerHeadCheck, @@ -44,13 +46,7 @@ impl<'a> CheckTrace<'a> { Self { step } } - fn pass(self, label: &'static str, detail: impl FnOnce() -> String) { - if trace_enabled() { - trace_step_check_pass(self.step.index, self.step.tag, label, detail()); - } - } - - fn pass_dynamic(self, label: &str, detail: impl FnOnce() -> String) { + fn pass(self, label: &str, detail: impl FnOnce() -> String) { if trace_enabled() { trace_step_check_pass(self.step.index, self.step.tag, label, detail()); } @@ -220,7 +216,7 @@ fn parse_checkpoint( fn check_payload_votes( trace: CheckTrace<'_>, label: &str, - actual_by_root: &std::collections::HashMap>>, + actual_by_root: &HashMap>>, check: &PayloadVoteCheck, ) -> Result<(), String> { let root = parse_root(trace, label, &check.block_root)?; @@ -239,7 +235,7 @@ fn check_payload_votes( ), ); } - trace.pass_dynamic(label, || format!("got {actual:?} for {root:?}")); + trace.pass(label, || format!("got {actual:?} for {root:?}")); Ok(()) } @@ -355,17 +351,3 @@ fn payload_status_code(status: PayloadStatus) -> PayloadStatusCode { fn root_hex(root: &Root) -> String { format!("0x{}", encode_hex(&root.0)) } - -#[cfg(test)] -mod tests { - use moonglass::fork_choice::PayloadStatus; - - use super::payload_status_code; - - #[test] - fn payload_status_codes_match_fork_choice_format() { - assert_eq!(payload_status_code(PayloadStatus::Empty).as_u8(), 0); - assert_eq!(payload_status_code(PayloadStatus::Full).as_u8(), 1); - assert_eq!(payload_status_code(PayloadStatus::Pending).as_u8(), 2); - } -} diff --git a/reftests/src/adapters/fork_choice/runner.rs b/tests/src/adapters/fork_choice/runner.rs similarity index 77% rename from reftests/src/adapters/fork_choice/runner.rs rename to tests/src/adapters/fork_choice/runner.rs index 33fd671..33214c9 100644 --- a/reftests/src/adapters/fork_choice/runner.rs +++ b/tests/src/adapters/fork_choice/runner.rs @@ -2,14 +2,17 @@ use std::fmt; -use moonglass::containers::{ - Attestation, AttesterSlashing, BeaconBlock, BeaconState, PayloadAttestationMessage, - SignedBeaconBlock, SignedExecutionPayloadEnvelope, +use moonglass_core::containers::{ + Attestation, AttesterSlashing, BeaconBlock, BeaconState, DataColumnSidecar, + PayloadAttestationMessage, SignedBeaconBlock, SignedExecutionPayloadEnvelope, }; -use moonglass::fork_choice::{Store, get_forkchoice_store}; +use moonglass_core::error::ForkChoiceError; +use moonglass_core::fork_choice::{Store, get_forkchoice_store}; +use moonglass_core::ssz::Deserialize as SszDeserialize; +use serde_yaml::Value; use super::checks::{StepContext, assert_checks}; -use super::steps::{Step, parse_steps}; +use super::steps::{BlockStep, Step, parse_steps}; use crate::adapters::{ Outcome, TraceData, root_hex, trace_block_snapshot, trace_enabled, trace_fail, trace_pass, trace_state_snapshot, trace_step_fail, trace_step_info, trace_step_pass, trace_step_pass_item, @@ -48,7 +51,7 @@ pub(super) fn run_case(case: &Case) -> Outcome { } }; - let mut store = match get_forkchoice_store(anchor_state, &anchor_block) { + let mut store = match get_forkchoice_store(&anchor_state, &anchor_block) { Ok(s) => { trace_pass("get_forkchoice_store", "initialized store"); trace_store_snapshot("store", &s); @@ -107,7 +110,12 @@ pub(super) fn run_case(case: &Case) -> Outcome { fn step_detail(step: &Step) -> String { match step { Step::Tick(s) => format!("tick={} valid={}", s.tick, s.valid), - Step::Block(s) => format!("block={} valid={}", s.block, s.valid), + Step::Block(s) => format!( + "block={} columns={} valid={}", + s.block, + s.columns.len(), + s.valid + ), Step::Attestation(s) => format!("attestation={} valid={}", s.attestation, s.valid), Step::AttesterSlashing(s) => { format!( @@ -142,7 +150,7 @@ fn drive_step( ) -> Result, StepFailure> { match step { Step::Tick(s) => expect_step_result(store.on_tick(s.tick), s.valid, "tick"), - Step::Block(s) => apply_block(store, files, &s.block, s.valid, index, tag), + Step::Block(s) => apply_block(store, files, &s, index, tag), Step::Attestation(s) => apply::( store, files, @@ -187,7 +195,7 @@ fn drive_step( } fn expect_step_result( - result: Result<(), moonglass::error::ForkChoiceError>, + result: Result<(), ForkChoiceError>, expect_valid: bool, label: &str, ) -> Result, StepFailure> { @@ -237,13 +245,13 @@ impl fmt::Display for StepFailure { } } -fn describe_step(value: &serde_yaml::Value) -> String { +fn describe_step(value: &Value) -> String { match value { - serde_yaml::Value::Mapping(map) => { + Value::Mapping(map) => { let mut keys: Vec = map .keys() .map(|k| match k { - serde_yaml::Value::String(s) => s.clone(), + Value::String(s) => s.clone(), other => format!("{other:?}"), }) .collect(); @@ -285,11 +293,11 @@ fn store_data(store: &Store) -> String { fn apply_block( store: &mut Store, files: CaseFiles<'_>, - file_stem: &FixtureStem, - expect_valid: bool, + step: &BlockStep, index: usize, tag: &str, ) -> Result, StepFailure> { + let file_stem = &step.block; let signed: SignedBeaconBlock = files .decode_ssz_snappy_stem(file_stem) .map_err(|e| StepFailure::decode(file_stem, e))?; @@ -302,11 +310,58 @@ fn apply_block( ); trace_step_pass_item(index, tag, "input", signed.trace_data()); } - expect_step_result( - store.on_block_with_embedded_messages(&signed), - expect_valid, - file_stem.as_str(), - ) + + // The block step's verdict reflects on_block alone. Recording the block's + // data columns is a downstream side effect that completes data availability + // and can verify a queued payload envelope, so its result is kept out of the + // block's accept or reject decision. on_block_with_embedded_messages runs on + // a copy and commits only on success, so a rejected block leaves the store + // untouched and no outer clone is needed here. + match (store.on_block_with_embedded_messages(&signed), step.valid) { + (Ok(()), true) => { + record_block_columns(store, files, &step.columns, index, tag)?.map_err(|e| { + StepFailure::UnexpectedFailure { + label: format!("{file_stem} columns"), + source: e.to_string(), + } + })?; + Ok(None) + } + (Err(e), false) => Ok(Some(format!("rejected as expected: {e}"))), + (Ok(()), false) => Err(StepFailure::UnexpectedSuccess { + label: file_stem.to_string(), + }), + (Err(e), true) => Err(StepFailure::UnexpectedFailure { + label: file_stem.to_string(), + source: e.to_string(), + }), + } +} + +fn record_block_columns( + store: &mut Store, + files: CaseFiles<'_>, + columns: &[FixtureStem], + index: usize, + tag: &str, +) -> Result, StepFailure> { + for stem in columns { + let sidecar: DataColumnSidecar = files + .decode_ssz_snappy_stem(stem) + .map_err(|e| StepFailure::decode(stem, e))?; + if trace_enabled() { + trace_step_pass_item( + index, + tag, + "decode", + format_args!("decoded {stem}.ssz_snappy"), + ); + } + if let Err(e) = store.record_data_column_sidecar(sidecar) { + return Ok(Err(e)); + } + } + Ok(Ok(())) } fn apply( @@ -319,8 +374,8 @@ fn apply( apply_fn: F, ) -> Result, StepFailure> where - T: ssz_rs::Deserialize + TraceData, - F: FnOnce(&mut Store, &T) -> Result<(), moonglass::error::ForkChoiceError>, + T: SszDeserialize + TraceData, + F: FnOnce(&mut Store, &T) -> Result<(), ForkChoiceError>, { let value: T = files .decode_ssz_snappy_stem(file_stem) diff --git a/reftests/src/adapters/fork_choice/steps.rs b/tests/src/adapters/fork_choice/steps.rs similarity index 89% rename from reftests/src/adapters/fork_choice/steps.rs rename to tests/src/adapters/fork_choice/steps.rs index f4935b8..ded5b0f 100644 --- a/reftests/src/adapters/fork_choice/steps.rs +++ b/tests/src/adapters/fork_choice/steps.rs @@ -8,8 +8,10 @@ //! harness failure. use std::path::Path; +use std::result::Result as StdResult; use serde::{Deserialize, Deserializer, de}; +use serde_yaml::Value; use crate::error::FixtureError; use crate::fixtures::{FixtureStem, read_yaml_path}; @@ -119,13 +121,13 @@ impl<'de> Deserialize<'de> for Step { where D: Deserializer<'de>, { - let value = serde_yaml::Value::deserialize(deserializer)?; + let value = Value::deserialize(deserializer)?; let Some(map) = value.as_mapping() else { return Ok(Self::Other(value)); }; for kind in StepKind::ALL { - if map.contains_key(serde_yaml::Value::String(kind.wire_key().to_owned())) { + if map.contains_key(Value::String(kind.wire_key().to_owned())) { return kind.parse_value(value); } } @@ -133,7 +135,7 @@ impl<'de> Deserialize<'de> for Step { } } -fn parse_step_value(value: serde_yaml::Value, wrap: impl FnOnce(T) -> Step) -> Result +fn parse_step_value(value: Value, wrap: impl FnOnce(T) -> Step) -> Result where T: for<'de> Deserialize<'de>, E: de::Error, @@ -155,19 +157,20 @@ pub(super) struct BlockStep { pub block: FixtureStem, #[serde(default = "yes")] pub valid: bool, - // Optional data-availability inputs in the fixture format: `blobs`/`proofs` - // (blob commitments) and `columns` (data columns). Data availability - // is mocked in this harness, so these are accepted to satisfy - // `deny_unknown_fields` and then ignored. + // `blobs`/`proofs` blob-commitment inputs belong to fixture shapes from forks + // that discovery never runs, so these captures are unreachable for the target + // fork. They exist only so `deny_unknown_fields` tolerates those keys rather + // than rejecting the step. The target fork carries data availability under + // `columns` below instead. #[serde(default)] #[allow(dead_code)] blobs: Option, #[serde(default)] #[allow(dead_code)] proofs: Option, + /// Data-column sidecar fixture stems to record for this block. #[serde(default)] - #[allow(dead_code)] - columns: Option, + pub columns: Vec, } #[derive(Debug, Deserialize)] @@ -403,32 +406,6 @@ fn yes() -> bool { true } -pub(super) fn parse_steps(path: &Path) -> std::result::Result, FixtureError> { +pub(super) fn parse_steps(path: &Path) -> StdResult, FixtureError> { read_yaml_path(path) } - -#[cfg(test)] -mod tests { - use super::{Step, parse_steps}; - - #[test] - fn parses_checked_in_target_fork_get_head_steps() { - let path = crate::testing::GET_HEAD_GENESIS.file("steps.yaml"); - let steps = parse_steps(&path).expect("parse steps"); - - let [Step::Checks(step)] = steps.as_slice() else { - panic!("expected checks step"); - }; - assert_eq!(step.checks.genesis_time, Some(0)); - let head = step.checks.head.as_ref().expect("head check"); - assert_eq!(head.slot, 0); - assert_eq!( - head.root, - "0x2a9cb69f4de117f55f3cd886f2765427572b62e31666bd9ba53d19a69e148418" - ); - assert_eq!( - head.payload_status.map(super::PayloadStatusCode::as_u8), - Some(0) - ); - } -} diff --git a/tests/src/adapters/kzg.rs b/tests/src/adapters/kzg.rs new file mode 100644 index 0000000..812866d --- /dev/null +++ b/tests/src/adapters/kzg.rs @@ -0,0 +1,612 @@ +//! Adapter for `kzg` reference-test fixtures. + +use std::fmt::Display; +use std::sync::OnceLock; + +use serde::Deserialize; +use serde::de::DeserializeOwned; + +use moonglass_core::constants::{BYTES_PER_BLOB, BYTES_PER_CELL, BYTES_PER_FIELD_ELEMENT}; +use moonglass_core::crypto::kzg::{ + BLSFieldElement, EthereumKzgSetup, bls_field_to_bytes, bytes_to_bls_field, compute_cells, + compute_cells_and_kzg_proofs, compute_verify_cell_kzg_proof_batch_challenge, + recover_cells_and_kzg_proofs, verify_cell_kzg_proof_batch, +}; +use moonglass_core::primitives::{ + Cell, CellIndex, CommitmentIndex, KZG_COMMITMENT_BYTES, KZG_PROOF_BYTES, KZGCommitment, + KZGProof, +}; + +use crate::adapters::{Adapter, CaseRunner, Outcome, SupportedHandler, trace_fail, trace_pass}; +use crate::fixtures::{CaseFiles, FixtureFile, decode_fixed_hex, encode_hex}; +use crate::inventory::{Case, Runner}; + +const DATA: FixtureFile = FixtureFile::new("data.yaml"); + +pub(super) static ADAPTER: Adapter = Adapter::new(); + +pub(super) struct Kzg; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum KzgHandler { + ComputeCells, + ComputeCellsAndKzgProofs, + ComputeVerifyCellKzgProofBatchChallenge, + RecoverCellsAndKzgProofs, + VerifyCellKzgProofBatch, +} + +impl KzgHandler { + const COMPUTE_CELLS: &'static str = "compute_cells"; + const COMPUTE_CELLS_AND_KZG_PROOFS: &'static str = "compute_cells_and_kzg_proofs"; + const COMPUTE_VERIFY_CELL_KZG_PROOF_BATCH_CHALLENGE: &'static str = + "compute_verify_cell_kzg_proof_batch_challenge"; + const RECOVER_CELLS_AND_KZG_PROOFS: &'static str = "recover_cells_and_kzg_proofs"; + const VERIFY_CELL_KZG_PROOF_BATCH: &'static str = "verify_cell_kzg_proof_batch"; +} + +impl SupportedHandler for KzgHandler { + const ALL: &'static [Self] = &[ + Self::ComputeCells, + Self::ComputeCellsAndKzgProofs, + Self::ComputeVerifyCellKzgProofBatchChallenge, + Self::RecoverCellsAndKzgProofs, + Self::VerifyCellKzgProofBatch, + ]; + + fn as_str(self) -> &'static str { + match self { + Self::ComputeCells => Self::COMPUTE_CELLS, + Self::ComputeCellsAndKzgProofs => Self::COMPUTE_CELLS_AND_KZG_PROOFS, + Self::ComputeVerifyCellKzgProofBatchChallenge => { + Self::COMPUTE_VERIFY_CELL_KZG_PROOF_BATCH_CHALLENGE + } + Self::RecoverCellsAndKzgProofs => Self::RECOVER_CELLS_AND_KZG_PROOFS, + Self::VerifyCellKzgProofBatch => Self::VERIFY_CELL_KZG_PROOF_BATCH, + } + } +} + +impl KzgHandler { + fn run(self, case: &Case) -> Outcome { + match self { + Self::ComputeCells => { + let case = match read_data::(case) { + Ok(case) => case, + Err(outcome) => return outcome, + }; + handle_compute_cells(&case) + } + Self::ComputeCellsAndKzgProofs => { + let case = match read_data::(case) { + Ok(case) => case, + Err(outcome) => return outcome, + }; + handle_compute_cells_and_kzg_proofs(&case) + } + Self::ComputeVerifyCellKzgProofBatchChallenge => { + let case = match read_data::(case) { + Ok(case) => case, + Err(outcome) => return outcome, + }; + handle_compute_verify_cell_kzg_proof_batch_challenge(&case) + } + Self::RecoverCellsAndKzgProofs => { + let case = match read_data::(case) { + Ok(case) => case, + Err(outcome) => return outcome, + }; + handle_recover_cells_and_kzg_proofs(&case) + } + Self::VerifyCellKzgProofBatch => { + let case = match read_data::(case) { + Ok(case) => case, + Err(outcome) => return outcome, + }; + handle_verify_cell_kzg_proof_batch(&case) + } + } + } +} + +impl CaseRunner for Kzg { + type Handler = KzgHandler; + + const RUNNER: Runner = Runner::Kzg; + + fn run(case: &Case, handler: Self::Handler) -> Outcome { + handler.run(case) + } +} + +fn read_data(case: &Case) -> Result +where + T: DeserializeOwned, +{ + match CaseFiles::new(case).read_yaml(DATA) { + Ok(data) => { + trace_pass("kzg data", format_args!("read {}", DATA.as_str())); + Ok(data) + } + Err(e) => { + let detail = format!("read {}: {e}", DATA.as_str()); + trace_fail("kzg data", &detail); + Err(Outcome::Fail(detail)) + } + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct CellKzgProofBatchCase { + input: CellKzgProofBatchInput, + output: Option, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ComputeCellsCase { + input: BlobInput, + output: Option>, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ComputeCellsAndKzgProofsCase { + input: BlobInput, + output: Option, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct RecoverCellsAndKzgProofsCase { + input: RecoverCellsAndKzgProofsInput, + output: Option, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ComputeVerifyCellKzgProofBatchChallengeCase { + input: ComputeVerifyCellKzgProofBatchChallengeInput, + output: String, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct BlobInput { + blob: String, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct CellKzgProofBatchInput { + commitments: Vec, + cell_indices: Vec, + cells: Vec, + proofs: Vec, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct RecoverCellsAndKzgProofsInput { + cell_indices: Vec, + cells: Vec, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ComputeVerifyCellKzgProofBatchChallengeInput { + commitments: Vec, + commitment_indices: Vec, + cell_indices: Vec, + cosets_evals: Vec>, + proofs: Vec, +} + +type CellsAndProofsOutput = (Vec, Vec); + +fn handle_compute_cells(case: &ComputeCellsCase) -> Outcome { + let blob = match decode_blob(&case.input.blob) { + Ok(blob) => blob, + Err(e) => return input_failure(&e, case.output.is_none()), + }; + let result = compute_cells(&blob); + match result { + Ok(cells) => match &case.output { + Some(want) => compare_cells("kzg compute_cells", &cells, want), + None => fail_expected_error("kzg compute_cells"), + }, + Err(e) if case.output.is_none() => { + trace_pass("kzg compute_cells", format_args!("failed as expected: {e}")); + Outcome::Pass + } + Err(e) => operation_failure("kzg compute_cells", e), + } +} + +fn handle_compute_cells_and_kzg_proofs(case: &ComputeCellsAndKzgProofsCase) -> Outcome { + let blob = match decode_blob(&case.input.blob) { + Ok(blob) => blob, + Err(e) => return input_failure(&e, case.output.is_none()), + }; + let setup = match mainnet_setup() { + Ok(setup) => setup, + Err(e) => return setup_failure(&e), + }; + let result = compute_cells_and_kzg_proofs(setup, &blob); + match result { + Ok((cells, proofs)) => match &case.output { + Some((want_cells, want_proofs)) => compare_cells_and_proofs( + "kzg compute_cells_and_kzg_proofs", + &cells, + &proofs, + want_cells, + want_proofs, + ), + None => fail_expected_error("kzg compute_cells_and_kzg_proofs"), + }, + Err(e) if case.output.is_none() => { + trace_pass( + "kzg compute_cells_and_kzg_proofs", + format_args!("failed as expected: {e}"), + ); + Outcome::Pass + } + Err(e) => operation_failure("kzg compute_cells_and_kzg_proofs", e), + } +} + +fn handle_recover_cells_and_kzg_proofs(case: &RecoverCellsAndKzgProofsCase) -> Outcome { + let cells = match parse_hex_items::(&case.input.cells, "cells", Cell) { + Ok(items) => items, + Err(e) => return input_failure(&e, case.output.is_none()), + }; + let cell_indices = case + .input + .cell_indices + .iter() + .copied() + .map(CellIndex::new) + .collect::>(); + let setup = match mainnet_setup() { + Ok(setup) => setup, + Err(e) => return setup_failure(&e), + }; + let result = recover_cells_and_kzg_proofs(setup, cell_indices, cells); + match result { + Ok((cells, proofs)) => match &case.output { + Some((want_cells, want_proofs)) => compare_cells_and_proofs( + "kzg recover_cells_and_kzg_proofs", + &cells, + &proofs, + want_cells, + want_proofs, + ), + None => fail_expected_error("kzg recover_cells_and_kzg_proofs"), + }, + Err(e) if case.output.is_none() => { + trace_pass( + "kzg recover_cells_and_kzg_proofs", + format_args!("failed as expected: {e}"), + ); + Outcome::Pass + } + Err(e) => operation_failure("kzg recover_cells_and_kzg_proofs", e), + } +} + +fn handle_compute_verify_cell_kzg_proof_batch_challenge( + case: &ComputeVerifyCellKzgProofBatchChallengeCase, +) -> Outcome { + let commitments = match parse_hex_items::( + &case.input.commitments, + "commitments", + KZGCommitment, + ) { + Ok(items) => items, + Err(e) => return input_failure(&e, false), + }; + let proofs = match parse_hex_items::( + &case.input.proofs, + "proofs", + KZGProof, + ) { + Ok(items) => items, + Err(e) => return input_failure(&e, false), + }; + let cosets_evals = match parse_cosets_evals(&case.input.cosets_evals) { + Ok(items) => items, + Err(e) => return input_failure(&e, false), + }; + let commitment_indices = case + .input + .commitment_indices + .iter() + .copied() + .map(CommitmentIndex::new) + .collect::>(); + let cell_indices = case + .input + .cell_indices + .iter() + .copied() + .map(CellIndex::new) + .collect::>(); + let challenge = compute_verify_cell_kzg_proof_batch_challenge( + &commitments, + &commitment_indices, + &cell_indices, + &cosets_evals, + &proofs, + ); + let got = format!("0x{}", encode_hex(&bls_field_to_bytes(challenge))); + if got == case.output { + trace_pass( + "kzg compute_verify_cell_kzg_proof_batch_challenge", + format_args!("got {got}"), + ); + Outcome::Pass + } else { + let detail = format!("expected {}, got {got}", case.output); + trace_fail("kzg compute_verify_cell_kzg_proof_batch_challenge", &detail); + Outcome::Fail(detail) + } +} + +fn handle_verify_cell_kzg_proof_batch(case: &CellKzgProofBatchCase) -> Outcome { + let commitments = match parse_hex_items::( + &case.input.commitments, + "commitments", + KZGCommitment, + ) { + Ok(items) => items, + Err(e) => return input_failure(&e, case.output.is_none()), + }; + let cells = match parse_hex_items::(&case.input.cells, "cells", Cell) { + Ok(items) => items, + Err(e) => return input_failure(&e, case.output.is_none()), + }; + let proofs = match parse_hex_items::( + &case.input.proofs, + "proofs", + KZGProof, + ) { + Ok(items) => items, + Err(e) => return input_failure(&e, case.output.is_none()), + }; + let cell_indices = case + .input + .cell_indices + .iter() + .copied() + .map(CellIndex::new) + .collect::>(); + trace_pass( + "kzg input", + format_args!( + "decoded commitments={} cell_indices={} cells={} proofs={}", + commitments.len(), + cell_indices.len(), + cells.len(), + proofs.len() + ), + ); + + let setup = match mainnet_setup() { + Ok(setup) => setup, + Err(e) => return setup_failure(&e), + }; + + let result = verify_cell_kzg_proof_batch(setup, &commitments, &cell_indices, &cells, &proofs); + match (result, case.output) { + (Ok(got), Some(want)) if got == want => { + trace_pass("kzg verify_cell_kzg_proof_batch", format_args!("got {got}")); + Outcome::Pass + } + (Ok(got), Some(want)) => { + let detail = format!("expected {want}, got {got}"); + trace_fail("kzg verify_cell_kzg_proof_batch", &detail); + Outcome::Fail(detail) + } + (Ok(got), None) => { + let detail = format!("expected failure, got {got}"); + trace_fail("kzg verify_cell_kzg_proof_batch", &detail); + Outcome::Fail(detail) + } + (Err(e), None) => { + trace_pass( + "kzg verify_cell_kzg_proof_batch", + format_args!("failed as expected: {e}"), + ); + Outcome::Pass + } + (Err(e), Some(_)) => { + let detail = format!("verification failed: {e}"); + trace_fail("kzg verify_cell_kzg_proof_batch", &detail); + Outcome::Fail(detail) + } + } +} + +fn parse_hex_items( + items: &[String], + label: &'static str, + wrap: fn([u8; N]) -> T, +) -> Result, String> { + items + .iter() + .enumerate() + .map(|(index, hex)| { + decode_fixed_hex::(hex) + .map(wrap) + .map_err(|e| format!("{label}[{index}]: {e}")) + }) + .collect() +} + +fn parse_cosets_evals(items: &[Vec]) -> Result>, String> { + items + .iter() + .enumerate() + .map(|(coset_index, coset)| { + coset + .iter() + .enumerate() + .map(|(value_index, hex)| { + let bytes = decode_fixed_hex::(hex) + .map_err(|e| format!("cosets_evals[{coset_index}][{value_index}]: {e}"))?; + bytes_to_bls_field(bytes) + .map_err(|e| format!("cosets_evals[{coset_index}][{value_index}]: {e}")) + }) + .collect() + }) + .collect() +} + +fn decode_blob(blob: &str) -> Result, String> { + decode_fixed_hex::(blob) + .map(|bytes| bytes.to_vec()) + .map_err(|e| format!("blob: {e}")) +} + +fn compare_cells(subject: &'static str, got: &[Cell], want: &[String]) -> Outcome { + match parse_hex_items::(want, "output cells", Cell) { + Ok(want) if got == want.as_slice() => { + trace_pass(subject, format_args!("got {} cells", got.len())); + Outcome::Pass + } + Ok(want) => { + let detail = cell_mismatch_detail(got, &want); + trace_fail(subject, &detail); + Outcome::Fail(detail) + } + Err(e) => { + let detail = format!("decode expected output: {e}"); + trace_fail(subject, &detail); + Outcome::Fail(detail) + } + } +} + +fn compare_cells_and_proofs( + subject: &'static str, + got_cells: &[Cell], + got_proofs: &[KZGProof], + want_cells: &[String], + want_proofs: &[String], +) -> Outcome { + let want_cells = match parse_hex_items::(want_cells, "output cells", Cell) + { + Ok(items) => items, + Err(e) => { + let detail = format!("decode expected cells: {e}"); + trace_fail(subject, &detail); + return Outcome::Fail(detail); + } + }; + let want_proofs = match parse_hex_items::( + want_proofs, + "output proofs", + KZGProof, + ) { + Ok(items) => items, + Err(e) => { + let detail = format!("decode expected proofs: {e}"); + trace_fail(subject, &detail); + return Outcome::Fail(detail); + } + }; + if got_cells == want_cells.as_slice() && got_proofs == want_proofs.as_slice() { + trace_pass( + subject, + format_args!("got cells={} proofs={}", got_cells.len(), got_proofs.len()), + ); + Outcome::Pass + } else { + let detail = + cells_and_proofs_mismatch_detail(got_cells, got_proofs, &want_cells, &want_proofs); + trace_fail(subject, &detail); + Outcome::Fail(detail) + } +} + +fn cell_mismatch_detail(got: &[Cell], want: &[Cell]) -> String { + if got.len() != want.len() { + return format!("expected {} cells, got {}", want.len(), got.len()); + } + for (index, (got, want)) in got.iter().zip(want).enumerate() { + if got != want { + return format!( + "cell[{index}] expected 0x{}, got 0x{}", + encode_hex(&want.0), + encode_hex(&got.0) + ); + } + } + "cells differ".to_owned() +} + +fn cells_and_proofs_mismatch_detail( + got_cells: &[Cell], + got_proofs: &[KZGProof], + want_cells: &[Cell], + want_proofs: &[KZGProof], +) -> String { + if got_cells != want_cells { + return cell_mismatch_detail(got_cells, want_cells); + } + if got_proofs.len() != want_proofs.len() { + return format!( + "expected {} proofs, got {}", + want_proofs.len(), + got_proofs.len() + ); + } + for (index, (got, want)) in got_proofs.iter().zip(want_proofs).enumerate() { + if got != want { + return format!( + "proof[{index}] expected 0x{}, got 0x{}", + encode_hex(&want.0), + encode_hex(&got.0) + ); + } + } + "cells and proofs differ".to_owned() +} + +fn input_failure(detail: &str, expected_failure: bool) -> Outcome { + if expected_failure { + trace_pass("kzg input", format_args!("failed as expected: {detail}")); + Outcome::Pass + } else { + let message = format!("decode input: {detail}"); + trace_fail("kzg input", &message); + Outcome::Fail(message) + } +} + +fn fail_expected_error(subject: &'static str) -> Outcome { + let detail = "expected failure, got success".to_owned(); + trace_fail(subject, &detail); + Outcome::Fail(detail) +} + +fn operation_failure(subject: &'static str, error: impl Display) -> Outcome { + let detail = format!("operation failed: {error}"); + trace_fail(subject, &detail); + Outcome::Fail(detail) +} + +fn setup_failure(error: &str) -> Outcome { + let detail = format!("load KZG setup: {error}"); + trace_fail("kzg setup", &detail); + Outcome::Fail(detail) +} + +fn mainnet_setup() -> Result<&'static EthereumKzgSetup, String> { + static SETUP: OnceLock> = OnceLock::new(); + SETUP + .get_or_init(|| EthereumKzgSetup::mainnet().map_err(|e| e.to_string())) + .as_ref() + .map_err(Clone::clone) +} diff --git a/tests/src/adapters/networking.rs b/tests/src/adapters/networking.rs new file mode 100644 index 0000000..1562d4b --- /dev/null +++ b/tests/src/adapters/networking.rs @@ -0,0 +1,160 @@ +//! Adapter for `networking` reference-test fixtures. + +use serde::Deserialize; + +use moonglass_core::containers::{ + compute_columns_for_custody_group, get_custody_groups, node_id_from_decimal, +}; +use moonglass_core::primitives::CustodyIndex; + +use crate::adapters::{Adapter, CaseRunner, Outcome, SupportedHandler, trace_fail, trace_pass}; +use crate::fixtures::{CaseFiles, FixtureFile}; +use crate::inventory::{Case, Runner}; + +const META: FixtureFile = FixtureFile::new("meta.yaml"); + +pub(super) static ADAPTER: Adapter = Adapter::new(); + +pub(super) struct Networking; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum NetworkingHandler { + ComputeColumnsForCustodyGroup, + GetCustodyGroups, +} + +impl NetworkingHandler { + const COMPUTE_COLUMNS_FOR_CUSTODY_GROUP: &'static str = "compute_columns_for_custody_group"; + const GET_CUSTODY_GROUPS: &'static str = "get_custody_groups"; +} + +impl SupportedHandler for NetworkingHandler { + const ALL: &'static [Self] = &[Self::ComputeColumnsForCustodyGroup, Self::GetCustodyGroups]; + + fn as_str(self) -> &'static str { + match self { + Self::ComputeColumnsForCustodyGroup => Self::COMPUTE_COLUMNS_FOR_CUSTODY_GROUP, + Self::GetCustodyGroups => Self::GET_CUSTODY_GROUPS, + } + } +} + +impl NetworkingHandler { + fn run(self, case: &Case) -> Outcome { + match self { + Self::ComputeColumnsForCustodyGroup => { + let case = match read_meta::(case) { + Ok(case) => case, + Err(outcome) => return outcome, + }; + handle_compute_columns_for_custody_group(&case) + } + Self::GetCustodyGroups => { + let case = match read_meta::(case) { + Ok(case) => case, + Err(outcome) => return outcome, + }; + handle_get_custody_groups(&case) + } + } + } +} + +impl CaseRunner for Networking { + type Handler = NetworkingHandler; + + const RUNNER: Runner = Runner::Networking; + + fn run(case: &Case, handler: Self::Handler) -> Outcome { + handler.run(case) + } +} + +fn read_meta(case: &Case) -> Result +where + T: for<'de> Deserialize<'de>, +{ + match CaseFiles::new(case).read_yaml(META) { + Ok(data) => { + trace_pass("networking meta", format_args!("read {}", META.as_str())); + Ok(data) + } + Err(e) => { + let detail = format!("read {}: {e}", META.as_str()); + trace_fail("networking meta", &detail); + Err(Outcome::Fail(detail)) + } + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ComputeColumnsForCustodyGroupCase { + custody_group: u64, + result: Vec, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct GetCustodyGroupsCase { + node_id: String, + custody_group_count: u64, + result: Vec, +} + +fn handle_compute_columns_for_custody_group(case: &ComputeColumnsForCustodyGroupCase) -> Outcome { + match compute_columns_for_custody_group(CustodyIndex::new(case.custody_group)) { + Ok(columns) => { + let got = columns + .iter() + .map(|column| column.as_u64()) + .collect::>(); + compare_u64_list( + "networking compute_columns_for_custody_group", + &got, + &case.result, + ) + } + Err(e) => { + let detail = format!("compute columns for custody group: {e}"); + trace_fail("networking compute_columns_for_custody_group", &detail); + Outcome::Fail(detail) + } + } +} + +fn handle_get_custody_groups(case: &GetCustodyGroupsCase) -> Outcome { + let node_id = match node_id_from_decimal(&case.node_id) { + Ok(node_id) => node_id, + Err(e) => { + let detail = format!("parse node id: {e}"); + trace_fail("networking get_custody_groups", &detail); + return Outcome::Fail(detail); + } + }; + match get_custody_groups(node_id, case.custody_group_count) { + Ok(groups) => { + let got = groups + .iter() + .map(|group| group.as_u64()) + .collect::>(); + compare_u64_list("networking get_custody_groups", &got, &case.result) + } + Err(e) => { + let detail = format!("get custody groups: {e}"); + trace_fail("networking get_custody_groups", &detail); + Outcome::Fail(detail) + } + } +} + +fn compare_u64_list(subject: &'static str, got: &[u64], want: &[u64]) -> Outcome { + if got == want { + trace_pass(subject, format_args!("matched {} values", got.len())); + Outcome::Pass + } else { + let detail = format!("expected {want:?}, got {got:?}"); + trace_fail(subject, &detail); + Outcome::Fail(detail) + } +} diff --git a/reftests/src/adapters/operations.rs b/tests/src/adapters/operations.rs similarity index 84% rename from reftests/src/adapters/operations.rs rename to tests/src/adapters/operations.rs index 85c5cca..02553e4 100644 --- a/reftests/src/adapters/operations.rs +++ b/tests/src/adapters/operations.rs @@ -2,18 +2,19 @@ //! //! Operation fixtures share the same state-transition harness. Input //! operations decode one operation-specific SSZ file before calling the -//! corresponding `BeaconState` method; state-only operations call directly into +//! corresponding `BeaconState` method. State-only operations call directly into //! the state. Missing post-state means the operation is expected to be rejected. use std::marker::PhantomData; -use moonglass::containers::{ +use moonglass_core::containers::{ Attestation, AttesterSlashing, BeaconBlock, BeaconState, BuilderDepositRequest, BuilderExitRequest, ConsolidationRequest, DepositRequest, PayloadAttestation, ProposerSlashing, SignedBLSToExecutionChange, SignedExecutionPayloadBid, SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, }; -use moonglass::error::TransitionError; +use moonglass_core::error::TransitionError; +use moonglass_core::ssz::Deserialize as SszDeserialize; use super::{ Adapter, CaseRunner, Outcome, StateTransition, SupportedHandler, TraceData, run_state_case, @@ -109,35 +110,29 @@ impl SupportedHandler for OperationHandler { impl OperationHandler { fn apply(self, case: &Case, state: &mut BeaconState) -> StateTransition { - self.operation().apply(case, state) - } - - fn operation(self) -> &'static dyn Operation { match self { - Self::VoluntaryExit | Self::VoluntaryExitChurn => &VOLUNTARY_EXIT_OPERATION, - Self::BlsToExecutionChange => &BLS_TO_EXECUTION_CHANGE_OPERATION, - Self::Attestation => &ATTESTATION_OPERATION, - Self::AttesterSlashing => &ATTESTER_SLASHING_OPERATION, - Self::ProposerSlashing => &PROPOSER_SLASHING_OPERATION, - Self::SyncAggregate => &SYNC_AGGREGATE_OPERATION, - Self::BlockHeader => &BLOCK_HEADER_OPERATION, - Self::PayloadAttestation => &PAYLOAD_ATTESTATION_OPERATION, - Self::DepositRequest => &DEPOSIT_REQUEST_OPERATION, - Self::BuilderDepositRequest => &BUILDER_DEPOSIT_REQUEST_OPERATION, - Self::BuilderExitRequest => &BUILDER_EXIT_REQUEST_OPERATION, - Self::WithdrawalRequest => &WITHDRAWAL_REQUEST_OPERATION, - Self::ConsolidationRequest => &CONSOLIDATION_REQUEST_OPERATION, - Self::ExecutionPayloadBid => &EXECUTION_PAYLOAD_BID_OPERATION, - Self::ParentExecutionPayload => &PARENT_EXECUTION_PAYLOAD_OPERATION, - Self::Withdrawals => &WITHDRAWALS_OPERATION, + Self::VoluntaryExit | Self::VoluntaryExitChurn => { + VOLUNTARY_EXIT_OPERATION.apply(case, state) + } + Self::BlsToExecutionChange => BLS_TO_EXECUTION_CHANGE_OPERATION.apply(case, state), + Self::Attestation => ATTESTATION_OPERATION.apply(case, state), + Self::AttesterSlashing => ATTESTER_SLASHING_OPERATION.apply(case, state), + Self::ProposerSlashing => PROPOSER_SLASHING_OPERATION.apply(case, state), + Self::SyncAggregate => SYNC_AGGREGATE_OPERATION.apply(case, state), + Self::BlockHeader => BLOCK_HEADER_OPERATION.apply(case, state), + Self::PayloadAttestation => PAYLOAD_ATTESTATION_OPERATION.apply(case, state), + Self::DepositRequest => DEPOSIT_REQUEST_OPERATION.apply(case, state), + Self::BuilderDepositRequest => BUILDER_DEPOSIT_REQUEST_OPERATION.apply(case, state), + Self::BuilderExitRequest => BUILDER_EXIT_REQUEST_OPERATION.apply(case, state), + Self::WithdrawalRequest => WITHDRAWAL_REQUEST_OPERATION.apply(case, state), + Self::ConsolidationRequest => CONSOLIDATION_REQUEST_OPERATION.apply(case, state), + Self::ExecutionPayloadBid => EXECUTION_PAYLOAD_BID_OPERATION.apply(case, state), + Self::ParentExecutionPayload => PARENT_EXECUTION_PAYLOAD_OPERATION.apply(case, state), + Self::Withdrawals => WITHDRAWALS_OPERATION.apply(case, state), } } } -trait Operation: Sync { - fn apply(&self, case: &Case, state: &mut BeaconState) -> StateTransition; -} - struct InputOperation { file: FixtureFile, apply: fn(&mut BeaconState, &T) -> Result<(), TransitionError>, @@ -157,9 +152,9 @@ impl InputOperation { } } -impl Operation for InputOperation +impl InputOperation where - T: ssz_rs::Deserialize + TraceData, + T: SszDeserialize + TraceData, { fn apply(&self, case: &Case, state: &mut BeaconState) -> StateTransition { match CaseFiles::new(case).decode_ssz_snappy::(self.file) { @@ -192,7 +187,7 @@ impl StateOperation { } } -impl Operation for StateOperation { +impl StateOperation { fn apply(&self, _case: &Case, state: &mut BeaconState) -> StateTransition { trace_pass("input", "operation uses pre-state only"); StateTransition::Applied((self.apply)(state)) @@ -260,7 +255,7 @@ static EXECUTION_PAYLOAD_BID_OPERATION: InputOperation = InputOperation::new( FixtureFile::new("block.ssz_snappy"), - BeaconState::accept_parent_payload_commitment, + BeaconState::process_parent_execution_payload, ); static WITHDRAWALS_OPERATION: StateOperation = StateOperation::new(BeaconState::process_withdrawals); diff --git a/tests/src/adapters/rewards.rs b/tests/src/adapters/rewards.rs new file mode 100644 index 0000000..06b0f32 --- /dev/null +++ b/tests/src/adapters/rewards.rs @@ -0,0 +1,237 @@ +//! Adapter for `rewards` reference-test fixtures. +//! +//! Each case starts from `pre.ssz_snappy` and checks the per-validator reward +//! and penalty vectors the state produces against four expected sidecars. Three +//! sidecars cover the participation flag deltas for the timely source, target, +//! and head flags. The fourth covers the inactivity-leak deltas. A case passes +//! only when every reward and penalty entry matches the expected value exactly. + +use std::result::Result as StdResult; + +use moonglass_core::constants::{ + TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, + VALIDATOR_REGISTRY_LIMIT, +}; +use moonglass_core::primitives::Gwei; +use moonglass_core::ssz::{ + ContainerDecoder, Deserialize as SszDeserialize, DeserializeError, List, SszSized, + container_is_variable_size, container_size_hint, field_layout, +}; + +use super::{ + Adapter, CaseRunner, Outcome, SupportedHandler, load_pre_state, trace_fail, trace_pass, +}; +use crate::fixtures::{CaseFiles, FixtureFile}; +use crate::inventory::{Case, Runner}; + +/// Expected deltas for the timely source participation flag. +const SOURCE_DELTAS: FixtureFile = FixtureFile::new("source_deltas.ssz_snappy"); +/// Expected deltas for the timely target participation flag. +const TARGET_DELTAS: FixtureFile = FixtureFile::new("target_deltas.ssz_snappy"); +/// Expected deltas for the timely head participation flag. +const HEAD_DELTAS: FixtureFile = FixtureFile::new("head_deltas.ssz_snappy"); +/// Expected deltas for the inactivity-leak penalty. +const INACTIVITY_PENALTY_DELTAS: FixtureFile = + FixtureFile::new("inactivity_penalty_deltas.ssz_snappy"); + +/// Statically registered `rewards` adapter. +pub(super) static ADAPTER: Adapter = Adapter::new(); + +/// Zero-sized runner implementation for the rewards family. +pub(super) struct Rewards; + +impl CaseRunner for Rewards { + type Handler = RewardsHandler; + + const RUNNER: Runner = Runner::Rewards; + + fn run(case: &Case, handler: Self::Handler) -> Outcome { + run(case, handler) + } +} + +/// Upstream handler family inside the rewards runner. +#[derive(Clone, Copy)] +pub(super) struct RewardsHandler { + /// Upstream handler directory name. + name: &'static str, +} + +impl RewardsHandler { + /// Upstream `basic` handler directory name. + const BASIC: &'static str = "basic"; +} + +impl SupportedHandler for RewardsHandler { + const ALL: &'static [Self] = &[Self { name: Self::BASIC }]; + + fn as_str(self) -> &'static str { + self.name + } +} + +/// Test-side mirror of the consensus-spec `Deltas` container. +/// +/// The container holds one reward vector and one penalty vector, each a bounded +/// list of `uint64` values indexed by validator. Both fields are variable size, +/// so the encoding places an offset for each field followed by the two payloads. +struct Deltas { + /// Per-validator reward amounts. + rewards: List, + /// Per-validator penalty amounts. + penalties: List, +} + +/// Field layout for [`Deltas`]. +fn deltas_layout() -> [moonglass_core::ssz::FieldLayout; 2] { + [ + field_layout::>(), + field_layout::>(), + ] +} + +impl SszSized for Deltas { + fn is_variable_size() -> bool { + container_is_variable_size(&deltas_layout()) + } + + fn size_hint() -> usize { + container_size_hint(&deltas_layout()) + } +} + +impl SszDeserialize for Deltas { + fn deserialize(encoding: &[u8]) -> StdResult { + let mut decoder = ContainerDecoder::new(encoding, &deltas_layout())?; + Ok(Self { + rewards: decoder.deserialize_next::>()?, + penalties: decoder.deserialize_next::>()?, + }) + } +} + +/// One reward and penalty comparison the case must satisfy. +struct DeltaCheck { + /// Sidecar file holding the expected reward and penalty vectors. + file: FixtureFile, + /// Computed reward and penalty vectors as `Gwei`. + computed: (Vec, Vec), +} + +fn run(case: &Case, _handler: RewardsHandler) -> Outcome { + let state = match load_pre_state(case) { + Ok(state) => state, + Err(msg) => return Outcome::Fail(msg), + }; + + let source = match state.get_flag_index_deltas(TIMELY_SOURCE_FLAG_INDEX) { + Ok(deltas) => deltas, + Err(e) => return compute_failure(SOURCE_DELTAS.as_str(), &e.to_string()), + }; + let target = match state.get_flag_index_deltas(TIMELY_TARGET_FLAG_INDEX) { + Ok(deltas) => deltas, + Err(e) => return compute_failure(TARGET_DELTAS.as_str(), &e.to_string()), + }; + let head = match state.get_flag_index_deltas(TIMELY_HEAD_FLAG_INDEX) { + Ok(deltas) => deltas, + Err(e) => return compute_failure(HEAD_DELTAS.as_str(), &e.to_string()), + }; + let inactivity = match state.get_inactivity_penalty_deltas() { + Ok(deltas) => deltas, + Err(e) => return compute_failure(INACTIVITY_PENALTY_DELTAS.as_str(), &e.to_string()), + }; + + let checks = [ + DeltaCheck { + file: SOURCE_DELTAS, + computed: source, + }, + DeltaCheck { + file: TARGET_DELTAS, + computed: target, + }, + DeltaCheck { + file: HEAD_DELTAS, + computed: head, + }, + DeltaCheck { + file: INACTIVITY_PENALTY_DELTAS, + computed: inactivity, + }, + ]; + + let files = CaseFiles::new(case); + let mut outcome = Outcome::Pass; + for check in checks { + outcome = outcome.combine(check_one(files, &check)); + } + outcome +} + +/// Compare one computed reward and penalty vector against its sidecar. +fn check_one(files: CaseFiles, check: &DeltaCheck) -> Outcome { + let name = check.file.as_str(); + let expected = match decode_deltas(files, check.file) { + Ok(deltas) => deltas, + Err(detail) => { + trace_fail(format_args!("rewards {name}"), &detail); + return Outcome::Fail(detail); + } + }; + let (rewards, penalties) = &check.computed; + if let Some(detail) = compare_field(name, "rewards", rewards, &expected.rewards) { + trace_fail(format_args!("rewards {name}"), &detail); + return Outcome::Fail(detail); + } + if let Some(detail) = compare_field(name, "penalties", penalties, &expected.penalties) { + trace_fail(format_args!("rewards {name}"), &detail); + return Outcome::Fail(detail); + } + trace_pass( + format_args!("rewards {name}"), + "rewards and penalties match", + ); + Outcome::Pass +} + +/// Decode one `Deltas` sidecar from its SSZ-snappy file. +fn decode_deltas(files: CaseFiles, file: FixtureFile) -> StdResult { + let bytes = files + .read_snappy(file) + .map_err(|e| format!("snappy decode {}: {e:#}", file.as_str()))?; + Deltas::deserialize(&bytes).map_err(|e| format!("ssz decode {}: {e}", file.as_str())) +} + +/// Compare a computed `Gwei` vector against an expected `u64` vector. +/// +/// Returns a diagnostic on the first mismatch, or on a length difference. +fn compare_field( + file: &str, + field: &str, + computed: &[Gwei], + expected: &List, +) -> Option { + if computed.len() != expected.len() { + return Some(format!( + "{file} {field} length mismatch: got {}, want {}", + computed.len(), + expected.len() + )); + } + for (index, (got, want)) in computed.iter().zip(expected.iter()).enumerate() { + if got.as_u64() != *want { + return Some(format!( + "{file} {field}[{index}] mismatch: got {}, want {want}", + got.as_u64() + )); + } + } + None +} + +/// Report a delta-computation failure as a case failure. +fn compute_failure(name: &str, detail: &str) -> Outcome { + let detail = format!("compute {name}: {detail}"); + trace_fail(format_args!("rewards {name}"), &detail); + Outcome::Fail(detail) +} diff --git a/reftests/src/adapters/sanity.rs b/tests/src/adapters/sanity.rs similarity index 97% rename from reftests/src/adapters/sanity.rs rename to tests/src/adapters/sanity.rs index 0d02f65..a2f5aac 100644 --- a/reftests/src/adapters/sanity.rs +++ b/tests/src/adapters/sanity.rs @@ -5,8 +5,8 @@ //! advances the pre-state by the number in `slots.yaml`. Both shapes finish //! with the common post-state comparison logic. -use moonglass::containers::{BeaconState, SignedBeaconBlock}; -use moonglass::primitives::Slot; +use moonglass_core::containers::{BeaconState, SignedBeaconBlock}; +use moonglass_core::primitives::Slot; use super::{ Adapter, CaseRunner, Outcome, StateTransition, SupportedHandler, run_state_case, @@ -132,7 +132,6 @@ impl SupportedHandler for RandomHandler { } /// Sanity, finality, and random runners share the apply-blocks-and-compare shape. -#[must_use] fn run_shared(case: &Case, shape: HandlerShape, subject: &str) -> Outcome { run_state_case(case, subject, |case, state| match shape { HandlerShape::Blocks => apply_blocks(case, state), @@ -179,7 +178,7 @@ fn apply_blocks(case: &Case, state: &mut BeaconState) -> StateTransition { return StateTransition::HarnessError(detail); } }; - if let Err(e) = state.apply_signed_block(&block) { + if let Err(e) = state.state_transition(&block) { trace_fail(format_args!("apply {stem}"), &e); return StateTransition::Applied(Err(e)); } diff --git a/tests/src/adapters/shuffling.rs b/tests/src/adapters/shuffling.rs new file mode 100644 index 0000000..b09e5bb --- /dev/null +++ b/tests/src/adapters/shuffling.rs @@ -0,0 +1,123 @@ +//! Adapter for `shuffling` reference-test fixtures. +//! +//! Each case provides a seed, an index count, and the full expected permutation +//! produced by the swap-or-not shuffle. The adapter shuffles every index in the +//! range and compares it to the expected mapping. A case passes only when every +//! shuffled index matches the expected position. The shuffle is preset aware +//! through the compile-time round count, so this one adapter validates each +//! preset's fixtures against the round count it was built with. + +use serde::Deserialize; + +use moonglass_core::primitives::Bytes32; +use moonglass_core::state_transition::compute_shuffled_index_checked; + +use super::{Adapter, CaseRunner, Outcome, SupportedHandler, trace_fail, trace_pass}; +use crate::fixtures::{CaseFiles, FixtureFile, decode_fixed_hex}; +use crate::inventory::{Case, Runner}; + +/// Mapping fixture naming the seed, count, and expected permutation. +const MAPPING: FixtureFile = FixtureFile::new("mapping.yaml"); + +/// Statically registered `shuffling` adapter. +pub(super) static ADAPTER: Adapter = Adapter::new(); + +/// Zero-sized runner implementation for the shuffling family. +pub(super) struct Shuffling; + +impl CaseRunner for Shuffling { + type Handler = ShufflingHandler; + + const RUNNER: Runner = Runner::Shuffling; + + fn run(case: &Case, handler: Self::Handler) -> Outcome { + run(case, handler) + } +} + +/// Upstream handler family inside the shuffling runner. +#[derive(Clone, Copy)] +pub(super) struct ShufflingHandler { + /// Upstream handler directory name. + name: &'static str, +} + +impl ShufflingHandler { + /// Upstream `core` handler directory name. + const CORE: &'static str = "core"; +} + +impl SupportedHandler for ShufflingHandler { + const ALL: &'static [Self] = &[Self { name: Self::CORE }]; + + fn as_str(self) -> &'static str { + self.name + } +} + +/// Parsed `mapping.yaml` fixture. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Mapping { + /// Shuffle seed in `0x`-prefixed hex. + seed: String, + /// Number of indices in the permutation. + count: u64, + /// Expected shuffled position for each input index. + #[serde(rename = "mapping")] + expected: Vec, +} + +fn run(case: &Case, _handler: ShufflingHandler) -> Outcome { + let files = CaseFiles::new(case); + let mapping: Mapping = match files.read_yaml(MAPPING) { + Ok(mapping) => { + trace_pass("shuffling mapping", "read mapping.yaml"); + mapping + } + Err(e) => { + let detail = format!("read mapping.yaml: {e:#}"); + trace_fail("shuffling mapping", &detail); + return Outcome::Fail(detail); + } + }; + + let seed: Bytes32 = match decode_fixed_hex(&mapping.seed) { + Ok(seed) => seed, + Err(e) => { + let detail = format!("decode seed: {e}"); + trace_fail("shuffling seed", &detail); + return Outcome::Fail(detail); + } + }; + + let count = mapping.expected.len() as u64; + if count != mapping.count { + let detail = format!( + "mapping length mismatch: got {}, want {}", + count, mapping.count + ); + trace_fail("shuffling mapping", &detail); + return Outcome::Fail(detail); + } + + for (index, &want) in mapping.expected.iter().enumerate() { + let index = index as u64; + let shuffled = match compute_shuffled_index_checked(index, count, seed) { + Ok(shuffled) => shuffled, + Err(e) => { + let detail = format!("compute_shuffled_index_checked({index}): {e}"); + trace_fail("shuffling compute", &detail); + return Outcome::Fail(detail); + } + }; + if shuffled != want { + let detail = format!("index {index} shuffled to {shuffled}, want {want}"); + trace_fail("shuffling compute", &detail); + return Outcome::Fail(detail); + } + } + + trace_pass("shuffling compute", format_args!("{count} indices match")); + Outcome::Pass +} diff --git a/tests/src/adapters/ssz_generic.rs b/tests/src/adapters/ssz_generic.rs new file mode 100644 index 0000000..4745790 --- /dev/null +++ b/tests/src/adapters/ssz_generic.rs @@ -0,0 +1,936 @@ +//! Adapter for `ssz_generic` reference-test fixtures. +//! +//! The generic suite exercises the in-house SSZ codec directly against the +//! consensus-spec vectors rather than against named beacon containers. Each +//! handler names a schema family (booleans, fixed-width integers, basic +//! vectors, bitfields, and a fixed set of test containers), and each case name +//! encodes the concrete schema parameters such as element width or capacity. +//! +//! A `valid` case must decode, re-encode to the exact input bytes, and produce +//! a hash-tree-root equal to `meta.yaml`. An `invalid` case must fail to decode. +//! Re-encoding plus root equality is the accepted gate for valid cases, so the +//! human-readable `value.yaml` is never parsed here. + +use std::result::Result as StdResult; + +use serde::Deserialize; +use thiserror::Error; + +use moonglass_core::primitives::{Root, Uint256}; +use moonglass_core::ssz::{ + Bitlist, Bitvector, ContainerDecoder, ContainerEncoder, Deserialize as SszDeserialize, + DeserializeError, FieldLayout, List, MerkleizationError, Merkleized, Node, + Serialize as SszSerialize, SerializeError, SimpleSerialize, SszSized, Vector, + container_is_variable_size, container_size_hint, field_layout, merkleize_roots, +}; + +use crate::adapters::{Adapter, CaseRunner, Outcome, SupportedHandler, trace_fail, trace_pass}; +use crate::error::FixtureError; +use crate::fixtures::{CaseFiles, FixtureFile, decode_fixed_hex, encode_hex}; +use crate::inventory::{Case, Runner}; + +/// Compressed SSZ encoding fixture shared by every generic case. +const SERIALIZED: FixtureFile = FixtureFile::new("serialized.ssz_snappy"); +/// Root-bearing metadata fixture present only for valid cases. +const META: FixtureFile = FixtureFile::new("meta.yaml"); +/// Upstream suite directory name marking expected-valid cases. +const VALID_SUITE: &str = "valid"; + +/// Statically registered `ssz_generic` adapter. +pub(super) static ADAPTER: Adapter = Adapter::new(); + +/// Zero-sized runner implementation for the generic SSZ family. +pub(super) struct SszGeneric; + +impl CaseRunner for SszGeneric { + type Handler = GenericHandler; + + const RUNNER: Runner = Runner::SszGeneric; + + fn run(case: &Case, handler: Self::Handler) -> Outcome { + run(case, handler) + } +} + +/// `ssz_generic` sidecar parsing result. +type Result = StdResult; + +/// Error returned while reading `ssz_generic` sidecar fixtures. +#[derive(Debug, Error)] +enum GenericError { + /// Reading or parsing a fixture file failed. + #[error(transparent)] + Fixture(#[from] FixtureError), + /// Hex decoding of the expected root failed. + #[error("decode root: {0}")] + Hex(String), +} + +/// Outcome of dispatching one generic case to a concrete Rust type. +/// +/// A schema parameter the in-house types do not instantiate (for example a +/// zero-length vector) yields [`Self::Unsupported`] so the caller can surface it +/// without claiming a pass or a failure. +enum CaseOutcome { + /// The case ran against a concrete type and produced an outcome. + Ran(Outcome), + /// The schema parameter has no in-house type to dispatch to. + Unsupported(String), +} + +/// Root parsed from a valid case's `meta.yaml`. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Meta { + /// Expected hash-tree-root in `0x`-prefixed hex. + root: String, +} + +/// One generic handler family and its case-name dispatcher. +#[derive(Clone, Copy)] +pub(super) struct GenericHandler { + /// Upstream handler directory name. + name: &'static str, + /// Dispatch a case in this handler given its parsed schema parameters. + dispatch: fn(case_name: &str, &[u8], Option, bool) -> CaseOutcome, +} + +impl GenericHandler { + /// Build a handler entry. + const fn new( + name: &'static str, + dispatch: fn(&str, &[u8], Option, bool) -> CaseOutcome, + ) -> Self { + Self { name, dispatch } + } + + /// Dispatch one case to its concrete type. + fn dispatch( + self, + case_name: &str, + bytes: &[u8], + meta_root: Option, + valid: bool, + ) -> CaseOutcome { + (self.dispatch)(case_name, bytes, meta_root, valid) + } +} + +impl SupportedHandler for GenericHandler { + const ALL: &'static [Self] = &[ + Self::new("boolean", dispatch_boolean), + Self::new("uints", dispatch_uints), + Self::new("basic_vector", dispatch_basic_vector), + Self::new("bitlist", dispatch_bitlist), + Self::new("bitvector", dispatch_bitvector), + Self::new("containers", dispatch_containers), + ]; + + fn as_str(self) -> &'static str { + self.name + } +} + +/// Whether a case is a progressive-container vector under the `containers` +/// handler. +/// +/// The upstream `containers` directory mixes the basic test containers with +/// progressive containers whose roots use a merkleization scheme the in-house +/// SSZ does not implement. Discovery skips these so the remaining basic-container +/// cases can run as a supported family. +pub(super) fn is_progressive_container_case(case: &Case) -> bool { + case.kind.runner == Runner::SszGeneric + && case.kind.handler.as_str() == "containers" + && case.id.starts_with("Progressive") +} + +fn run(case: &Case, handler: GenericHandler) -> Outcome { + let files = CaseFiles::new(case); + let valid = case.suite == VALID_SUITE; + let meta_root = if valid { + match read_meta_root(files) { + Ok(root) => { + trace_pass("ssz_generic meta", "read expected root"); + Some(root) + } + Err(e) => { + let detail = format!("read meta.yaml: {e:#}"); + trace_fail("ssz_generic meta", &detail); + return Outcome::Fail(detail); + } + } + } else { + None + }; + let bytes = match files.read_snappy(SERIALIZED) { + Ok(b) => { + trace_pass( + "ssz_generic serialized", + format_args!("decoded {} bytes", b.len()), + ); + b + } + Err(e) => { + let detail = format!("snappy decode: {e:#}"); + trace_fail("ssz_generic serialized", &detail); + return Outcome::Fail(detail); + } + }; + match handler.dispatch(&case.id, &bytes, meta_root, valid) { + CaseOutcome::Ran(outcome) => outcome, + CaseOutcome::Unsupported(detail) => { + trace_fail("ssz_generic dispatch", &detail); + Outcome::Fail(detail) + } + } +} + +fn read_meta_root(files: CaseFiles) -> Result { + let meta: Meta = files.read_yaml(META)?; + let bytes = decode_fixed_hex(&meta.root).map_err(|e| GenericError::Hex(e.to_string()))?; + Ok(Root(bytes)) +} + +/// Match a runtime length against a literal list, dispatching to a generic arm. +/// +/// Each literal expands `$arm!(literal)` so the concrete const-generic type is +/// chosen at compile time. A length outside the list is reported as unsupported +/// rather than failed, since the in-house types only instantiate the lengths the +/// upstream vectors exercise. +macro_rules! dispatch_len { + ($n:expr, $arm:ident, $($len:literal),+ $(,)?) => { + match $n { + $( $len => $arm!($len), )+ + other => CaseOutcome::Unsupported(format!("unsupported length {other}")), + } + }; +} + +/// Run one valid or invalid case against a concrete SSZ type. +/// +/// Valid cases must round-trip the bytes exactly and match the expected root. +/// Invalid cases must fail to decode. +fn run_case(bytes: &[u8], meta_root: Option, valid: bool) -> Outcome +where + T: SszDeserialize + SszSerialize + Merkleized, +{ + let decoded = T::deserialize(bytes); + if !valid { + return match decoded { + Ok(_) => { + let detail = "expected decode failure, value decoded".to_owned(); + trace_fail("ssz_generic invalid", &detail); + Outcome::Fail(detail) + } + Err(e) => { + trace_pass( + "ssz_generic invalid", + format_args!("rejected as expected: {e}"), + ); + Outcome::Pass + } + }; + } + let value = match decoded { + Ok(v) => { + trace_pass("ssz_generic decode", "decoded value"); + v + } + Err(e) => { + let detail = format!("ssz decode: {e}"); + trace_fail("ssz_generic decode", &detail); + return Outcome::Fail(detail); + } + }; + let mut reencoded = Vec::with_capacity(bytes.len()); + if let Err(e) = SszSerialize::serialize(&value, &mut reencoded) { + let detail = format!("ssz re-encode: {e}"); + trace_fail("ssz_generic re-encode", &detail); + return Outcome::Fail(detail); + } + if reencoded != bytes { + let detail = format!( + "ssz re-encode mismatch: got {} bytes, want {} bytes", + reencoded.len(), + bytes.len() + ); + trace_fail("ssz_generic re-encode", &detail); + return Outcome::Fail(detail); + } + trace_pass( + "ssz_generic re-encode", + format_args!("{} bytes", reencoded.len()), + ); + let node = match Merkleized::hash_tree_root(&value) { + Ok(r) => { + trace_pass("ssz_generic hash_tree_root", "computed root"); + r + } + Err(e) => { + let detail = format!("hash_tree_root: {e}"); + trace_fail("ssz_generic hash_tree_root", &detail); + return Outcome::Fail(detail); + } + }; + let Some(expected) = meta_root else { + let detail = "valid case missing expected root".to_owned(); + trace_fail("ssz_generic root", &detail); + return Outcome::Fail(detail); + }; + let got = Root::from(node); + if got == expected { + trace_pass("ssz_generic root", "root matches meta.yaml"); + Outcome::Pass + } else { + let detail = format!( + "root mismatch: got 0x{}, want 0x{}", + encode_hex(&got.0), + encode_hex(&expected.0) + ); + trace_fail("ssz_generic root", &detail); + Outcome::Fail(detail) + } +} + +/// Outcome for a zero-length vector or bitvector schema. +/// +/// SSZ forbids zero-length vectors and bitvectors, so the type itself is illegal +/// and there is no in-house type to instantiate. Every such case is therefore an +/// expected rejection: the case passes when it is an invalid-suite case and fails +/// if the upstream vectors ever mark one valid. +fn reject_illegal_zero_length(valid: bool) -> Outcome { + if valid { + let detail = "zero-length vector or bitvector marked valid".to_owned(); + trace_fail("ssz_generic invalid", &detail); + Outcome::Fail(detail) + } else { + trace_pass( + "ssz_generic invalid", + "zero-length type rejected as expected", + ); + Outcome::Pass + } +} + +/// Report a case whose schema could not be parsed from its name. +fn unparsed(case_name: &str) -> CaseOutcome { + CaseOutcome::Unsupported(format!( + "could not parse schema from case name '{case_name}'" + )) +} + +/// Dispatch a parsed-and-supported case to the generic worker. +fn ran(bytes: &[u8], meta_root: Option, valid: bool) -> CaseOutcome +where + T: SszDeserialize + SszSerialize + Merkleized, +{ + CaseOutcome::Ran(run_case::(bytes, meta_root, valid)) +} + +fn dispatch_boolean( + _case_name: &str, + bytes: &[u8], + meta_root: Option, + valid: bool, +) -> CaseOutcome { + ran::(bytes, meta_root, valid) +} + +fn dispatch_uints( + case_name: &str, + bytes: &[u8], + meta_root: Option, + valid: bool, +) -> CaseOutcome { + let Some(bits) = parse_field(case_name, "uint") else { + return unparsed(case_name); + }; + match bits { + 8 => ran::(bytes, meta_root, valid), + 16 => ran::(bytes, meta_root, valid), + 32 => ran::(bytes, meta_root, valid), + 64 => ran::(bytes, meta_root, valid), + 128 => ran::(bytes, meta_root, valid), + 256 => ran::(bytes, meta_root, valid), + other => CaseOutcome::Unsupported(format!("unsupported uint width {other}")), + } +} + +/// Parse a basic-vector case name of the form `vec___...`. +fn parse_vector_schema(case_name: &str) -> Option<(&str, usize)> { + let mut parts = case_name.split('_'); + (parts.next()? == "vec").then_some(())?; + let elem = parts.next()?; + let len = parts.next()?.parse::().ok()?; + Some((elem, len)) +} + +/// Dispatch a `basic_vector` case named `vec___...`. +fn dispatch_basic_vector( + case_name: &str, + bytes: &[u8], + meta_root: Option, + valid: bool, +) -> CaseOutcome { + let Some((elem, len)) = parse_vector_schema(case_name) else { + return unparsed(case_name); + }; + dispatch_vector_elem(elem, len, bytes, meta_root, valid) +} + +/// Match the element type for a basic vector, then dispatch on its length. +fn dispatch_vector_elem( + elem: &str, + len: usize, + bytes: &[u8], + meta_root: Option, + valid: bool, +) -> CaseOutcome { + if len == 0 { + return CaseOutcome::Ran(reject_illegal_zero_length(valid)); + } + macro_rules! vector_arm { + ($len:literal) => { + match elem { + "bool" => ran::>(bytes, meta_root, valid), + "uint8" => ran::>(bytes, meta_root, valid), + "uint16" => ran::>(bytes, meta_root, valid), + "uint32" => ran::>(bytes, meta_root, valid), + "uint64" => ran::>(bytes, meta_root, valid), + "uint128" => ran::>(bytes, meta_root, valid), + "uint256" => ran::>(bytes, meta_root, valid), + other => CaseOutcome::Unsupported(format!("unsupported vector element {other}")), + } + }; + } + dispatch_len!(len, vector_arm, 1, 2, 3, 4, 5, 8, 16, 31, 512, 513) +} + +/// Dispatch a `bitlist` case named `bitlist__...`. +fn dispatch_bitlist( + case_name: &str, + bytes: &[u8], + meta_root: Option, + valid: bool, +) -> CaseOutcome { + let Some(limit) = parse_field(case_name, "bitlist") else { + return unparsed(case_name); + }; + macro_rules! bitlist_arm { + ($n:literal) => { + ran::>(bytes, meta_root, valid) + }; + } + dispatch_len!( + limit, + bitlist_arm, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 15, + 16, + 17, + 31, + 32, + 33, + 511, + 512, + 513 + ) +} + +/// Dispatch a `bitvector` case named `bitvec__...`. +fn dispatch_bitvector( + case_name: &str, + bytes: &[u8], + meta_root: Option, + valid: bool, +) -> CaseOutcome { + let Some(len) = parse_field(case_name, "bitvec") else { + return unparsed(case_name); + }; + if len == 0 { + return CaseOutcome::Ran(reject_illegal_zero_length(valid)); + } + macro_rules! bitvector_arm { + ($n:literal) => { + ran::>(bytes, meta_root, valid) + }; + } + dispatch_len!( + len, + bitvector_arm, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 15, + 16, + 17, + 31, + 32, + 33, + 511, + 512, + 513 + ) +} + +/// Dispatch a `containers` case whose name starts with a struct type name. +fn dispatch_containers( + case_name: &str, + bytes: &[u8], + meta_root: Option, + valid: bool, +) -> CaseOutcome { + if case_name.starts_with("SingleFieldTestStruct") { + ran::(bytes, meta_root, valid) + } else if case_name.starts_with("SmallTestStruct") { + ran::(bytes, meta_root, valid) + } else if case_name.starts_with("FixedTestStruct") { + ran::(bytes, meta_root, valid) + } else if case_name.starts_with("VarTestStruct") { + ran::(bytes, meta_root, valid) + } else if case_name.starts_with("ComplexTestStruct") { + ran::(bytes, meta_root, valid) + } else if case_name.starts_with("BitsStruct") { + ran::(bytes, meta_root, valid) + } else { + CaseOutcome::Unsupported(format!("unsupported container type for case '{case_name}'")) + } +} + +/// Parse the numeric parameter from a case name of the form `__...`. +fn parse_field(case_name: &str, prefix: &str) -> Option { + case_name + .strip_prefix(prefix)? + .strip_prefix('_')? + .split('_') + .next()? + .parse::() + .ok() +} + +/// `ssz_generic` container with a single basic field. +#[derive(Debug, PartialEq, Eq)] +struct SingleFieldTestStruct { + /// Sole `uint8` field. + a: u8, +} + +/// `ssz_generic` container with two basic fields. +#[derive(Debug, PartialEq, Eq)] +struct SmallTestStruct { + /// First `uint16` field. + a: u16, + /// Second `uint16` field. + b: u16, +} + +/// `ssz_generic` fixed-size container with three basic fields. +#[derive(Debug, PartialEq, Eq)] +struct FixedTestStruct { + /// `uint8` field. + a: u8, + /// `uint64` field. + b: u64, + /// `uint32` field. + c: u32, +} + +/// `ssz_generic` variable-size container with one list field. +#[derive(Debug, PartialEq, Eq)] +struct VarTestStruct { + /// Leading `uint16` field. + a: u16, + /// Bounded `uint16` list field. + b: List, + /// Trailing `uint8` field. + c: u8, +} + +/// `ssz_generic` container mixing fixed and variable fields and nested structs. +#[derive(Debug, PartialEq, Eq)] +struct ComplexTestStruct { + /// Leading `uint16` field. + a: u16, + /// Bounded `uint16` list field. + b: List, + /// Fixed `uint8` field. + c: u8, + /// Bounded byte list field. + d: List, + /// Nested variable-size container. + e: VarTestStruct, + /// Fixed-length vector of fixed-size containers. + f: Vector, + /// Fixed-length vector of variable-size containers. + g: Vector, +} + +/// `ssz_generic` container exercising bitfield fields. +#[derive(Debug, PartialEq, Eq)] +struct BitsStruct { + /// Bounded bitlist field. + a: Bitlist<5>, + /// Fixed bitvector field. + b: Bitvector<2>, + /// Single-bit bitvector field. + c: Bitvector<1>, + /// Bounded bitlist field. + d: Bitlist<6>, + /// Byte-wide bitvector field. + e: Bitvector<8>, +} + +/// Field layout for [`SingleFieldTestStruct`]. +fn single_field_test_struct_layout() -> [FieldLayout; 1] { + [field_layout::()] +} + +impl SszSized for SingleFieldTestStruct { + fn is_variable_size() -> bool { + container_is_variable_size(&single_field_test_struct_layout()) + } + + fn size_hint() -> usize { + container_size_hint(&single_field_test_struct_layout()) + } +} + +impl SszSerialize for SingleFieldTestStruct { + fn serialize(&self, buffer: &mut Vec) -> StdResult { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.a)?; + encoder.finish(buffer) + } +} + +impl SszDeserialize for SingleFieldTestStruct { + fn deserialize(encoding: &[u8]) -> StdResult { + let mut decoder = ContainerDecoder::new(encoding, &single_field_test_struct_layout())?; + Ok(Self { + a: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SingleFieldTestStruct { + fn hash_tree_root(&self) -> StdResult { + Ok(merkleize_roots(&[Merkleized::hash_tree_root(&self.a)?])) + } +} + +impl SimpleSerialize for SingleFieldTestStruct { + fn is_composite_type() -> bool { + true + } +} + +/// Field layout for [`SmallTestStruct`]. +fn small_test_struct_layout() -> [FieldLayout; 2] { + [field_layout::(), field_layout::()] +} + +impl SszSized for SmallTestStruct { + fn is_variable_size() -> bool { + container_is_variable_size(&small_test_struct_layout()) + } + + fn size_hint() -> usize { + container_size_hint(&small_test_struct_layout()) + } +} + +impl SszSerialize for SmallTestStruct { + fn serialize(&self, buffer: &mut Vec) -> StdResult { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.a)?; + encoder.write_field(&self.b)?; + encoder.finish(buffer) + } +} + +impl SszDeserialize for SmallTestStruct { + fn deserialize(encoding: &[u8]) -> StdResult { + let mut decoder = ContainerDecoder::new(encoding, &small_test_struct_layout())?; + Ok(Self { + a: decoder.deserialize_next::()?, + b: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for SmallTestStruct { + fn hash_tree_root(&self) -> StdResult { + Ok(merkleize_roots(&[ + Merkleized::hash_tree_root(&self.a)?, + Merkleized::hash_tree_root(&self.b)?, + ])) + } +} + +impl SimpleSerialize for SmallTestStruct { + fn is_composite_type() -> bool { + true + } +} + +/// Field layout for [`FixedTestStruct`]. +fn fixed_test_struct_layout() -> [FieldLayout; 3] { + [ + field_layout::(), + field_layout::(), + field_layout::(), + ] +} + +impl SszSized for FixedTestStruct { + fn is_variable_size() -> bool { + container_is_variable_size(&fixed_test_struct_layout()) + } + + fn size_hint() -> usize { + container_size_hint(&fixed_test_struct_layout()) + } +} + +impl SszSerialize for FixedTestStruct { + fn serialize(&self, buffer: &mut Vec) -> StdResult { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.a)?; + encoder.write_field(&self.b)?; + encoder.write_field(&self.c)?; + encoder.finish(buffer) + } +} + +impl SszDeserialize for FixedTestStruct { + fn deserialize(encoding: &[u8]) -> StdResult { + let mut decoder = ContainerDecoder::new(encoding, &fixed_test_struct_layout())?; + Ok(Self { + a: decoder.deserialize_next::()?, + b: decoder.deserialize_next::()?, + c: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for FixedTestStruct { + fn hash_tree_root(&self) -> StdResult { + Ok(merkleize_roots(&[ + Merkleized::hash_tree_root(&self.a)?, + Merkleized::hash_tree_root(&self.b)?, + Merkleized::hash_tree_root(&self.c)?, + ])) + } +} + +impl SimpleSerialize for FixedTestStruct { + fn is_composite_type() -> bool { + true + } +} + +/// Field layout for [`VarTestStruct`]. +fn var_test_struct_layout() -> [FieldLayout; 3] { + [ + field_layout::(), + field_layout::>(), + field_layout::(), + ] +} + +impl SszSized for VarTestStruct { + fn is_variable_size() -> bool { + container_is_variable_size(&var_test_struct_layout()) + } + + fn size_hint() -> usize { + container_size_hint(&var_test_struct_layout()) + } +} + +impl SszSerialize for VarTestStruct { + fn serialize(&self, buffer: &mut Vec) -> StdResult { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.a)?; + encoder.write_field(&self.b)?; + encoder.write_field(&self.c)?; + encoder.finish(buffer) + } +} + +impl SszDeserialize for VarTestStruct { + fn deserialize(encoding: &[u8]) -> StdResult { + let mut decoder = ContainerDecoder::new(encoding, &var_test_struct_layout())?; + Ok(Self { + a: decoder.deserialize_next::()?, + b: decoder.deserialize_next::>()?, + c: decoder.deserialize_next::()?, + }) + } +} + +impl Merkleized for VarTestStruct { + fn hash_tree_root(&self) -> StdResult { + Ok(merkleize_roots(&[ + Merkleized::hash_tree_root(&self.a)?, + Merkleized::hash_tree_root(&self.b)?, + Merkleized::hash_tree_root(&self.c)?, + ])) + } +} + +impl SimpleSerialize for VarTestStruct { + fn is_composite_type() -> bool { + true + } +} + +/// Field layout for [`ComplexTestStruct`]. +fn complex_test_struct_layout() -> [FieldLayout; 7] { + [ + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::(), + field_layout::>(), + field_layout::>(), + ] +} + +impl SszSized for ComplexTestStruct { + fn is_variable_size() -> bool { + container_is_variable_size(&complex_test_struct_layout()) + } + + fn size_hint() -> usize { + container_size_hint(&complex_test_struct_layout()) + } +} + +impl SszSerialize for ComplexTestStruct { + fn serialize(&self, buffer: &mut Vec) -> StdResult { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.a)?; + encoder.write_field(&self.b)?; + encoder.write_field(&self.c)?; + encoder.write_field(&self.d)?; + encoder.write_field(&self.e)?; + encoder.write_field(&self.f)?; + encoder.write_field(&self.g)?; + encoder.finish(buffer) + } +} + +impl SszDeserialize for ComplexTestStruct { + fn deserialize(encoding: &[u8]) -> StdResult { + let mut decoder = ContainerDecoder::new(encoding, &complex_test_struct_layout())?; + Ok(Self { + a: decoder.deserialize_next::()?, + b: decoder.deserialize_next::>()?, + c: decoder.deserialize_next::()?, + d: decoder.deserialize_next::>()?, + e: decoder.deserialize_next::()?, + f: decoder.deserialize_next::>()?, + g: decoder.deserialize_next::>()?, + }) + } +} + +impl Merkleized for ComplexTestStruct { + fn hash_tree_root(&self) -> StdResult { + Ok(merkleize_roots(&[ + Merkleized::hash_tree_root(&self.a)?, + Merkleized::hash_tree_root(&self.b)?, + Merkleized::hash_tree_root(&self.c)?, + Merkleized::hash_tree_root(&self.d)?, + Merkleized::hash_tree_root(&self.e)?, + Merkleized::hash_tree_root(&self.f)?, + Merkleized::hash_tree_root(&self.g)?, + ])) + } +} + +impl SimpleSerialize for ComplexTestStruct { + fn is_composite_type() -> bool { + true + } +} + +/// Field layout for [`BitsStruct`]. +fn bits_struct_layout() -> [FieldLayout; 5] { + [ + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + field_layout::>(), + ] +} + +impl SszSized for BitsStruct { + fn is_variable_size() -> bool { + container_is_variable_size(&bits_struct_layout()) + } + + fn size_hint() -> usize { + container_size_hint(&bits_struct_layout()) + } +} + +impl SszSerialize for BitsStruct { + fn serialize(&self, buffer: &mut Vec) -> StdResult { + let mut encoder = ContainerEncoder::for_type::(); + encoder.write_field(&self.a)?; + encoder.write_field(&self.b)?; + encoder.write_field(&self.c)?; + encoder.write_field(&self.d)?; + encoder.write_field(&self.e)?; + encoder.finish(buffer) + } +} + +impl SszDeserialize for BitsStruct { + fn deserialize(encoding: &[u8]) -> StdResult { + let mut decoder = ContainerDecoder::new(encoding, &bits_struct_layout())?; + Ok(Self { + a: decoder.deserialize_next::>()?, + b: decoder.deserialize_next::>()?, + c: decoder.deserialize_next::>()?, + d: decoder.deserialize_next::>()?, + e: decoder.deserialize_next::>()?, + }) + } +} + +impl Merkleized for BitsStruct { + fn hash_tree_root(&self) -> StdResult { + Ok(merkleize_roots(&[ + Merkleized::hash_tree_root(&self.a)?, + Merkleized::hash_tree_root(&self.b)?, + Merkleized::hash_tree_root(&self.c)?, + Merkleized::hash_tree_root(&self.d)?, + Merkleized::hash_tree_root(&self.e)?, + ])) + } +} + +impl SimpleSerialize for BitsStruct { + fn is_composite_type() -> bool { + true + } +} diff --git a/reftests/src/adapters/ssz_static.rs b/tests/src/adapters/ssz_static.rs similarity index 74% rename from reftests/src/adapters/ssz_static.rs rename to tests/src/adapters/ssz_static.rs index f5c695b..1c0076a 100644 --- a/reftests/src/adapters/ssz_static.rs +++ b/tests/src/adapters/ssz_static.rs @@ -5,24 +5,30 @@ //! exactly and the computed root matches `roots.yaml`. use std::path::{Path, PathBuf}; +use std::result::Result as StdResult; use serde::Deserialize; use thiserror::Error; -use moonglass::containers::{ - Attestation, AttestationData, AttesterSlashing, BLSToExecutionChange, BeaconBlock, - BeaconBlockBody, BeaconBlockHeader, BeaconState, Builder, BuilderDepositRequest, +use moonglass_core::containers::{ + AggregateAndProof, Attestation, AttestationData, AttesterSlashing, BLSToExecutionChange, + BeaconBlock, BeaconBlockBody, BeaconBlockHeader, BeaconState, Builder, BuilderDepositRequest, BuilderExitRequest, BuilderPendingPayment, BuilderPendingWithdrawal, Checkpoint, - ConsolidationRequest, Deposit, DepositData, DepositRequest, Eth1Data, ExecutionPayload, + ConsolidationRequest, ContributionAndProof, DataColumnSidecar, DataColumnsByRootIdentifier, + Deposit, DepositData, DepositMessage, DepositRequest, Eth1Data, ExecutionPayload, ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionRequests, Fork, ForkData, - HistoricalSummary, IndexedAttestation, IndexedPayloadAttestation, PayloadAttestation, - PayloadAttestationData, PayloadAttestationMessage, PendingConsolidation, PendingDeposit, - PendingPartialWithdrawal, ProposerSlashing, SignedBLSToExecutionChange, SignedBeaconBlock, - SignedBeaconBlockHeader, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, - SignedVoluntaryExit, SigningData, SingleAttestation, SyncAggregate, SyncCommittee, Validator, - VoluntaryExit, Withdrawal, WithdrawalRequest, + HistoricalSummary, IndexedAttestation, IndexedPayloadAttestation, MatrixEntry, + PartialDataColumnGroupID, PartialDataColumnSidecar, PayloadAttestation, PayloadAttestationData, + PayloadAttestationMessage, PendingConsolidation, PendingDeposit, PendingPartialWithdrawal, + PowBlock, ProposerPreferences, ProposerSlashing, SignedAggregateAndProof, + SignedBLSToExecutionChange, SignedBeaconBlock, SignedBeaconBlockHeader, + SignedContributionAndProof, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, + SignedProposerPreferences, SignedVoluntaryExit, SigningData, SingleAttestation, SyncAggregate, + SyncAggregatorSelectionData, SyncCommittee, SyncCommitteeContribution, SyncCommitteeMessage, + Validator, VoluntaryExit, Withdrawal, WithdrawalRequest, }; -use moonglass::primitives::Root; +use moonglass_core::primitives::Root; +use moonglass_core::ssz::{Deserialize as SszDeserialize, Merkleized, Serialize as SszSerialize}; use crate::adapters::{Adapter, CaseRunner, Outcome, SupportedHandler, trace_fail, trace_pass}; use crate::error::{FixtureError, HexError}; @@ -47,7 +53,7 @@ impl CaseRunner for SszStatic { } /// `ssz_static` sidecar parsing result. -type Result = std::result::Result; +type Result = StdResult; /// Error returned while reading `ssz_static` sidecar fixtures. #[derive(Debug, Error)] @@ -87,6 +93,7 @@ impl SupportedHandler for StaticContainer { Self::new("Attestation", run_one::), Self::new("AttestationData", run_one::), Self::new("AttesterSlashing", run_one::), + Self::new("AggregateAndProof", run_one::), Self::new("BeaconBlock", run_one::), Self::new("BeaconBlockBody", run_one::), Self::new("BeaconBlockHeader", run_one::), @@ -102,8 +109,15 @@ impl SupportedHandler for StaticContainer { ), Self::new("Checkpoint", run_one::), Self::new("ConsolidationRequest", run_one::), + Self::new("ContributionAndProof", run_one::), + Self::new("DataColumnSidecar", run_one::), + Self::new( + "DataColumnsByRootIdentifier", + run_one::, + ), Self::new("Deposit", run_one::), Self::new("DepositData", run_one::), + Self::new("DepositMessage", run_one::), Self::new("DepositRequest", run_one::), Self::new("Eth1Data", run_one::), Self::new("ExecutionPayload", run_one::), @@ -121,6 +135,15 @@ impl SupportedHandler for StaticContainer { "IndexedPayloadAttestation", run_one::, ), + Self::new("MatrixEntry", run_one::), + Self::new( + "PartialDataColumnGroupID", + run_one::, + ), + Self::new( + "PartialDataColumnSidecar", + run_one::, + ), Self::new("PayloadAttestation", run_one::), Self::new("PayloadAttestationData", run_one::), Self::new( @@ -133,7 +156,13 @@ impl SupportedHandler for StaticContainer { "PendingPartialWithdrawal", run_one::, ), + Self::new("PowBlock", run_one::), + Self::new("ProposerPreferences", run_one::), Self::new("ProposerSlashing", run_one::), + Self::new( + "SignedAggregateAndProof", + run_one::, + ), Self::new("SignedBeaconBlock", run_one::), Self::new( "SignedBeaconBlockHeader", @@ -143,6 +172,10 @@ impl SupportedHandler for StaticContainer { "SignedBLSToExecutionChange", run_one::, ), + Self::new( + "SignedContributionAndProof", + run_one::, + ), Self::new( "SignedExecutionPayloadBid", run_one::, @@ -151,11 +184,24 @@ impl SupportedHandler for StaticContainer { "SignedExecutionPayloadEnvelope", run_one::, ), + Self::new( + "SignedProposerPreferences", + run_one::, + ), Self::new("SignedVoluntaryExit", run_one::), Self::new("SigningData", run_one::), Self::new("SingleAttestation", run_one::), Self::new("SyncAggregate", run_one::), + Self::new( + "SyncAggregatorSelectionData", + run_one::, + ), Self::new("SyncCommittee", run_one::), + Self::new( + "SyncCommitteeContribution", + run_one::, + ), + Self::new("SyncCommitteeMessage", run_one::), Self::new("Validator", run_one::), Self::new("VoluntaryExit", run_one::), Self::new("Withdrawal", run_one::), @@ -172,8 +218,6 @@ impl SupportedHandler for StaticContainer { struct Roots { root: String, } - -#[must_use] fn run(case: &Case, container: StaticContainer) -> Outcome { let files = CaseFiles::new(case); let roots_path = files.path(ROOTS); @@ -207,9 +251,9 @@ fn run(case: &Case, container: StaticContainer) -> Outcome { fn run_one(bytes: &[u8], expected: &[u8; 32], container: &'static str) -> Outcome where - T: ssz_rs::Deserialize + ssz_rs::Serialize + ssz_rs::Merkleized, + T: SszDeserialize + SszSerialize + Merkleized, { - let mut value = match T::deserialize(bytes) { + let value = match T::deserialize(bytes) { Ok(v) => { trace_pass(format_args!("ssz decode {container}"), "decoded value"); v @@ -221,7 +265,7 @@ where } }; let mut reencoded = Vec::with_capacity(bytes.len()); - if let Err(e) = ssz_rs::Serialize::serialize(&value, &mut reencoded) { + if let Err(e) = SszSerialize::serialize(&value, &mut reencoded) { let detail = format!("ssz re-encode: {e}"); trace_fail(format_args!("ssz re-encode {container}"), &detail); return Outcome::Fail(detail); @@ -239,7 +283,7 @@ where format_args!("ssz re-encode {container}"), format_args!("{} bytes", reencoded.len()), ); - let node = match ssz_rs::Merkleized::hash_tree_root(&mut value) { + let node = match Merkleized::hash_tree_root(&value) { Ok(r) => { trace_pass(format_args!("hash_tree_root {container}"), "computed root"); r diff --git a/reftests/src/adapters/trace_data.rs b/tests/src/adapters/trace_data.rs similarity index 98% rename from reftests/src/adapters/trace_data.rs rename to tests/src/adapters/trace_data.rs index 63412ab..626dcb3 100644 --- a/reftests/src/adapters/trace_data.rs +++ b/tests/src/adapters/trace_data.rs @@ -1,13 +1,13 @@ //! Compact trace descriptions for decoded consensus containers. -use moonglass::containers::{ +use moonglass_core::containers::{ Attestation, AttestationData, AttesterSlashing, BeaconBlock, BeaconBlockHeader, BuilderDepositRequest, BuilderExitRequest, ConsolidationRequest, DepositRequest, ExecutionRequests, PayloadAttestation, PayloadAttestationData, PayloadAttestationMessage, ProposerSlashing, SignedBLSToExecutionChange, SignedBeaconBlock, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, }; -use moonglass::primitives::{BLSPubkey, ExecutionAddress, Hash32, Root}; +use moonglass_core::primitives::{BLSPubkey, ExecutionAddress, Hash32, Root}; use crate::fixtures::encode_hex; diff --git a/reftests/src/bin/reftests-minimal.rs b/tests/src/bin/reftests-minimal.rs similarity index 87% rename from reftests/src/bin/reftests-minimal.rs rename to tests/src/bin/reftests-minimal.rs index 3f1ea0b..f9933bf 100644 --- a/reftests/src/bin/reftests-minimal.rs +++ b/tests/src/bin/reftests-minimal.rs @@ -3,7 +3,7 @@ use std::process::ExitCode; fn main() -> ExitCode { - match reftests::run_from_env() { + match tests::run_from_env() { Ok(()) => ExitCode::SUCCESS, Err(err) => { eprintln!("error: {err}"); diff --git a/reftests/src/bin/reftests.rs b/tests/src/bin/reftests.rs similarity index 87% rename from reftests/src/bin/reftests.rs rename to tests/src/bin/reftests.rs index f5e7338..58f5915 100644 --- a/reftests/src/bin/reftests.rs +++ b/tests/src/bin/reftests.rs @@ -3,7 +3,7 @@ use std::process::ExitCode; fn main() -> ExitCode { - match reftests::run_from_env() { + match tests::run_from_env() { Ok(()) => ExitCode::SUCCESS, Err(err) => { eprintln!("error: {err}"); diff --git a/reftests/src/error.rs b/tests/src/error.rs similarity index 96% rename from reftests/src/error.rs rename to tests/src/error.rs index a1da808..c354cba 100644 --- a/reftests/src/error.rs +++ b/tests/src/error.rs @@ -1,15 +1,22 @@ //! Public error boundary for the reftest harness. +use std::error::Error as StdError; use std::fmt; use std::io; use std::path::PathBuf; +use std::process::ExitStatus; +use std::result::Result as StdResult; +use std::str::Utf8Error; use std::time::SystemTimeError; +use moonglass_core::ssz::DeserializeError; use tar::EntryType; use thiserror::Error as ThisError; +use crate::inventory::Runner; + /// Reftest harness result. -pub type Result = std::result::Result; +pub type Result = StdResult; /// Error returned by the reftest harness entrypoint. #[derive(Debug)] @@ -23,11 +30,6 @@ impl Error { kind: Box::new(kind), } } - - #[cfg(test)] - pub(crate) fn kind(&self) -> &ErrorKind { - &self.kind - } } impl From for Error { @@ -42,8 +44,8 @@ impl fmt::Display for Error { } } -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { self.kind.source() } } @@ -148,7 +150,7 @@ pub(crate) enum ArchiveError { pub(crate) enum CoverageError { #[error("expected supported handler {runner}/{handler} is not wired")] ExpectedHandlerNotWired { - runner: crate::inventory::Runner, + runner: Runner, handler: &'static str, }, #[error("{lane} is missing expected {inventory}: {items}")] @@ -296,7 +298,7 @@ pub(crate) enum FixtureError { SszDecode { path: PathBuf, #[source] - source: ssz_rs::DeserializeError, + source: DeserializeError, }, #[error("parse {path:?}: {source}")] Yaml { @@ -415,7 +417,7 @@ pub(crate) enum WorkerError { }, #[error("case worker exited {status}\nworker stdout:\n{stdout}\nworker stderr:\n{stderr}")] WorkerExited { - status: std::process::ExitStatus, + status: ExitStatus, stdout: String, stderr: String, }, @@ -428,7 +430,7 @@ pub(crate) enum WorkerError { )] WorkerOutcomeUtf8 { #[source] - source: std::str::Utf8Error, + source: Utf8Error, stdout: String, stderr: String, }, diff --git a/reftests/src/fixtures.rs b/tests/src/fixtures.rs similarity index 100% rename from reftests/src/fixtures.rs rename to tests/src/fixtures.rs diff --git a/reftests/src/fixtures/case.rs b/tests/src/fixtures/case.rs similarity index 79% rename from reftests/src/fixtures/case.rs rename to tests/src/fixtures/case.rs index 333143d..2b724f4 100644 --- a/reftests/src/fixtures/case.rs +++ b/tests/src/fixtures/case.rs @@ -8,16 +8,19 @@ //! for a new upstream knob should be an explicit harness change. use std::fmt; -use std::io; +use std::fs; +use std::io::ErrorKind; use std::path::{Path, PathBuf}; +use std::result::Result as StdResult; +use moonglass_core::ssz; use serde::{Deserialize, Deserializer, de}; use crate::error::FixtureError; use crate::inventory::Case; /// Fixture loading result. -pub(crate) type Result = std::result::Result; +pub(crate) type Result = StdResult; /// Static fixture filename used by one or more adapters. #[derive(Clone, Copy, Debug)] @@ -39,7 +42,6 @@ impl FixtureFile { } /// Borrow the raw filename. - #[must_use] pub(crate) const fn as_str(self) -> &'static str { self.0 } @@ -51,7 +53,6 @@ pub(crate) struct FixtureStem(String); impl FixtureStem { /// Build an internally generated stem such as `blocks_0`. - #[must_use] pub(crate) fn indexed(prefix: &'static str, index: u64) -> Self { let stem = format!("{prefix}_{index}"); debug_assert!(valid_fixture_stem(&stem).is_ok()); @@ -59,7 +60,6 @@ impl FixtureStem { } /// Borrow the raw fixture stem. - #[must_use] pub(crate) fn as_str(&self) -> &str { &self.0 } @@ -72,7 +72,7 @@ impl fmt::Display for FixtureStem { } impl<'de> Deserialize<'de> for FixtureStem { - fn deserialize(deserializer: D) -> std::result::Result + fn deserialize(deserializer: D) -> StdResult where D: Deserializer<'de>, { @@ -82,7 +82,7 @@ impl<'de> Deserialize<'de> for FixtureStem { } } -fn valid_fixture_stem(stem: &str) -> std::result::Result<(), &'static str> { +fn valid_fixture_stem(stem: &str) -> StdResult<(), &'static str> { if stem.is_empty() { return Err("fixture stem must not be empty"); } @@ -103,13 +103,11 @@ pub(crate) struct CaseFiles<'a> { impl<'a> CaseFiles<'a> { /// Create a case-file view for `case`. - #[must_use] pub(crate) fn new(case: &'a Case) -> Self { Self { root: &case.root } } /// Return the absolute path for a static fixture file. - #[must_use] pub(crate) fn path(self, file: FixtureFile) -> PathBuf { self.root.join(file.as_str()) } @@ -140,7 +138,7 @@ impl<'a> CaseFiles<'a> { /// Decode a static SSZ-snappy fixture into a consensus container. pub(crate) fn decode_ssz_snappy(self, file: FixtureFile) -> Result where - T: ssz_rs::Deserialize, + T: ssz::Deserialize, { decode_ssz_snappy(&self.path(file)) } @@ -148,7 +146,7 @@ impl<'a> CaseFiles<'a> { /// Decode a generated `.ssz_snappy` fixture. pub(crate) fn decode_ssz_snappy_stem(self, stem: &FixtureStem) -> Result where - T: ssz_rs::Deserialize, + T: ssz::Deserialize, { decode_ssz_snappy(&self.root.join(format!("{stem}.ssz_snappy"))) } @@ -156,7 +154,7 @@ impl<'a> CaseFiles<'a> { /// Decode a static optional SSZ-snappy fixture. pub(crate) fn decode_optional_ssz_snappy(self, file: FixtureFile) -> Result> where - T: ssz_rs::Deserialize, + T: ssz::Deserialize, { decode_optional_ssz_snappy(&self.path(file)) } @@ -187,7 +185,7 @@ pub(crate) enum BlsSetting { } impl<'de> Deserialize<'de> for BlsSetting { - fn deserialize(deserializer: D) -> std::result::Result + fn deserialize(deserializer: D) -> StdResult where D: Deserializer<'de>, { @@ -231,9 +229,9 @@ pub(crate) fn read_snappy_file(path: &Path) -> Result> { /// Like [`read_snappy_file`], but treats a missing file as `None`. pub(crate) fn read_optional_snappy_file(path: &Path) -> Result>> { - let compressed = match std::fs::read(path) { + let compressed = match fs::read(path) { Ok(bytes) => bytes, - Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None), Err(e) => { return Err(FixtureError::Read { path: path.to_path_buf(), @@ -257,7 +255,7 @@ fn decode_snappy_bytes(compressed: &[u8], path: &Path) -> Result> { /// Decompress an SSZ-snappy file and decode it into a consensus container. pub(crate) fn decode_ssz_snappy(path: &Path) -> Result where - T: ssz_rs::Deserialize, + T: ssz::Deserialize, { let bytes = read_snappy_file(path)?; T::deserialize(&bytes).map_err(|source| FixtureError::SszDecode { @@ -269,7 +267,7 @@ where /// Decode an optional SSZ-snappy fixture, returning `None` only for `NotFound`. pub(crate) fn decode_optional_ssz_snappy(path: &Path) -> Result> where - T: ssz_rs::Deserialize, + T: ssz::Deserialize, { let Some(bytes) = read_optional_snappy_file(path)? else { return Ok(None); @@ -284,9 +282,9 @@ where /// Read `meta.yaml` from a case directory if present. pub(crate) fn read_meta(case_dir: &Path) -> Result { let path = case_dir.join(FixtureFile::META.as_str()); - let text = match std::fs::read_to_string(&path) { + let text = match fs::read_to_string(&path) { Ok(text) => text, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Meta::default()), + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(Meta::default()), Err(e) => { return Err(FixtureError::Read { path, source: e }); } @@ -360,14 +358,14 @@ fn check_manifest_field(path: &Path, field: &'static str, got: &str, want: &str) } fn read_bytes(path: &Path) -> Result> { - std::fs::read(path).map_err(|source| FixtureError::Read { + fs::read(path).map_err(|source| FixtureError::Read { path: path.to_path_buf(), source, }) } fn read_to_string(path: &Path) -> Result { - std::fs::read_to_string(path).map_err(|source| FixtureError::Read { + fs::read_to_string(path).map_err(|source| FixtureError::Read { path: path.to_path_buf(), source, }) @@ -382,65 +380,3 @@ where source, }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::testing::{ - BLS_DISABLED_ATTESTATION, SANITY_BLOCK_INVALID_OLD_STYLE_DEPOSIT_REJECTED, SLOTS_1, - }; - - #[test] - fn meta_parses_blocks_count_from_checked_in_vector() { - let m = - read_meta(&SANITY_BLOCK_INVALID_OLD_STYLE_DEPOSIT_REJECTED.root()).expect("read meta"); - assert_eq!(m.blocks_count, Some(1)); - assert_eq!(m.bls_setting, None); - } - - #[test] - fn meta_parses_bls_setting_from_checked_in_vector() { - let m = read_meta(&BLS_DISABLED_ATTESTATION.root()).expect("read meta"); - assert_eq!(m.bls_setting, Some(BlsSetting::Disabled)); - } - - #[test] - fn read_meta_returns_default_when_checked_in_case_has_no_meta() { - let m = read_meta(&SLOTS_1.root()).expect("read"); - assert!(m.blocks_count.is_none()); - assert!(m.bls_setting.is_none()); - } - - #[test] - fn manifest_matches_checked_in_case_identity() { - let case = SLOTS_1.to_case(); - - validate_case_manifest(&case).expect("manifest should match asset path"); - } - - #[test] - fn manifest_mismatch_reports_real_manifest_field() { - let mut case = SLOTS_1.to_case(); - case.id = "not_slots_1".to_owned(); - - let err = validate_case_manifest(&case).expect_err("manifest mismatch should fail"); - assert!(matches!( - err, - FixtureError::ManifestMismatch { - path, - field: "case", - got, - want - } if path.ends_with("manifest.yaml") && got == "slots_1" && want == "not_slots_1" - )); - } - - #[test] - fn fixture_stem_rejects_path_syntax() { - let err = serde_yaml::from_str::("'../block_0'").expect_err("reject path"); - assert!( - err.to_string() - .contains("fixture stem may contain only ASCII") - ); - } -} diff --git a/reftests/src/fixtures/compare.rs b/tests/src/fixtures/compare.rs similarity index 91% rename from reftests/src/fixtures/compare.rs rename to tests/src/fixtures/compare.rs index f723bdc..bf07771 100644 --- a/reftests/src/fixtures/compare.rs +++ b/tests/src/fixtures/compare.rs @@ -5,9 +5,9 @@ //! keeps diagnostics compact while pointing at the part of the transition that //! likely diverged. -use moonglass::containers::BeaconState; -use moonglass::primitives::Root; -use ssz_rs::MerkleizationError; +use moonglass_core::containers::BeaconState; +use moonglass_core::primitives::Root; +use moonglass_core::ssz::{MerkleizationError, Merkleized}; use crate::fixtures::encode_hex; @@ -35,7 +35,7 @@ pub(crate) fn diff( } fn state_root(state: &mut BeaconState) -> Result<[u8; 32], MerkleizationError> { - let node = ssz_rs::Merkleized::hash_tree_root(state)?; + let node = Merkleized::hash_tree_root(state)?; Ok(Root::from(node).0) } @@ -245,6 +245,12 @@ fn push_withdrawal_and_queue_fields( &got.pending_consolidations, &want.pending_consolidations, ); + push_if_changed( + fields, + "payload_expected_withdrawals", + &got.payload_expected_withdrawals, + &want.payload_expected_withdrawals, + ); } fn push_target_fork_fields(fields: &mut Vec<&'static str>, got: &BeaconState, want: &BeaconState) { @@ -285,6 +291,7 @@ fn push_target_fork_fields(fields: &mut Vec<&'static str>, got: &BeaconState, wa &got.latest_execution_payload_bid, &want.latest_execution_payload_bid, ); + push_if_changed(fields, "ptc_window", &got.ptc_window, &want.ptc_window); } fn push_if_changed( @@ -297,25 +304,3 @@ fn push_if_changed( fields.push(name); } } - -#[cfg(test)] -mod tests { - use moonglass::primitives::Slot; - - use super::*; - - #[test] - fn diff_lists_changed_top_level_fields() { - let mut got = BeaconState::default(); - let mut want = BeaconState { - slot: Slot::new(1), - ..BeaconState::default() - }; - - let detail = diff(&mut got, &mut want) - .expect("hash roots") - .expect("states differ"); - assert!(detail.contains("state root mismatch")); - assert!(detail.contains("differing fields: slot")); - } -} diff --git a/reftests/src/fixtures/hex.rs b/tests/src/fixtures/hex.rs similarity index 67% rename from reftests/src/fixtures/hex.rs rename to tests/src/fixtures/hex.rs index ff7b91a..79b6460 100644 --- a/reftests/src/fixtures/hex.rs +++ b/tests/src/fixtures/hex.rs @@ -4,10 +4,12 @@ //! strings. Keeping this local avoids adding a general-purpose dependency for //! the few conversions the harness needs. +use std::result::Result as StdResult; + use crate::error::HexError; /// Hex parsing result. -pub(crate) type Result = std::result::Result; +pub(crate) type Result = StdResult; /// Encode bytes as lowercase hex without a `0x` prefix. pub(crate) fn encode(bytes: &[u8]) -> String { @@ -63,35 +65,3 @@ fn nibble(b: u8) -> Result { _ => Err(HexError::InvalidByte(b)), } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn encode_round_values() { - assert_eq!(encode(&[]), ""); - assert_eq!(encode(&[0x00]), "00"); - assert_eq!(encode(&[0xff]), "ff"); - assert_eq!(encode(&[0x12, 0x34, 0xab, 0xcd]), "1234abcd"); - } - - #[test] - fn decode_prefixed_fixed_checks_length() { - assert_eq!( - decode_prefixed_fixed::<2>("0x1234").expect("decode"), - [0x12, 0x34] - ); - assert_eq!( - decode_prefixed_fixed::<3>("0x1234").expect_err("wrong length"), - HexError::WrongLength { - expected: 3, - actual: 2, - } - ); - assert_eq!( - decode_prefixed_fixed::<2>("1234").expect_err("missing prefix"), - HexError::MissingPrefix - ); - } -} diff --git a/reftests/src/harness.rs b/tests/src/harness.rs similarity index 100% rename from reftests/src/harness.rs rename to tests/src/harness.rs diff --git a/reftests/src/harness/app.rs b/tests/src/harness/app.rs similarity index 53% rename from reftests/src/harness/app.rs rename to tests/src/harness/app.rs index ac75adb..5f42528 100644 --- a/reftests/src/harness/app.rs +++ b/tests/src/harness/app.rs @@ -1,5 +1,7 @@ //! Top-level command flow for the reftest harness. +use std::env::args; +use std::io::stdout; use std::time::Instant; use super::{ @@ -7,6 +9,7 @@ use super::{ report::Summary, trace, worker, }; +use crate::adapters::Outcome; use crate::error::{ErrorKind, Result}; use crate::inventory::{self, CoverageLane}; use crate::vectors::{self, FixtureSet}; @@ -17,6 +20,10 @@ const ACTIVE_LANE: RunLane = RunLane::Mainnet; #[cfg(all(feature = "minimal", not(feature = "mainnet")))] const ACTIVE_LANE: RunLane = RunLane::Minimal; +// Only the variant matching the active preset feature is constructed, so the +// other one is unused under that build. Both arms stay in the source so the +// type and its methods cover either preset. +#[allow(dead_code)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum RunLane { Mainnet, @@ -45,16 +52,16 @@ impl RunLane { } } - const fn includes_general(self) -> bool { + const fn shuffling_coverage_lane(self) -> CoverageLane { match self { - Self::Mainnet => true, - Self::Minimal => false, + Self::Mainnet => CoverageLane::ShufflingMainnet, + Self::Minimal => CoverageLane::ShufflingMinimal, } } } pub(crate) fn run_from_env() -> Result<()> { - let args = std::env::args().skip(1).collect::>(); + let args = args().skip(1).collect::>(); if worker::internal_case_worker(&args) { return worker::run_case_worker().map_err(Into::into); } @@ -64,10 +71,7 @@ pub(crate) fn run_from_env() -> Result<()> { } fn run(args: &Args) -> Result<()> { - match ACTIVE_LANE { - RunLane::Mainnet => run_mainnet_lane(args), - RunLane::Minimal => run_minimal_only(args), - } + run_preset_lane(args, ACTIVE_LANE) } #[derive(Debug, Clone, Default, PartialEq, Eq)] @@ -153,19 +157,11 @@ impl Args { } fn matches_name(&self, name: &str) -> bool { - self.include_name(name) - } - - fn include_name(&self, name: &str) -> bool { self.name_patterns.is_empty() || self .name_patterns .iter() - .any(|pattern| Self::matches_pattern(name, pattern)) - } - - fn matches_pattern(name: &str, pattern: &str) -> bool { - name.contains(pattern) + .any(|pattern| name.contains(pattern)) } fn enforce_coverage(&self) -> bool { @@ -180,20 +176,11 @@ struct SelectedSkipped { cases: usize, } -fn run_mainnet_lane(args: &Args) -> Result<()> { - run_preset_lane(args, RunLane::Mainnet) -} - -fn run_minimal_only(args: &Args) -> Result<()> { - run_preset_lane(args, RunLane::Minimal) -} - fn run_preset_lane(args: &Args, lane: RunLane) -> Result<()> { let started = Instant::now(); let mut discovery = discover_preset(lane, args)?; - if lane.includes_general() { - append_general_discovery(&mut discovery, args)?; - } + append_general_discovery(&mut discovery, args)?; + append_shuffling_discovery(&mut discovery, lane, args)?; inventory::sort_discovery(&mut discovery); let cases = args.select_cases(&discovery.cases); @@ -234,23 +221,27 @@ fn append_general_discovery(discovery: &mut inventory::Discovery, args: &Args) - Ok(()) } +fn append_shuffling_discovery( + discovery: &mut inventory::Discovery, + lane: RunLane, + args: &Args, +) -> Result<()> { + let tag_dir = vectors::tag_dir(lane.fixture_set())?; + let shuffling = inventory::shuffling_discovery(&tag_dir, lane.preset())?; + validate_coverage(&shuffling, lane.shuffling_coverage_lane(), args)?; + discovery.cases.extend(shuffling.cases); + discovery.skipped.extend(shuffling.skipped); + Ok(()) +} + fn print_run_header(cases: usize, lane: RunLane) { let test_word = test_word(cases); - if lane.includes_general() { - println!( - "running {cases} {test_word} for consensus-specs {} ({}/{}, plus general)", - CONSENSUS_SPECS_TAG, - lane.preset(), - TARGET_FORK - ); - } else { - println!( - "running {cases} {test_word} for consensus-specs {} ({}/{})", - CONSENSUS_SPECS_TAG, - lane.preset(), - TARGET_FORK - ); - } + println!( + "running {cases} {test_word} for consensus-specs {} ({}/{}, plus general)", + CONSENSUS_SPECS_TAG, + lane.preset(), + TARGET_FORK + ); } const fn test_word(cases: usize) -> &'static str { @@ -324,13 +315,13 @@ fn run_cases( let run = worker::run_case(case, trace_mode); print_case_result(case, &run.outcome, color); if args.no_capture { - trace::write_no_capture_output(case, &run, color, std::io::stdout().lock()) + trace::write_no_capture_output(case, &run, color, stdout().lock()) .map_err(|source| ErrorKind::Report { source })?; } summary.record(case, &run.outcome); } summary - .write(started.elapsed(), color, std::io::stdout().lock()) + .write(started.elapsed(), color, stdout().lock()) .map_err(|source| ErrorKind::Report { source })?; if summary.has_failures() { return Err(ErrorKind::ReftestsFailed { @@ -341,7 +332,7 @@ fn run_cases( Ok(()) } -fn print_case_result(case: &inventory::Case, outcome: &crate::adapters::Outcome, color: Color) { +fn print_case_result(case: &inventory::Case, outcome: &Outcome, color: Color) { println!( "test {} ... {}", case.display_path(), @@ -349,197 +340,16 @@ fn print_case_result(case: &inventory::Case, outcome: &crate::adapters::Outcome, ); } -const fn outcome_status(outcome: &crate::adapters::Outcome) -> &'static str { +const fn outcome_status(outcome: &Outcome) -> &'static str { match outcome { - crate::adapters::Outcome::Pass => "ok", - crate::adapters::Outcome::Fail(_) => "FAILED", + Outcome::Pass => "ok", + Outcome::Fail(_) => "FAILED", } } -const fn outcome_style(outcome: &crate::adapters::Outcome) -> Style { +const fn outcome_style(outcome: &Outcome) -> Style { match outcome { - crate::adapters::Outcome::Pass => Style::Pass, - crate::adapters::Outcome::Fail(_) => Style::Fail, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::inventory::{Handler, RunnerName, SkipReason, SkippedFamily, SkippedFixture}; - use crate::testing::GET_HEAD_GENESIS; - - fn args(items: &[&str]) -> Vec { - items.iter().map(|s| (*s).to_owned()).collect() - } - - #[test] - fn parse_args_accepts_no_public_arguments() { - assert_eq!( - Args::parse(args(&[])).expect("parse empty args"), - Args::default() - ); - } - - #[test] - fn parse_args_accepts_nocapture() { - assert_eq!( - Args::parse(args(&["--nocapture"])).expect("parse --nocapture"), - Args { - no_capture: true, - ..Args::default() - } - ); - } - - #[test] - fn parse_args_accepts_libtest_name_patterns() { - assert_eq!( - Args::parse(args(&["fork_choice", "get_head"])).expect("parse filters"), - Args { - no_capture: false, - name_patterns: vec!["fork_choice".to_owned(), "get_head".to_owned()], - } - ); - } - - #[test] - fn parse_args_allows_dash_prefixed_filters_after_separator() { - assert_eq!( - Args::parse(args(&["--", "--dash-prefixed-case"])).expect("parse separator"), - Args { - name_patterns: vec!["--dash-prefixed-case".to_owned()], - ..Args::default() - } - ); - } - - #[test] - fn filters_match_case_display_names_like_libtest() { - let args = Args::parse(args(&["fork_choice"])).expect("parse"); - assert!(args.matches_name("minimal/gloas/fork_choice/get_head/pyspec_tests/genesis")); - assert!( - args.matches_name( - "minimal/gloas/fork_choice/get_head/pyspec_tests/filtered_block_tree" - ) - ); - assert!(!args.matches_name("minimal/gloas/sanity/blocks/pyspec_tests/empty")); - } - - #[test] - fn filters_can_match_case_ids() { - let args = Args::parse(args(&["eth_aggregate_pubkeys_empty_list"])).expect("parse filter"); - assert!(args.matches_name( - "general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_empty_list" - )); - assert!(!args.matches_name( - "general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_valid_0" - )); - } - - #[test] - fn coverage_is_enforced_only_for_unselected_runs() { - assert!(Args::parse(args(&[])).expect("parse").enforce_coverage()); - assert!( - Args::parse(args(&["--nocapture"])) - .expect("parse") - .enforce_coverage() - ); - assert!( - !Args::parse(args(&["get_head"])) - .expect("parse") - .enforce_coverage() - ); - } - - #[test] - fn filtered_out_count_includes_ignored_fixture_inventory() { - let case = GET_HEAD_GENESIS.to_case(); - let mut other = case.clone(); - other.id = "other".to_owned(); - let skipped = SkippedFixture::Family(SkippedFamily { - config: "general".to_owned(), - fork: "deneb".to_owned(), - runner: RunnerName::Unknown("kzg".to_owned()), - handler: Handler::new("verify_kzg_proof".to_owned()), - reason: SkipReason::UnsupportedRunner, - cases: 3, - case_paths: vec![ - "general/deneb/kzg/verify_kzg_proof/pyspec_tests/a".to_owned(), - "general/deneb/kzg/verify_kzg_proof/pyspec_tests/b".to_owned(), - "general/deneb/kzg/verify_kzg_proof/pyspec_tests/c".to_owned(), - ], - }); - let discovery = inventory::Discovery { - cases: vec![case, other], - skipped: vec![skipped], - }; - - let args = Args::parse(args(&["genesis"])).expect("parse"); - let cases = args.select_cases(&discovery.cases); - let skipped = args.select_skipped(&discovery.skipped); - - assert_eq!(cases.len(), 1); - assert!(skipped.is_empty()); - assert_eq!(filtered_out_count(&discovery, &cases, &skipped), 4); - } - - #[test] - fn name_selection_can_target_ignored_case_inside_family() { - let ignored_name = "general/deneb/kzg/verify_kzg_proof/pyspec_tests/valid"; - let skipped = SkippedFixture::Family(SkippedFamily { - config: "general".to_owned(), - fork: "deneb".to_owned(), - runner: RunnerName::Unknown("kzg".to_owned()), - handler: Handler::new("verify_kzg_proof".to_owned()), - reason: SkipReason::UnsupportedRunner, - cases: 2, - case_paths: vec![ - ignored_name.to_owned(), - "general/deneb/kzg/verify_kzg_proof/pyspec_tests/other".to_owned(), - ], - }); - let discovery = inventory::Discovery { - cases: Vec::new(), - skipped: vec![skipped], - }; - - let args = Args::parse(args(&["valid"])).expect("parse"); - let skipped = args.select_skipped(&discovery.skipped); - - assert_eq!( - skipped, - vec![SelectedSkipped { - path: ignored_name.to_owned(), - reason: "unsupported runner", - cases: 1, - }] - ); - assert_eq!(filtered_out_count(&discovery, &[], &skipped), 1); - } - - #[test] - fn empty_selection_is_rejected() { - let err = ensure_selection_not_empty(&[], &[], "minimal").expect_err("empty selection"); - - assert!(matches!( - err.kind(), - ErrorKind::NoSelectedCases { label } if label == "minimal" - )); - } - - #[test] - fn parse_args_rejects_unknown_flags() { - let err = Args::parse(args(&["--nope"])).expect_err("unknown flag"); - assert!(matches!( - err.kind(), - ErrorKind::UnexpectedArgument { arg } if arg == "--nope" - )); - - let err = Args::parse(args(&["--no-capture"])).expect_err("unknown flag"); - assert!(matches!( - err.kind(), - ErrorKind::UnexpectedArgument { arg } if arg == "--no-capture" - )); + Outcome::Pass => Style::Pass, + Outcome::Fail(_) => Style::Fail, } } diff --git a/reftests/src/harness/color.rs b/tests/src/harness/color.rs similarity index 59% rename from reftests/src/harness/color.rs rename to tests/src/harness/color.rs index 417303c..a3be78c 100644 --- a/reftests/src/harness/color.rs +++ b/tests/src/harness/color.rs @@ -4,23 +4,21 @@ use std::fmt; /// Always-on ANSI color state for one harness run. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct Color { - enabled: bool, -} +pub(crate) struct Color; impl Color { /// Create the harness color policy. pub(crate) const fn always() -> Self { - Self { enabled: true } + Self } - /// Wrap `text` in a style when color is enabled. + /// Wrap `text` in the given style. + /// + /// Takes `self` so the harness can thread one color policy through its + /// writers and paint via `policy.paint(..)`. + #[allow(clippy::unused_self)] pub(crate) const fn paint(self, style: Style, text: &str) -> Painted<'_> { - Painted { - enabled: self.enabled, - style, - text, - } + Painted { style, text } } } @@ -47,31 +45,12 @@ impl Style { /// Lazily formatted colored text. pub(crate) struct Painted<'a> { - enabled: bool, style: Style, text: &'a str, } impl fmt::Display for Painted<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.enabled { - write!(f, "\x1b[{}m{}\x1b[0m", self.style.code(), self.text) - } else { - f.write_str(self.text) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn paint_always_emits_ansi() { - let color = Color::always(); - assert_eq!( - color.paint(Style::Fail, "FAILED").to_string(), - "\x1b[31mFAILED\x1b[0m" - ); + write!(f, "\x1b[{}m{}\x1b[0m", self.style.code(), self.text) } } diff --git a/tests/src/harness/report.rs b/tests/src/harness/report.rs new file mode 100644 index 0000000..a9a8c09 --- /dev/null +++ b/tests/src/harness/report.rs @@ -0,0 +1,152 @@ +//! Cargo-style reporting for reference-test runs. +//! +//! Skipped fixtures are reported as ignored cases so unsupported or +//! metadata-excluded coverage remains visible without changing the exit code. + +use std::collections::BTreeMap; +use std::io; +use std::time::Duration; + +use super::color::{Color, Style}; +use crate::adapters::Outcome; +use crate::inventory::Case; + +#[derive(Debug, Clone)] +struct Failure { + case_path: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct IgnoredKey { + path: String, + reason: String, +} + +#[derive(Debug, Default)] +pub(crate) struct Summary { + totals: Totals, + ignored: BTreeMap, + filtered_out: usize, + failures: Vec, +} + +impl Summary { + /// Create an empty run summary. + pub(crate) fn new() -> Self { + Self::default() + } + + /// Record one executed case outcome. + pub(crate) fn record(&mut self, case: &Case, outcome: &Outcome) { + let case_path = case.display_path(); + match outcome { + Outcome::Pass => self.totals.pass += 1, + Outcome::Fail(_) => { + self.totals.fail += 1; + self.failures.push(Failure { case_path }); + } + } + } + + /// Record one ignored fixture report row. + pub(crate) fn record_ignored_fixture( + &mut self, + path: String, + reason: &'static str, + cases: usize, + ) { + let key = IgnoredKey { + path, + reason: reason.to_owned(), + }; + *self.ignored.entry(key).or_default() += cases; + } + + /// Record runnable cases excluded by libtest-style name-pattern selection. + pub(crate) fn record_filtered_out(&mut self, count: usize) { + self.filtered_out += count; + } + + /// Returns true if any case failed. + pub(crate) fn has_failures(&self) -> bool { + !self.failures.is_empty() + } + /// Return aggregate pass/fail counts, excluding skipped cases. + pub(crate) fn totals(&self) -> Totals { + self.totals + } + + /// Write failure details, final result line, and ignored inventory. + pub(crate) fn write( + &self, + elapsed: Duration, + color: Color, + mut out: impl io::Write, + ) -> io::Result<()> { + let totals = self.totals(); + if !self.failures.is_empty() { + writeln!(out)?; + writeln!(out, "{}:", color.paint(Style::Fail, "failures"))?; + for f in &self.failures { + writeln!(out, " {}", f.case_path)?; + } + } + + writeln!(out)?; + let status = if totals.fail == 0 { "ok" } else { "FAILED" }; + let status_style = if totals.fail == 0 { + Style::Pass + } else { + Style::Fail + }; + let ignored = self.ignored.values().sum::(); + writeln!( + out, + "test result: {status}. {p} passed; {f} failed; {ignored} ignored; 0 measured; {filtered} filtered out; finished in {elapsed}", + status = color.paint(status_style, status), + p = totals.pass, + f = totals.fail, + filtered = self.filtered_out, + elapsed = format_elapsed(elapsed), + )?; + + self.write_ignored(&mut out)?; + Ok(()) + } + + fn write_ignored(&self, mut out: impl io::Write) -> io::Result<()> { + if self.ignored.is_empty() { + return Ok(()); + } + + let mut max_key_len = 0; + let mut rows = Vec::with_capacity(self.ignored.len()); + for (ignored, cases) in &self.ignored { + let key = ignored.path.clone(); + max_key_len = max_key_len.max(key.len()); + rows.push((key, ignored.reason.as_str(), *cases)); + } + + writeln!(out)?; + writeln!(out, "ignored fixture cases/families:")?; + for (key, reason, cases) in &rows { + writeln!( + out, + "{key: String { + format!("{:.2}s", elapsed.as_secs_f64()) +} diff --git a/reftests/src/harness/trace.rs b/tests/src/harness/trace.rs similarity index 59% rename from reftests/src/harness/trace.rs rename to tests/src/harness/trace.rs index 2be5fe4..1cb2dc8 100644 --- a/reftests/src/harness/trace.rs +++ b/tests/src/harness/trace.rs @@ -1,5 +1,6 @@ //! `--nocapture` execution trace rendering. +use std::env::current_exe; use std::io; use super::{ @@ -47,7 +48,7 @@ fn write_failure_detail(mut out: impl io::Write, detail: &str, color: Color) -> } fn rerun_command(case_name: &str) -> String { - let binary = std::env::current_exe().ok().map_or_else( + let binary = current_exe().ok().map_or_else( || "reftests".to_owned(), |path| shell_quote(&path.display().to_string()), ); @@ -160,86 +161,3 @@ fn write_stream( } Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::adapters::TraceScope; - use crate::harness::color::Color; - use crate::testing::GET_HEAD_GENESIS; - - #[test] - fn no_capture_output_includes_trace_context_for_passing_cases() { - let case = GET_HEAD_GENESIS.to_case(); - let run = CaseRun { - outcome: Outcome::Pass, - stdout: b"stdout line\n".to_vec(), - stderr: b"stderr line\n".to_vec(), - elapsed_ms: Some(42), - trace: vec![TraceEvent { - scope: TraceScope::StepCheck { - index: 0, - tag: "Checks".to_owned(), - }, - label: "head.root".to_owned(), - status: TraceStatus::Pass, - detail: "root/slot matched".to_owned(), - }], - }; - - let mut output = Vec::new(); - write_no_capture_output(&case, &run, Color::always(), &mut output) - .expect("write trace output"); - let output = String::from_utf8(output).expect("trace output is utf-8"); - - assert!( - !output.contains( - "---- minimal/gloas/fork_choice/get_head/pyspec_tests/genesis trace ----" - ) - ); - assert!(output.contains("elapsed: 42 ms")); - assert!(!output.contains("pid 1234")); - assert!(!output.contains("rerun:")); - assert!(!output.contains("trace:")); - assert!( - output - .lines() - .any(|line| line.contains("ok") && line.contains("head.root")) - ); - assert!(output.contains("root/slot matched")); - assert!(output.contains("stdout line")); - assert!(output.contains("stderr line")); - } - - #[test] - fn shell_quote_keeps_simple_case_names_readable() { - let case = GET_HEAD_GENESIS.to_case(); - - assert_eq!(shell_quote(&case.display_path()), case.display_path()); - assert_eq!(shell_quote(""), "''"); - assert_eq!(shell_quote("case with ' quote"), "'case with '\\'' quote'"); - } - - #[test] - fn no_capture_output_includes_failure_detail() { - let case = GET_HEAD_GENESIS.to_case(); - let run = CaseRun { - outcome: Outcome::Fail("head mismatch\nexpected 0x01".to_owned()), - stdout: Vec::new(), - stderr: Vec::new(), - elapsed_ms: Some(7), - trace: Vec::new(), - }; - - let mut output = Vec::new(); - write_no_capture_output(&case, &run, Color::always(), &mut output) - .expect("write trace output"); - let output = String::from_utf8(output).expect("trace output is utf-8"); - - assert!(output.contains("failure")); - assert!(output.contains("head mismatch")); - assert!(output.contains("expected 0x01")); - assert!(output.contains("fixture:")); - assert!(output.contains("rerun:")); - } -} diff --git a/reftests/src/harness/worker.rs b/tests/src/harness/worker.rs similarity index 71% rename from reftests/src/harness/worker.rs rename to tests/src/harness/worker.rs index 2841dfb..2a28758 100644 --- a/reftests/src/harness/worker.rs +++ b/tests/src/harness/worker.rs @@ -1,9 +1,12 @@ //! Process-isolated execution for individual reference-test cases. use std::any::Any; -use std::io::{self, Write as _}; +use std::env; +use std::io::{self, Write as _, stdin, stdout}; use std::panic::{AssertUnwindSafe, catch_unwind}; -use std::process::{Output, Stdio}; +use std::process::{self, Child, Command, Output, Stdio}; +use std::result::Result as StdResult; +use std::str; use std::sync::atomic::{AtomicU64, Ordering}; use std::thread; use std::time::Instant; @@ -23,7 +26,7 @@ const MAX_OUTPUT_BYTES: usize = 1024 * 1024; const MAX_FAILURE_OUTPUT_BYTES: usize = 16 * 1024; static WORKER_TOKEN_COUNTER: AtomicU64 = AtomicU64::new(1); -pub(crate) type Result = std::result::Result; +pub(crate) type Result = StdResult; struct WorkerReport { response: protocol::Response, @@ -196,12 +199,12 @@ pub(crate) fn internal_case_worker(args: &[String]) -> bool { return false; }; flag == WORKER_ARG - && std::env::var(WORKER_ENV).is_ok_and(|value| value == WORKER_ENV_VALUE) - && std::env::var(WORKER_TOKEN_ENV).is_ok_and(|value| value == *token) + && env::var(WORKER_ENV).is_ok_and(|value| value == WORKER_ENV_VALUE) + && env::var(WORKER_TOKEN_ENV).is_ok_and(|value| value == *token) } pub(crate) fn run_case_worker() -> Result<()> { - let request: protocol::Request = serde_json::from_reader(std::io::stdin()) + let request: protocol::Request = serde_json::from_reader(stdin()) .map_err(|source| WorkerError::ReadWorkerCase { source })?; let (case, trace_mode) = request.into_parts(); @@ -211,7 +214,7 @@ pub(crate) fn run_case_worker() -> Result<()> { let trace = adapters::take_trace(); let response = protocol::Response::new(outcome, elapsed_ms, trace); - let stdout = std::io::stdout(); + let stdout = stdout(); let mut stdout = stdout.lock(); stdout .write_all(JSON_PREFIX.as_bytes()) @@ -229,18 +232,17 @@ fn run_case_process(case: &Case, trace: TraceMode) -> Result { // harness. The binary entrypoint checks worker mode before normal CLI // parsing, keeping the child protocol private to this crate. let token = worker_token(); - let mut child = std::process::Command::new( - std::env::current_exe().map_err(|source| WorkerError::CurrentExe { source })?, - ) - .arg(WORKER_ARG) - .arg(&token) - .env(WORKER_ENV, WORKER_ENV_VALUE) - .env(WORKER_TOKEN_ENV, &token) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|source| WorkerError::SpawnWorker { source })?; + let mut child = + Command::new(env::current_exe().map_err(|source| WorkerError::CurrentExe { source })?) + .arg(WORKER_ARG) + .arg(&token) + .env(WORKER_ENV, WORKER_ENV_VALUE) + .env(WORKER_TOKEN_ENV, &token) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|source| WorkerError::SpawnWorker { source })?; let send_result = (|| -> Result<()> { let mut stdin = child @@ -287,10 +289,10 @@ fn run_case_process(case: &Case, trace: TraceMode) -> Result { fn worker_token() -> String { let next = WORKER_TOKEN_COUNTER.fetch_add(1, Ordering::Relaxed); - format!("{}-{next}", std::process::id()) + format!("{}-{next}", process::id()) } -fn abort_worker(mut child: std::process::Child) { +fn abort_worker(mut child: Child) { let _ = child.kill(); let _ = child.wait(); } @@ -358,7 +360,7 @@ fn decode_worker_output(output: Output) -> Result { }); } }; - let json = std::str::from_utf8(&json).map_err(|source| WorkerError::WorkerOutcomeUtf8 { + let json = str::from_utf8(&json).map_err(|source| WorkerError::WorkerOutcomeUtf8 { source, stdout: stream_excerpt(&stdout_before_marker), stderr: stream_excerpt(&stderr), @@ -376,18 +378,7 @@ fn decode_worker_output(output: Output) -> Result { }) } -#[cfg(test)] -fn worker_json_payload(stdout: &[u8]) -> Option<(&[u8], &[u8])> { - let marker = JSON_PREFIX.as_bytes(); - stdout - .windows(marker.len()) - .rposition(|window| window == marker) - .map(|pos| (&stdout[..pos], &stdout[pos + marker.len()..])) -} - -fn split_worker_json_payload( - mut stdout: Vec, -) -> std::result::Result<(Vec, Vec), Vec> { +fn split_worker_json_payload(mut stdout: Vec) -> StdResult<(Vec, Vec), Vec> { let marker = JSON_PREFIX.as_bytes(); let Some(pos) = stdout .windows(marker.len()) @@ -445,113 +436,3 @@ fn panic_message(payload: &(dyn Any + Send)) -> String { "non-string panic payload".to_owned() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn internal_worker_requires_exact_private_arguments() { - assert!(!internal_case_worker(&[])); - assert!(!internal_case_worker(&[WORKER_ARG.to_owned()])); - assert!(!internal_case_worker(&[ - WORKER_ARG.to_owned(), - "token".to_owned(), - "extra".to_owned(), - ])); - assert!(!internal_case_worker(&[ - "--public-arg".to_owned(), - "token".to_owned(), - ])); - } - - #[test] - fn worker_output_decoder_ignores_stdout_before_marker() { - let mut stdout = vec![0xff, b'd', b'e', b'b', b'u', b'g']; - stdout.extend_from_slice(JSON_PREFIX.as_bytes()); - stdout.extend_from_slice(br#"{"outcome":{"status":"pass"},"elapsed_ms":7,"trace":[]}"#); - - let (before_marker, json) = worker_json_payload(&stdout).expect("payload"); - assert_eq!(before_marker, &[0xff, b'd', b'e', b'b', b'u', b'g']); - let response: protocol::Response = - serde_json::from_slice(json).expect("decode worker response"); - assert!(matches!(Outcome::from(response.outcome), Outcome::Pass)); - assert_eq!(response.elapsed_ms, 7); - } - - #[test] - fn worker_output_decoder_rejects_missing_marker() { - assert!(worker_json_payload(b"\"Pass\"\n").is_none()); - } - - #[test] - fn protocol_error_includes_captured_output() { - let output = Output { - status: successful_exit_status(), - stdout: b"captured stdout\n".to_vec(), - stderr: b"captured stderr\n".to_vec(), - }; - - let Err(err) = decode_worker_output(output) else { - panic!("missing marker should fail"); - }; - let detail = err.to_string(); - assert!(detail.contains("worker output did not include outcome marker")); - assert!(detail.contains("captured stdout")); - assert!(detail.contains("captured stderr")); - } - - fn successful_exit_status() -> std::process::ExitStatus { - std::process::Command::new(std::env::current_exe().expect("current test binary")) - .arg("--list") - .output() - .expect("list current test binary") - .status - } - - #[test] - fn limited_output_reader_drains_but_caps_storage() { - struct CountingReader { - cursor: std::io::Cursor>, - read_bytes: usize, - } - - impl std::io::Read for CountingReader { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let read = self.cursor.read(buf)?; - self.read_bytes += read; - Ok(read) - } - } - - let input_len = MAX_OUTPUT_BYTES + 17; - let mut reader = CountingReader { - cursor: std::io::Cursor::new(vec![b'x'; input_len]), - read_bytes: 0, - }; - let captured = read_limited_output(&mut reader).expect("read"); - - assert_eq!(captured.bytes.len(), MAX_OUTPUT_BYTES); - assert_eq!(reader.read_bytes, input_len); - assert!(captured.truncated); - } - - #[test] - fn case_run_keeps_failure_detail_and_captured_streams_separate() { - let report = WorkerReport { - response: protocol::Response::new( - Outcome::Fail("case failed".to_owned()), - 3, - Vec::new(), - ), - stdout: b"captured stdout\n".to_vec(), - stderr: b"captured stderr\n".to_vec(), - }; - - let run = case_run_from_worker_report(report); - - assert!(matches!(run.outcome, Outcome::Fail(ref detail) if detail == "case failed")); - assert_eq!(run.stdout, b"captured stdout\n"); - assert_eq!(run.stderr, b"captured stderr\n"); - } -} diff --git a/reftests/src/inventory.rs b/tests/src/inventory.rs similarity index 65% rename from reftests/src/inventory.rs rename to tests/src/inventory.rs index 88e5d8e..bfff1d8 100644 --- a/reftests/src/inventory.rs +++ b/tests/src/inventory.rs @@ -6,8 +6,5 @@ mod discover; pub(crate) use coverage::{CoverageLane, validate}; pub(crate) use discover::{ Case, CaseKind, Discovery, Handler, MetadataSkipReason, Runner, SkippedFixture, - general_discovery, preset_discovery, sort_discovery, + general_discovery, preset_discovery, shuffling_discovery, sort_discovery, }; - -#[cfg(test)] -pub(crate) use discover::{RunnerName, SkipReason, SkippedFamily}; diff --git a/reftests/src/inventory/coverage.rs b/tests/src/inventory/coverage.rs similarity index 71% rename from reftests/src/inventory/coverage.rs rename to tests/src/inventory/coverage.rs index c0cbe5c..659a455 100644 --- a/reftests/src/inventory/coverage.rs +++ b/tests/src/inventory/coverage.rs @@ -18,11 +18,18 @@ use crate::error::CoverageError; use crate::inventory::{Discovery, Handler, SkippedFixture}; use crate::{MAINNET_PRESET, MINIMAL_PRESET, TARGET_FORK}; +/// Fork directory that publishes the shuffling fixtures. +const SHUFFLING_FORK: &str = "phase0"; + #[derive(Clone, Copy, Debug)] pub(crate) enum CoverageLane { General, Mainnet, Minimal, + /// Shuffling fixtures under the mainnet preset. + ShufflingMainnet, + /// Shuffling fixtures under the minimal preset. + ShufflingMinimal, } impl CoverageLane { @@ -31,6 +38,8 @@ impl CoverageLane { Self::General => "general".to_owned(), Self::Mainnet => format!("{MAINNET_PRESET}/{TARGET_FORK}"), Self::Minimal => format!("{MINIMAL_PRESET}/{TARGET_FORK}"), + Self::ShufflingMainnet => format!("{MAINNET_PRESET}/{SHUFFLING_FORK}"), + Self::ShufflingMinimal => format!("{MINIMAL_PRESET}/{SHUFFLING_FORK}"), } } @@ -39,6 +48,8 @@ impl CoverageLane { Self::General => GENERAL_RUNNABLE, Self::Mainnet => MAINNET_RUNNABLE, Self::Minimal => MINIMAL_RUNNABLE, + Self::ShufflingMainnet => SHUFFLING_MAINNET_RUNNABLE, + Self::ShufflingMinimal => SHUFFLING_MINIMAL_RUNNABLE, } } @@ -47,6 +58,7 @@ impl CoverageLane { Self::General => GENERAL_SKIPPED, Self::Mainnet => MAINNET_SKIPPED, Self::Minimal => MINIMAL_SKIPPED, + Self::ShufflingMainnet | Self::ShufflingMinimal => SHUFFLING_SKIPPED, } } } @@ -71,6 +83,13 @@ impl ExpectedCount { cases, } } + + const fn shuffling(preset: &'static str, tail: &'static str, cases: usize) -> Self { + Self { + path: ExpectedPath::shuffling(preset, tail), + cases, + } + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -134,10 +153,20 @@ impl ExpectedPath { } } + const fn shuffling(preset: &'static str, tail: &'static str) -> Self { + Self { + namespace: ExpectedNamespace::Shuffling { preset }, + tail, + } + } + fn key(self) -> String { match self.namespace { ExpectedNamespace::General { fork } => format!("general/{fork}/{}", self.tail), ExpectedNamespace::Preset { preset } => format!("{preset}/{TARGET_FORK}/{}", self.tail), + ExpectedNamespace::Shuffling { preset } => { + format!("{preset}/{SHUFFLING_FORK}/{}", self.tail) + } } } } @@ -146,6 +175,7 @@ impl ExpectedPath { enum ExpectedNamespace { General { fork: &'static str }, Preset { preset: &'static str }, + Shuffling { preset: &'static str }, } pub(crate) fn validate(discovery: &Discovery, lane: CoverageLane) -> Result<(), CoverageError> { @@ -276,6 +306,21 @@ fn format_inventory_list(items: &[String]) -> String { const GENERAL_RUNNABLE: &[ExpectedCount] = &[ ExpectedCount::general("altair", "bls/eth_aggregate_pubkeys", 8), ExpectedCount::general("altair", "bls/eth_fast_aggregate_verify", 12), + ExpectedCount::general("fulu", "kzg/compute_cells", 11), + ExpectedCount::general("fulu", "kzg/compute_cells_and_kzg_proofs", 11), + ExpectedCount::general( + "fulu", + "kzg/compute_verify_cell_kzg_proof_batch_challenge", + 10, + ), + ExpectedCount::general("fulu", "kzg/recover_cells_and_kzg_proofs", 18), + ExpectedCount::general("fulu", "kzg/verify_cell_kzg_proof_batch", 32), + ExpectedCount::general("phase0", "ssz_generic/basic_vector", 1157), + ExpectedCount::general("phase0", "ssz_generic/bitlist", 494), + ExpectedCount::general("phase0", "ssz_generic/bitvector", 85), + ExpectedCount::general("phase0", "ssz_generic/boolean", 6), + ExpectedCount::general("phase0", "ssz_generic/containers", 407), + ExpectedCount::general("phase0", "ssz_generic/uints", 66), ]; const MAINNET_RUNNABLE: &[ExpectedCount] = &[ @@ -338,6 +383,12 @@ const MAINNET_RUNNABLE: &[ExpectedCount] = &[ ), ExpectedCount::preset(MAINNET_PRESET, "fork_choice/payload_data_availability", 3), ExpectedCount::preset(MAINNET_PRESET, "fork_choice/payload_timeliness", 3), + ExpectedCount::preset( + MAINNET_PRESET, + "networking/compute_columns_for_custody_group", + 5, + ), + ExpectedCount::preset(MAINNET_PRESET, "networking/get_custody_groups", 11), ExpectedCount::preset(MAINNET_PRESET, "operations/attestation", 56), ExpectedCount::preset(MAINNET_PRESET, "operations/attester_slashing", 30), ExpectedCount::preset(MAINNET_PRESET, "operations/block_header", 6), @@ -356,11 +407,13 @@ const MAINNET_RUNNABLE: &[ExpectedCount] = &[ ExpectedCount::preset(MAINNET_PRESET, "operations/withdrawal_request", 19), ExpectedCount::preset(MAINNET_PRESET, "operations/withdrawals", 84), ExpectedCount::preset(MAINNET_PRESET, "random/random", 16), + ExpectedCount::preset(MAINNET_PRESET, "rewards/basic", 11), ExpectedCount::preset(MAINNET_PRESET, "sanity/blocks", 62), ExpectedCount::preset(MAINNET_PRESET, "sanity/slots", 17), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/Attestation", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/AttestationData", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/AttesterSlashing", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/AggregateAndProof", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/BLSToExecutionChange", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/BeaconBlock", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/BeaconBlockBody", 5), @@ -373,8 +426,12 @@ const MAINNET_RUNNABLE: &[ExpectedCount] = &[ ExpectedCount::preset(MAINNET_PRESET, "ssz_static/BuilderPendingWithdrawal", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/Checkpoint", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/ConsolidationRequest", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/ContributionAndProof", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/DataColumnSidecar", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/DataColumnsByRootIdentifier", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/Deposit", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/DepositData", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/DepositMessage", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/DepositRequest", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/Eth1Data", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/ExecutionPayload", 5), @@ -386,27 +443,38 @@ const MAINNET_RUNNABLE: &[ExpectedCount] = &[ ExpectedCount::preset(MAINNET_PRESET, "ssz_static/HistoricalSummary", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/IndexedAttestation", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/IndexedPayloadAttestation", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/MatrixEntry", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/PartialDataColumnGroupID", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/PartialDataColumnSidecar", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/PayloadAttestation", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/PayloadAttestationData", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/PayloadAttestationMessage", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/PendingConsolidation", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/PendingDeposit", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/PendingPartialWithdrawal", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/PowBlock", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/ProposerPreferences", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/ProposerSlashing", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SignedAggregateAndProof", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SignedBLSToExecutionChange", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SignedBeaconBlock", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SignedBeaconBlockHeader", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SignedContributionAndProof", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SignedExecutionPayloadBid", 5), ExpectedCount::preset( MAINNET_PRESET, "ssz_static/SignedExecutionPayloadEnvelope", 5, ), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SignedProposerPreferences", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SignedVoluntaryExit", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SigningData", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SingleAttestation", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SyncAggregate", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SyncAggregatorSelectionData", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SyncCommittee", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SyncCommitteeContribution", 5), + ExpectedCount::preset(MAINNET_PRESET, "ssz_static/SyncCommitteeMessage", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/Validator", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/VoluntaryExit", 5), ExpectedCount::preset(MAINNET_PRESET, "ssz_static/Withdrawal", 5), @@ -417,135 +485,78 @@ const GENERAL_SKIPPED: &[ExpectedSkip] = &[ ExpectedSkip::general( "deneb", "kzg/blob_to_kzg_commitment", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 11, ), ExpectedSkip::general( "deneb", "kzg/compute_blob_kzg_proof", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 15, ), ExpectedSkip::general( "deneb", "kzg/compute_challenge", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 9, ), ExpectedSkip::general( "deneb", "kzg/compute_kzg_proof", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 52, ), ExpectedSkip::general( "deneb", "kzg/verify_blob_kzg_proof", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 29, ), ExpectedSkip::general( "deneb", "kzg/verify_blob_kzg_proof_batch", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 24, ), ExpectedSkip::general( "deneb", "kzg/verify_kzg_proof", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 122, ), - ExpectedSkip::general( - "fulu", - "kzg/compute_cells", - SkipReason::UnsupportedRunner, - 11, - ), - ExpectedSkip::general( - "fulu", - "kzg/compute_cells_and_kzg_proofs", - SkipReason::UnsupportedRunner, - 11, - ), - ExpectedSkip::general( - "fulu", - "kzg/compute_verify_cell_kzg_proof_batch_challenge", - SkipReason::UnsupportedRunner, - 10, - ), - ExpectedSkip::general( - "fulu", - "kzg/recover_cells_and_kzg_proofs", - SkipReason::UnsupportedRunner, - 18, - ), - ExpectedSkip::general( - "fulu", - "kzg/verify_cell_kzg_proof_batch", - SkipReason::UnsupportedRunner, - 32, - ), ExpectedSkip::general( "phase0", "ssz_generic/basic_progressive_list", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 887, ), - ExpectedSkip::general( - "phase0", - "ssz_generic/basic_vector", - SkipReason::UnsupportedRunner, - 1157, - ), - ExpectedSkip::general( - "phase0", - "ssz_generic/bitlist", - SkipReason::UnsupportedRunner, - 494, - ), - ExpectedSkip::general( - "phase0", - "ssz_generic/bitvector", - SkipReason::UnsupportedRunner, - 85, - ), - ExpectedSkip::general( - "phase0", - "ssz_generic/boolean", - SkipReason::UnsupportedRunner, - 6, - ), ExpectedSkip::general( "phase0", "ssz_generic/compatible_unions", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 521, ), + // The basic test containers run, but the same directory also carries + // progressive containers whose roots use an unimplemented merkleization + // scheme. Discovery records those as one aggregated skipped sub-family. ExpectedSkip::general( "phase0", "ssz_generic/containers", - SkipReason::UnsupportedRunner, - 701, + SkipReason::CaseMetadata(MetadataSkipReason::ProgressiveSszUnsupported), + 294, ), ExpectedSkip::general( "phase0", "ssz_generic/progressive_bitlist", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 703, ), ExpectedSkip::general( "phase0", "ssz_generic/progressive_containers", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 525, ), - ExpectedSkip::general( - "phase0", - "ssz_generic/uints", - SkipReason::UnsupportedRunner, - 66, - ), ]; const MAINNET_SKIPPED: &[ExpectedSkip] = &[ @@ -561,46 +572,34 @@ const MAINNET_SKIPPED: &[ExpectedSkip] = &[ SkipReason::UnsupportedRunner, 4, ), - ExpectedSkip::preset( - MAINNET_PRESET, - "networking/compute_columns_for_custody_group", - SkipReason::UnsupportedRunner, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "networking/get_custody_groups", - SkipReason::UnsupportedRunner, - 11, - ), ExpectedSkip::preset( MAINNET_PRESET, "networking/gossip_attester_slashing", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 12, ), ExpectedSkip::preset( MAINNET_PRESET, "networking/gossip_bls_to_execution_change", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 6, ), ExpectedSkip::preset( MAINNET_PRESET, "networking/gossip_proposer_slashing", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 9, ), ExpectedSkip::preset( MAINNET_PRESET, "networking/gossip_sync_committee_contribution_and_proof", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 15, ), ExpectedSkip::preset( MAINNET_PRESET, "networking/gossip_sync_committee_message", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 7, ), ExpectedSkip::preset( @@ -621,59 +620,23 @@ const MAINNET_SKIPPED: &[ExpectedSkip] = &[ SkipReason::CaseMetadata(MetadataSkipReason::BlsDisabledExecution), 1, ), - ExpectedSkip::preset( - MAINNET_PRESET, - "rewards/basic", - SkipReason::UnsupportedRunner, - 11, - ), ExpectedSkip::preset( MAINNET_PRESET, "rewards/inactivity_scores", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 12, ), ExpectedSkip::preset( MAINNET_PRESET, "rewards/leak", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 13, ), ExpectedSkip::preset( MAINNET_PRESET, "rewards/random", - SkipReason::UnsupportedRunner, - 10, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/AggregateAndProof", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/ContributionAndProof", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/DataColumnSidecar", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/DataColumnsByRootIdentifier", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/DepositMessage", SkipReason::UnsupportedHandler, - 5, + 10, ), ExpectedSkip::preset( MAINNET_PRESET, @@ -711,78 +674,12 @@ const MAINNET_SKIPPED: &[ExpectedSkip] = &[ SkipReason::UnsupportedHandler, 5, ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/MatrixEntry", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/PartialDataColumnGroupID", - SkipReason::UnsupportedHandler, - 5, - ), ExpectedSkip::preset( MAINNET_PRESET, "ssz_static/PartialDataColumnPartsMetadata", SkipReason::UnsupportedHandler, 5, ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/PartialDataColumnSidecar", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/PowBlock", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/ProposerPreferences", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/SignedAggregateAndProof", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/SignedContributionAndProof", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/SignedProposerPreferences", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/SyncAggregatorSelectionData", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/SyncCommitteeContribution", - SkipReason::UnsupportedHandler, - 5, - ), - ExpectedSkip::preset( - MAINNET_PRESET, - "ssz_static/SyncCommitteeMessage", - SkipReason::UnsupportedHandler, - 5, - ), ExpectedSkip::preset( MAINNET_PRESET, "transition/core", @@ -855,6 +752,12 @@ const MINIMAL_RUNNABLE: &[ExpectedCount] = &[ ExpectedCount::preset(MINIMAL_PRESET, "fork_choice/payload_timeliness", 3), ExpectedCount::preset(MINIMAL_PRESET, "fork_choice/reorg", 8), ExpectedCount::preset(MINIMAL_PRESET, "fork_choice/withholding", 2), + ExpectedCount::preset( + MINIMAL_PRESET, + "networking/compute_columns_for_custody_group", + 5, + ), + ExpectedCount::preset(MINIMAL_PRESET, "networking/get_custody_groups", 11), ExpectedCount::preset(MINIMAL_PRESET, "operations/attestation", 60), ExpectedCount::preset(MINIMAL_PRESET, "operations/attester_slashing", 30), ExpectedCount::preset(MINIMAL_PRESET, "operations/block_header", 6), @@ -873,11 +776,13 @@ const MINIMAL_RUNNABLE: &[ExpectedCount] = &[ ExpectedCount::preset(MINIMAL_PRESET, "operations/withdrawal_request", 29), ExpectedCount::preset(MINIMAL_PRESET, "operations/withdrawals", 85), ExpectedCount::preset(MINIMAL_PRESET, "random/random", 16), + ExpectedCount::preset(MINIMAL_PRESET, "rewards/basic", 11), ExpectedCount::preset(MINIMAL_PRESET, "sanity/blocks", 77), ExpectedCount::preset(MINIMAL_PRESET, "sanity/slots", 17), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/Attestation", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/AttestationData", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/AttesterSlashing", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/AggregateAndProof", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/BLSToExecutionChange", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/BeaconBlock", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/BeaconBlockBody", 123), @@ -890,8 +795,16 @@ const MINIMAL_RUNNABLE: &[ExpectedCount] = &[ ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/BuilderPendingWithdrawal", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/Checkpoint", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/ConsolidationRequest", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/ContributionAndProof", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/DataColumnSidecar", 123), + ExpectedCount::preset( + MINIMAL_PRESET, + "ssz_static/DataColumnsByRootIdentifier", + 123, + ), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/Deposit", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/DepositData", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/DepositMessage", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/DepositRequest", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/Eth1Data", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/ExecutionPayload", 123), @@ -903,27 +816,42 @@ const MINIMAL_RUNNABLE: &[ExpectedCount] = &[ ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/HistoricalSummary", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/IndexedAttestation", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/IndexedPayloadAttestation", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/MatrixEntry", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/PartialDataColumnGroupID", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/PartialDataColumnSidecar", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/PayloadAttestation", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/PayloadAttestationData", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/PayloadAttestationMessage", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/PendingConsolidation", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/PendingDeposit", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/PendingPartialWithdrawal", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/PowBlock", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/ProposerPreferences", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/ProposerSlashing", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SignedAggregateAndProof", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SignedBLSToExecutionChange", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SignedBeaconBlock", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SignedBeaconBlockHeader", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SignedContributionAndProof", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SignedExecutionPayloadBid", 123), ExpectedCount::preset( MINIMAL_PRESET, "ssz_static/SignedExecutionPayloadEnvelope", 123, ), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SignedProposerPreferences", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SignedVoluntaryExit", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SigningData", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SingleAttestation", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SyncAggregate", 123), + ExpectedCount::preset( + MINIMAL_PRESET, + "ssz_static/SyncAggregatorSelectionData", + 123, + ), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SyncCommittee", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SyncCommitteeContribution", 123), + ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/SyncCommitteeMessage", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/Validator", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/VoluntaryExit", 123), ExpectedCount::preset(MINIMAL_PRESET, "ssz_static/Withdrawal", 123), @@ -1021,46 +949,34 @@ const MINIMAL_SKIPPED: &[ExpectedSkip] = &[ SkipReason::UnsupportedRunner, 1, ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "networking/compute_columns_for_custody_group", - SkipReason::UnsupportedRunner, - 5, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "networking/get_custody_groups", - SkipReason::UnsupportedRunner, - 11, - ), ExpectedSkip::preset( MINIMAL_PRESET, "networking/gossip_attester_slashing", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 12, ), ExpectedSkip::preset( MINIMAL_PRESET, "networking/gossip_bls_to_execution_change", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 6, ), ExpectedSkip::preset( MINIMAL_PRESET, "networking/gossip_proposer_slashing", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 9, ), ExpectedSkip::preset( MINIMAL_PRESET, "networking/gossip_sync_committee_contribution_and_proof", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 14, ), ExpectedSkip::preset( MINIMAL_PRESET, "networking/gossip_sync_committee_message", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 7, ), ExpectedSkip::preset( @@ -1081,59 +997,23 @@ const MINIMAL_SKIPPED: &[ExpectedSkip] = &[ SkipReason::CaseMetadata(MetadataSkipReason::BlsDisabledExecution), 1, ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "rewards/basic", - SkipReason::UnsupportedRunner, - 11, - ), ExpectedSkip::preset( MINIMAL_PRESET, "rewards/inactivity_scores", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 12, ), ExpectedSkip::preset( MINIMAL_PRESET, "rewards/leak", - SkipReason::UnsupportedRunner, + SkipReason::UnsupportedHandler, 13, ), ExpectedSkip::preset( MINIMAL_PRESET, "rewards/random", - SkipReason::UnsupportedRunner, - 10, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/AggregateAndProof", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/ContributionAndProof", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/DataColumnSidecar", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/DataColumnsByRootIdentifier", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/DepositMessage", SkipReason::UnsupportedHandler, - 123, + 10, ), ExpectedSkip::preset( MINIMAL_PRESET, @@ -1171,78 +1051,12 @@ const MINIMAL_SKIPPED: &[ExpectedSkip] = &[ SkipReason::UnsupportedHandler, 123, ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/MatrixEntry", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/PartialDataColumnGroupID", - SkipReason::UnsupportedHandler, - 123, - ), ExpectedSkip::preset( MINIMAL_PRESET, "ssz_static/PartialDataColumnPartsMetadata", SkipReason::UnsupportedHandler, 123, ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/PartialDataColumnSidecar", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/PowBlock", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/ProposerPreferences", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/SignedAggregateAndProof", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/SignedContributionAndProof", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/SignedProposerPreferences", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/SyncAggregatorSelectionData", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/SyncCommitteeContribution", - SkipReason::UnsupportedHandler, - 123, - ), - ExpectedSkip::preset( - MINIMAL_PRESET, - "ssz_static/SyncCommitteeMessage", - SkipReason::UnsupportedHandler, - 123, - ), ExpectedSkip::preset( MINIMAL_PRESET, "transition/core", @@ -1251,192 +1065,16 @@ const MINIMAL_SKIPPED: &[ExpectedSkip] = &[ ), ]; -#[cfg(test)] -mod tests { - use super::*; - - fn split_inventory_path(path: &str) -> (Option<&'static str>, String, String) { - let segs: Vec<&str> = path.splitn(3, '/').collect(); - let config = segs[0]; - let fork = segs.get(1).copied().unwrap_or("").to_owned(); - let tail = segs.get(2).copied().unwrap_or("").to_owned(); - match config { - "general" => (None, fork, tail), - "minimal" => (Some("MINIMAL_PRESET"), fork, tail), - "mainnet" => (Some("MAINNET_PRESET"), fork, tail), - other => panic!("unexpected config {other}"), - } - } - - fn emit_skip_reason(reason: SkipReason) -> &'static str { - match reason { - SkipReason::UnsupportedRunner => "SkipReason::UnsupportedRunner", - SkipReason::UnsupportedHandler => "SkipReason::UnsupportedHandler", - SkipReason::CaseMetadata(MetadataSkipReason::BlsDisabledExecution) => { - "SkipReason::CaseMetadata(MetadataSkipReason::BlsDisabledExecution)" - } - } - } +const SHUFFLING_MAINNET_RUNNABLE: &[ExpectedCount] = &[ExpectedCount::shuffling( + MAINNET_PRESET, + "shuffling/core", + 300, +)]; - #[test] - #[ignore = "regenerates the pinned inventory source; run manually after a tag bump"] - fn emit_inventory_source() { - for (label, fixtures, preset) in [ - ( - "MINIMAL", - crate::vectors::FixtureSet::Minimal, - Some(MINIMAL_PRESET), - ), - ( - "MAINNET", - crate::vectors::FixtureSet::Mainnet, - Some(MAINNET_PRESET), - ), - ("GENERAL", crate::vectors::FixtureSet::General, None), - ] { - let dir = crate::vectors::tag_dir(fixtures).expect("tag dir"); - let discovery = match preset { - Some(preset) => { - crate::inventory::discover::preset_discovery(&dir, preset, TARGET_FORK) - .expect("preset discovery") - } - None => crate::inventory::discover::general_discovery(&dir).expect("general"), - }; +const SHUFFLING_MINIMAL_RUNNABLE: &[ExpectedCount] = &[ExpectedCount::shuffling( + MINIMAL_PRESET, + "shuffling/core", + 300, +)]; - let runnable = runnable_counts(&discovery); - println!("=== {label}_RUNNABLE ==="); - for (path, count) in &runnable { - let (preset, fork, tail) = split_inventory_path(path); - match preset { - Some(p) => { - println!(" ExpectedCount::preset({p}, \"{tail}\", {count}),"); - } - None => { - println!(" ExpectedCount::general(\"{fork}\", \"{tail}\", {count}),"); - } - } - } - - let mut skipped: BTreeMap<(String, SkipReason), usize> = BTreeMap::new(); - for fixture in &discovery.skipped { - *skipped - .entry((fixture.display_path(), fixture.reason())) - .or_default() += fixture.cases(); - } - println!("=== {label}_SKIPPED ==="); - for ((path, reason), count) in &skipped { - let (preset, fork, tail) = split_inventory_path(path); - let r = emit_skip_reason(*reason); - match preset { - Some(p) => { - println!(" ExpectedSkip::preset({p}, \"{tail}\", {r}, {count}),"); - } - None => { - println!( - " ExpectedSkip::general(\"{fork}\", \"{tail}\", {r}, {count})," - ); - } - } - } - println!( - "=== {label}_SUMS runnable={} skipped={} ===", - runnable.values().sum::(), - skipped.values().sum::() - ); - } - } - - fn preset_key(preset: &'static str, tail: &'static str) -> String { - ExpectedPath::preset(preset, tail).key() - } - - fn skipped_preset_key(preset: &'static str, tail: &'static str, reason: SkipReason) -> String { - skipped_key(&preset_key(preset, tail), reason.as_str()) - } - - #[test] - fn expected_supported_handlers_are_wired() { - validate_supported_handlers().expect("expected support contract"); - } - - #[test] - fn minimal_contract_pins_runnable_and_skipped_inventory() { - let runnable = expected_count_map(CoverageLane::Minimal.runnable_inventory()); - let skipped = expected_skip_map(CoverageLane::Minimal.skipped_inventory()); - - assert_eq!(runnable.values().sum::(), 6_832); - assert_eq!(skipped.values().sum::(), 3_183); - assert!(!runnable.keys().any(|family| family.contains("/bls/"))); - assert_eq!( - runnable.get(&preset_key( - MINIMAL_PRESET, - "fork_choice/deposit_with_reorg" - )), - Some(&1) - ); - assert_eq!( - runnable.get(&preset_key( - MINIMAL_PRESET, - "epoch_processing/sync_committee_updates" - )), - Some(&5) - ); - assert_eq!( - skipped.get(&skipped_preset_key( - MINIMAL_PRESET, - "operations/attestation/pyspec_tests/invalid_index", - SkipReason::CaseMetadata(MetadataSkipReason::BlsDisabledExecution) - )), - Some(&1) - ); - } - - #[test] - fn general_contract_pins_bls_and_unsupported_kzg() { - let runnable = expected_count_map(CoverageLane::General.runnable_inventory()); - let skipped = expected_skip_map(CoverageLane::General.skipped_inventory()); - - assert_eq!(runnable.values().sum::(), 20); - assert_eq!(skipped.values().sum::(), 5_489); - assert_eq!( - runnable.get("general/altair/bls/eth_aggregate_pubkeys"), - Some(&8) - ); - assert_eq!( - runnable.get("general/altair/bls/eth_fast_aggregate_verify"), - Some(&12) - ); - assert!(!runnable.contains_key("general/altair/bls/verify")); - assert_eq!( - skipped.get("general/deneb/kzg/verify_kzg_proof [unsupported runner]"), - Some(&122) - ); - } - - #[test] - fn mainnet_contract_pins_known_target_fork_gaps() { - let runnable = expected_count_map(CoverageLane::Mainnet.runnable_inventory()); - let skipped = expected_skip_map(CoverageLane::Mainnet.skipped_inventory()); - - assert_eq!(runnable.values().sum::(), 946); - assert_eq!(skipped.values().sum::(), 287); - assert!(!runnable.contains_key(&preset_key( - MAINNET_PRESET, - "fork_choice/deposit_with_reorg" - ))); - assert!(!runnable.contains_key(&preset_key(MAINNET_PRESET, "fork_choice/reorg"))); - assert!(!runnable.contains_key(&preset_key(MAINNET_PRESET, "fork_choice/withholding"))); - assert!(!runnable.contains_key(&preset_key( - MAINNET_PRESET, - "epoch_processing/sync_committee_updates" - ))); - assert_eq!( - skipped.get(&skipped_preset_key( - MAINNET_PRESET, - "ssz_static/AggregateAndProof", - SkipReason::UnsupportedHandler - )), - Some(&5) - ); - } -} +const SHUFFLING_SKIPPED: &[ExpectedSkip] = &[]; diff --git a/reftests/src/inventory/discover.rs b/tests/src/inventory/discover.rs similarity index 78% rename from reftests/src/inventory/discover.rs rename to tests/src/inventory/discover.rs index 34a3f8c..67a88c2 100644 --- a/reftests/src/inventory/discover.rs +++ b/tests/src/inventory/discover.rs @@ -6,8 +6,11 @@ //! handlers stay as upstream strings, and skipped fixtures keep a reason that //! is reported separately from executed cases. +use std::collections::BTreeMap; use std::fmt; +use std::fs; use std::path::{Path, PathBuf}; +use std::result::Result as StdResult; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; @@ -15,15 +18,20 @@ use crate::error::DiscoverError; use crate::{adapters, fixtures}; /// Discovery result. -pub(crate) type Result = std::result::Result; +pub(crate) type Result = StdResult; /// Upstream runner families the harness knows how to dispatch. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) enum Runner { SszStatic, + SszGeneric, Bls, + Kzg, + Networking, ForkChoice, Operations, + Rewards, + Shuffling, EpochProcessing, Sanity, Finality, @@ -34,9 +42,14 @@ impl Runner { /// All runner families understood by discovery. pub(crate) const ALL: &'static [Self] = &[ Self::SszStatic, + Self::SszGeneric, Self::Bls, + Self::Kzg, + Self::Networking, Self::ForkChoice, Self::Operations, + Self::Rewards, + Self::Shuffling, Self::EpochProcessing, Self::Sanity, Self::Finality, @@ -44,7 +57,6 @@ impl Runner { ]; /// Parse an upstream runner directory name. - #[must_use] pub(crate) fn parse(name: &str) -> Option { Self::ALL .iter() @@ -53,13 +65,17 @@ impl Runner { } /// Return the upstream directory name for this runner. - #[must_use] pub(crate) const fn as_str(self) -> &'static str { match self { Self::SszStatic => "ssz_static", + Self::SszGeneric => "ssz_generic", Self::Bls => "bls", + Self::Kzg => "kzg", + Self::Networking => "networking", Self::ForkChoice => "fork_choice", Self::Operations => "operations", + Self::Rewards => "rewards", + Self::Shuffling => "shuffling", Self::EpochProcessing => "epoch_processing", Self::Sanity => "sanity", Self::Finality => "finality", @@ -75,7 +91,7 @@ impl fmt::Display for Runner { } impl Serialize for Runner { - fn serialize(&self, serializer: S) -> std::result::Result + fn serialize(&self, serializer: S) -> StdResult where S: Serializer, { @@ -84,7 +100,7 @@ impl Serialize for Runner { } impl<'de> Deserialize<'de> for Runner { - fn deserialize(deserializer: D) -> std::result::Result + fn deserialize(deserializer: D) -> StdResult where D: Deserializer<'de>, { @@ -100,13 +116,11 @@ pub(crate) struct Handler(String); impl Handler { /// Wrap an upstream handler directory name. - #[must_use] pub(crate) fn new(name: String) -> Self { Self(name) } /// Borrow the upstream handler directory name. - #[must_use] pub(crate) fn as_str(&self) -> &str { &self.0 } @@ -140,7 +154,6 @@ pub(crate) enum SkipReason { impl SkipReason { /// Human-readable reason printed in the skipped-family report. - #[must_use] pub(crate) const fn as_str(self) -> &'static str { match self { Self::UnsupportedRunner => "unsupported runner", @@ -155,14 +168,16 @@ impl SkipReason { pub(crate) enum MetadataSkipReason { /// Case requires BLS-disabled execution. BlsDisabledExecution, + /// Case exercises progressive SSZ merkleization, which is not implemented. + ProgressiveSszUnsupported, } impl MetadataSkipReason { /// Human-readable reason printed in the skipped-family report. - #[must_use] pub(crate) const fn as_str(self) -> &'static str { match self { Self::BlsDisabledExecution => "bls_setting=2 requires BLS-disabled execution", + Self::ProgressiveSszUnsupported => "progressive SSZ merkleization is not implemented", } } } @@ -195,7 +210,6 @@ pub(crate) struct Case { impl Case { /// Slash-joined fixture family of the form `config/fork/runner/handler`. - #[must_use] pub(crate) fn family_path(&self) -> String { format!( "{}/{}/{}/{}", @@ -204,13 +218,11 @@ impl Case { } /// Slash-joined identifier of the form `config/fork/runner/handler/suite/case_id`. - #[must_use] pub(crate) fn display_path(&self) -> String { format!("{}/{}/{}", self.family_path(), self.suite, self.id) } /// Canonical fixture root for diagnostics, falling back to the stored path. - #[must_use] pub(crate) fn canonical_root_string(&self) -> String { self.root .canonicalize() @@ -231,7 +243,6 @@ pub(crate) enum SkippedFixture { impl SkippedFixture { /// Slash-joined identifier for reports. - #[must_use] pub(crate) fn display_path(&self) -> String { match self { Self::Family(family) => family.display_path(), @@ -240,7 +251,6 @@ impl SkippedFixture { } /// Reason printed in the skipped report. - #[must_use] pub(crate) const fn reason(&self) -> SkipReason { match self { Self::Family(family) => family.reason, @@ -249,7 +259,6 @@ impl SkippedFixture { } /// Number of fixture cases represented by this skipped item. - #[must_use] pub(crate) const fn cases(&self) -> usize { match self { Self::Family(family) => family.cases, @@ -288,7 +297,6 @@ pub(crate) enum RunnerName { impl RunnerName { /// Borrow the upstream runner directory name. - #[must_use] pub(crate) fn as_str(&self) -> &str { match self { Self::Known(runner) => runner.as_str(), @@ -305,7 +313,6 @@ impl fmt::Display for RunnerName { impl SkippedFamily { /// Slash-joined identifier of the form `config/fork/runner/handler`. - #[must_use] pub(crate) fn display_path(&self) -> String { format!( "{}/{}/{}/{}", @@ -314,7 +321,6 @@ impl SkippedFamily { } /// Full case display paths represented by this skipped family. - #[must_use] pub(crate) fn case_paths(&self) -> &[String] { &self.case_paths } @@ -346,6 +352,34 @@ pub(crate) fn preset_discovery(tag_dir: &Path, preset: &str, fork: &str) -> Resu Ok(discovery) } +/// Discover the `shuffling` fixtures, which live under a fixed base fork directory. +/// +/// Shuffling vectors are fork-independent, so the spec publishes them under a base +/// fork directory rather than the target fork, and the normal preset walk does not +/// reach them. This walks the shuffling runner directory directly and emits its +/// cases for the preset under test. A missing directory yields an empty discovery +/// so a preset without shuffling fixtures is not treated as an error. +pub(crate) fn shuffling_discovery(tag_dir: &Path, preset: &str) -> Result { + let mut discovery = Discovery::default(); + let runner_dir = tag_dir + .join("tests") + .join(preset) + .join("phase0") + .join(Runner::Shuffling.as_str()); + if !runner_dir.is_dir() { + return Ok(discovery); + } + walk_handler_tree( + &runner_dir, + preset, + "phase0", + Runner::Shuffling.as_str(), + &mut discovery, + )?; + sort_discovery(&mut discovery); + Ok(discovery) +} + /// Discover shared `general` fixtures. /// /// General fixtures are less uniform than preset fixtures: some first-level @@ -406,31 +440,20 @@ fn general_entry_layout(entry: &Path, name: &str) -> Result Ok(GeneralEntryLayout::Runner) } -struct GeneralManifest { - manifest: fixtures::CaseManifest, -} - -fn case_manifests_within(dir: &Path) -> Result> { +fn case_manifests_within(dir: &Path) -> Result> { case_dirs_with_manifest(dir)? .into_iter() - .map(|case_dir| { - let manifest = fixtures::read_case_manifest(&case_dir)?; - Ok(GeneralManifest { manifest }) - }) + .map(|case_dir| Ok(fixtures::read_case_manifest(&case_dir)?)) .collect() } fn layout_from_general_manifests( entry: &Path, name: &str, - manifests: &[GeneralManifest], + manifests: &[fixtures::CaseManifest], ) -> Result { - let all_fork = manifests - .iter() - .all(|located| located.manifest.fork == name); - let all_runner = manifests - .iter() - .all(|located| located.manifest.runner == name); + let all_fork = manifests.iter().all(|located| located.fork == name); + let all_runner = manifests.iter().all(|located| located.runner == name); match (all_fork, all_runner) { (true, false) => Ok(GeneralEntryLayout::Fork), @@ -506,6 +529,7 @@ fn walk_handler_tree( continue; } + let mut aggregated: BTreeMap> = BTreeMap::new(); for suite_entry in read_subdirs(&handler_entry)? { let suite = file_name(&suite_entry)?; for case_entry in read_subdirs(&suite_entry)? { @@ -522,7 +546,12 @@ fn walk_handler_tree( root: case_entry, }; fixtures::validate_case_manifest(&case)?; - if let Some(reason) = adapters::case_skip_reason(&case)? { + if let Some(reason) = adapters::is_aggregated_skip_case(&case) { + aggregated + .entry(reason) + .or_default() + .push(case.display_path()); + } else if let Some(reason) = adapters::case_skip_reason(&case)? { discovery.skipped.push(SkippedFixture::Case(SkippedCase { case, reason: SkipReason::CaseMetadata(reason), @@ -532,6 +561,20 @@ fn walk_handler_tree( } } } + for (reason, mut case_paths) in aggregated { + case_paths.sort(); + discovery + .skipped + .push(SkippedFixture::Family(SkippedFamily { + config: config.to_owned(), + fork: fork.to_owned(), + runner: RunnerName::Known(runner_kind), + handler: handler.clone(), + reason: SkipReason::CaseMetadata(reason), + cases: case_paths.len(), + case_paths, + })); + } } Ok(()) } @@ -609,7 +652,7 @@ pub(crate) fn sort_discovery(discovery: &mut Discovery) { fn read_subdirs(dir: &Path) -> Result> { let mut out = Vec::new(); - for entry in std::fs::read_dir(dir).map_err(|source| DiscoverError::ReadDir { + for entry in fs::read_dir(dir).map_err(|source| DiscoverError::ReadDir { path: dir.to_path_buf(), source, })? { @@ -645,9 +688,7 @@ fn case_dirs_with_manifest(dir: &Path) -> Result> { continue; } - let mut children = read_subdirs(¤t)?; - children.reverse(); - stack.extend(children); + stack.extend(read_subdirs(¤t)?); } out.sort(); Ok(out) @@ -664,118 +705,3 @@ fn file_name(path: &Path) -> Result { })?; Ok(name.to_owned()) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::testing::{ - AssetCase, BLS_AGGREGATE_EMPTY_LIST, BLS_AGGREGATE_VALID_0, BLS_DISABLED_ATTESTATION, - BLS_FAST_AGGREGATE_VERIFY_VALID_0, EPOCH_EFFECTIVE_BALANCE_HYSTERESIS, - GET_CUSTODY_GROUPS_1, GET_HEAD_GENESIS, KZG_VERIFY_PROOF_0_0, - SANITY_BLOCK_INVALID_OLD_STYLE_DEPOSIT_REJECTED, SLOTS_1, SSZ_STATIC_FORK_RANDOM_0, - VOLUNTARY_EXIT_BASIC, - }; - - #[test] - fn preset_discovery_uses_checked_in_vector_subset() { - let discovery = preset_discovery( - &crate::testing::vector_asset_root(), - "minimal", - crate::TARGET_FORK, - ) - .expect("discover"); - - assert_cases( - &discovery.cases, - &[ - EPOCH_EFFECTIVE_BALANCE_HYSTERESIS, - GET_HEAD_GENESIS, - VOLUNTARY_EXIT_BASIC, - SANITY_BLOCK_INVALID_OLD_STYLE_DEPOSIT_REJECTED, - SLOTS_1, - SSZ_STATIC_FORK_RANDOM_0, - ], - ); - assert_eq!(discovery.skipped.len(), 2); - assert_skipped_family( - &discovery.skipped[0], - GET_CUSTODY_GROUPS_1, - SkipReason::UnsupportedRunner, - 1, - ); - assert_skipped_case( - &discovery.skipped[1], - BLS_DISABLED_ATTESTATION, - SkipReason::CaseMetadata(MetadataSkipReason::BlsDisabledExecution), - ); - } - - #[test] - fn general_discovery_uses_checked_in_vector_subset() { - let discovery = - general_discovery(&crate::testing::vector_asset_root()).expect("discover general"); - - assert_cases( - &discovery.cases, - &[ - BLS_AGGREGATE_EMPTY_LIST, - BLS_AGGREGATE_VALID_0, - BLS_FAST_AGGREGATE_VERIFY_VALID_0, - ], - ); - assert_eq!(discovery.skipped.len(), 1); - assert_skipped_family( - &discovery.skipped[0], - KZG_VERIFY_PROOF_0_0, - SkipReason::UnsupportedRunner, - 1, - ); - } - - fn assert_cases(cases: &[Case], expected: &[AssetCase]) { - assert_eq!(cases.len(), expected.len(), "{cases:#?}"); - for (case, expected) in cases.iter().zip(expected) { - assert_case(case, *expected); - } - } - - fn assert_case(case: &Case, expected: AssetCase) { - assert_eq!(case.config, expected.preset); - assert_eq!(case.fork, expected.fork); - assert_eq!(case.kind.runner.as_str(), expected.runner); - assert_eq!(case.kind.handler.as_str(), expected.handler); - assert_eq!(case.suite, expected.suite); - assert_eq!(case.id, expected.case); - assert_eq!(case.root, expected.root()); - } - - fn assert_skipped_family( - skipped: &SkippedFixture, - expected: AssetCase, - reason: SkipReason, - cases: usize, - ) { - let SkippedFixture::Family(family) = skipped else { - panic!("expected skipped family, got {skipped:#?}"); - }; - assert_eq!(family.config, expected.preset); - assert_eq!(family.fork, expected.fork); - assert_eq!(family.runner.as_str(), expected.runner); - assert_eq!(family.handler.as_str(), expected.handler); - assert_eq!(family.reason, reason); - assert_eq!(family.cases, cases); - assert_eq!(family.case_paths.len(), cases); - assert_eq!(skipped.reason(), reason); - assert_eq!(skipped.cases(), cases); - } - - fn assert_skipped_case(skipped: &SkippedFixture, expected: AssetCase, reason: SkipReason) { - let SkippedFixture::Case(case) = skipped else { - panic!("expected skipped case, got {skipped:#?}"); - }; - assert_case(&case.case, expected); - assert_eq!(case.reason, reason); - assert_eq!(skipped.reason(), reason); - assert_eq!(skipped.cases(), 1); - } -} diff --git a/reftests/src/lib.rs b/tests/src/lib.rs similarity index 93% rename from reftests/src/lib.rs rename to tests/src/lib.rs index e71ee30..60f2ec3 100644 --- a/reftests/src/lib.rs +++ b/tests/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + //! Consensus-spec reference-test runner library for Moonglass. //! //! The binary crate is intentionally thin. This library owns discovery, @@ -10,9 +12,6 @@ mod harness; mod inventory; mod vectors; -#[cfg(test)] -mod testing; - pub use error::{Error, Result}; /// `ethereum/consensus-specs` release targeted by the runner. diff --git a/reftests/src/vectors.rs b/tests/src/vectors.rs similarity index 53% rename from reftests/src/vectors.rs rename to tests/src/vectors.rs index cddc088..99654bd 100644 --- a/reftests/src/vectors.rs +++ b/tests/src/vectors.rs @@ -26,26 +26,4 @@ impl FixtureSet { } } -#[cfg(test)] -pub(crate) use archive::sha256_hex; pub(crate) use release::tag_dir; - -#[cfg(test)] -mod tests { - use super::FixtureSet; - - #[test] - fn fixture_sets_use_distinct_cache_directories() { - assert_eq!(FixtureSet::General.cache_dir(), "general"); - assert_eq!(FixtureSet::Mainnet.cache_dir(), "mainnet"); - assert_eq!(FixtureSet::Minimal.cache_dir(), "minimal"); - assert_ne!( - FixtureSet::General.cache_dir(), - FixtureSet::Mainnet.cache_dir() - ); - assert_ne!( - FixtureSet::Mainnet.cache_dir(), - FixtureSet::Minimal.cache_dir() - ); - } -} diff --git a/reftests/src/vectors/archive.rs b/tests/src/vectors/archive.rs similarity index 61% rename from reftests/src/vectors/archive.rs rename to tests/src/vectors/archive.rs index 785f71e..40c9f3d 100644 --- a/reftests/src/vectors/archive.rs +++ b/tests/src/vectors/archive.rs @@ -5,9 +5,10 @@ //! accepted, unpacked size and entry count are capped, and callers separately //! reject symlinks in the published `tests/` tree before discovery. -use std::fs::File; +use std::fs::{self, File}; use std::io::{BufReader, Read}; use std::path::Path; +use std::result::Result as StdResult; use flate2::read::GzDecoder; use sha2::{Digest, Sha256}; @@ -17,7 +18,7 @@ use crate::error::ArchiveError; use crate::fixtures::encode_hex; /// Archive operation result. -type Result = std::result::Result; +type Result = StdResult; #[derive(Clone, Copy)] pub(super) struct Limits { @@ -29,7 +30,7 @@ pub(super) struct Limits { /// Extract a gzipped tar archive into `dest` within the provided limits. pub(super) fn extract_tar_gz(archive: &Path, dest: &Path, limits: Limits) -> Result<()> { - std::fs::create_dir_all(dest).map_err(|source| ArchiveError::Io { + fs::create_dir_all(dest).map_err(|source| ArchiveError::Io { action: "create directory", path: dest.to_path_buf(), source, @@ -101,7 +102,7 @@ pub(super) fn extract_tar_gz(archive: &Path, dest: &Path, limits: Limits) -> Res /// Return whether `path` or any descendant is a symlink. pub(super) fn contains_symlink(path: &Path) -> Result { - let metadata = std::fs::symlink_metadata(path).map_err(|source| ArchiveError::Io { + let metadata = fs::symlink_metadata(path).map_err(|source| ArchiveError::Io { action: "inspect", path: path.to_path_buf(), source, @@ -113,7 +114,7 @@ pub(super) fn contains_symlink(path: &Path) -> Result { return Ok(false); } - for entry in std::fs::read_dir(path).map_err(|source| ArchiveError::Io { + for entry in fs::read_dir(path).map_err(|source| ArchiveError::Io { action: "read directory", path: path.to_path_buf(), source, @@ -153,89 +154,3 @@ pub(crate) fn sha256_hex(path: &Path) -> Result { let digest = hasher.finalize(); Ok(encode_hex(&digest)) } - -#[cfg(test)] -mod tests { - use std::io::Cursor; - - use flate2::Compression; - use flate2::write::GzEncoder; - use tar::Header; - - use super::*; - - #[test] - fn sha256_hex_matches_known_vector() { - // sha256("abc") is one of the canonical NIST vectors. - let dir = crate::testing::TempDir::new("sha256"); - let tmp = dir.path().join("sha256"); - std::fs::write(&tmp, b"abc").expect("write"); - let got = sha256_hex(&tmp).expect("hash"); - assert_eq!( - got, - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" - ); - } - - #[test] - fn extract_tar_gz_rejects_entry_count_over_limit() { - let dir = crate::testing::TempDir::new("archive-entry-limit"); - let archive = dir.path().join("fixture.tar.gz"); - write_archive_with_file(&archive, "tests/case.txt", b"data"); - - let err = extract_tar_gz( - &archive, - &dir.path().join("out"), - Limits { - max_entries: 0, - max_unpacked_bytes: 1024, - }, - ) - .expect_err("entry limit should reject"); - - assert!(matches!( - err, - ArchiveError::TooManyEntries { max_entries: 0, .. } - )); - } - - #[test] - fn extract_tar_gz_rejects_unpacked_bytes_over_limit() { - let dir = crate::testing::TempDir::new("archive-byte-limit"); - let archive = dir.path().join("fixture.tar.gz"); - write_archive_with_file(&archive, "tests/case.txt", b"data"); - - let err = extract_tar_gz( - &archive, - &dir.path().join("out"), - Limits { - max_entries: 4, - max_unpacked_bytes: 3, - }, - ) - .expect_err("byte limit should reject"); - - assert!(matches!( - err, - ArchiveError::UnpackedBytesLimit { - max_unpacked_bytes: 3, - .. - } - )); - } - - fn write_archive_with_file(path: &Path, name: &str, contents: &[u8]) { - let file = File::create(path).expect("archive file"); - let encoder = GzEncoder::new(file, Compression::default()); - let mut archive = tar::Builder::new(encoder); - let mut header = Header::new_gnu(); - header.set_size(contents.len() as u64); - header.set_mode(0o644); - header.set_cksum(); - archive - .append_data(&mut header, name, Cursor::new(contents)) - .expect("append file"); - let encoder = archive.into_inner().expect("finish tar"); - encoder.finish().expect("finish gzip"); - } -} diff --git a/reftests/src/vectors/fetch.rs b/tests/src/vectors/fetch.rs similarity index 91% rename from reftests/src/vectors/fetch.rs rename to tests/src/vectors/fetch.rs index ce301f4..9f7397e 100644 --- a/reftests/src/vectors/fetch.rs +++ b/tests/src/vectors/fetch.rs @@ -5,9 +5,10 @@ //! sha256 digest, locally computed sha256, and extraction limits before the //! extracted `tests/` tree is published into the local cache. -use std::fs::File; -use std::io::{self, BufWriter, Read, Write}; +use std::fs::{self, File}; +use std::io::{BufWriter, ErrorKind, Read, Write}; use std::path::Path; +use std::result::Result as StdResult; use serde::Deserialize; @@ -18,7 +19,7 @@ use super::archive::{self, Limits}; use super::manifest::{Manifest, manifest_path}; /// Fetch operation result. -pub(super) type Result = std::result::Result; +pub(super) type Result = StdResult; #[derive(Clone, Copy)] pub(super) struct RequiredArchive { @@ -96,7 +97,7 @@ struct Asset { pub(super) fn fetch_release(tag: &str, tag_dir: &Path, fixtures: FixtureSet) -> Result { let release = resolve_release(tag)?; let archives_dir = tag_dir.join(".archives"); - std::fs::create_dir_all(&archives_dir).map_err(|source| FetchError::Io { + fs::create_dir_all(&archives_dir).map_err(|source| FetchError::Io { action: "create directory", path: archives_dir.clone(), source, @@ -118,7 +119,7 @@ pub(super) fn fetch_release(tag: &str, tag_dir: &Path, fixtures: FixtureSet) -> let staging_dir = tag_dir.join(".extracting-tests"); remove_path_if_exists(&staging_dir)?; - std::fs::create_dir_all(&staging_dir).map_err(|source| FetchError::Io { + fs::create_dir_all(&staging_dir).map_err(|source| FetchError::Io { action: "create directory", path: staging_dir.clone(), source, @@ -143,7 +144,7 @@ pub(super) fn fetch_release(tag: &str, tag_dir: &Path, fixtures: FixtureSet) -> let live_tests = tag_dir.join("tests"); remove_path_if_exists(&live_tests)?; - std::fs::rename(&staged_tests, &live_tests).map_err(|source| FetchError::Io { + fs::rename(&staged_tests, &live_tests).map_err(|source| FetchError::Io { action: "publish extracted tests", path: live_tests.clone(), source, @@ -184,7 +185,7 @@ fn validate_release_asset(asset: &Asset, required: &RequiredArchive) -> Result<( fn ensure_archive(required: &RequiredArchive, asset: &Asset, path: &Path) -> Result { let cached_metadata = match path.metadata() { Ok(metadata) => Some(metadata), - Err(e) if e.kind() == io::ErrorKind::NotFound => None, + Err(e) if e.kind() == ErrorKind::NotFound => None, Err(e) => { return Err(FetchError::Io { action: "inspect", @@ -199,7 +200,7 @@ fn ensure_archive(required: &RequiredArchive, asset: &Asset, path: &Path) -> Res if hash == required.sha256 && metadata.len() == required.compressed_bytes { return Ok(hash); } - std::fs::remove_file(path).map_err(|source| FetchError::Io { + fs::remove_file(path).map_err(|source| FetchError::Io { action: "remove stale archive", path: path.to_path_buf(), source, @@ -242,9 +243,9 @@ fn ensure_expected_digest(required: &RequiredArchive, asset: &Asset) -> Result<( } fn remove_path_if_exists(path: &Path) -> Result<()> { - let metadata = match std::fs::symlink_metadata(path) { + let metadata = match fs::symlink_metadata(path) { Ok(metadata) => metadata, - Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()), Err(err) => { return Err(FetchError::Io { action: "inspect", @@ -255,13 +256,13 @@ fn remove_path_if_exists(path: &Path) -> Result<()> { }; if metadata.is_dir() && !metadata.file_type().is_symlink() { - std::fs::remove_dir_all(path).map_err(|source| FetchError::Io { + fs::remove_dir_all(path).map_err(|source| FetchError::Io { action: "remove", path: path.to_path_buf(), source, })?; } else { - std::fs::remove_file(path).map_err(|source| FetchError::Io { + fs::remove_file(path).map_err(|source| FetchError::Io { action: "remove", path: path.to_path_buf(), source, @@ -288,9 +289,9 @@ fn api_get_json(url: &str) -> Result { fn download_to(url: &str, dest: &Path, expected_bytes: u64) -> Result<()> { let tmp = dest.with_extension("part"); - match std::fs::remove_file(&tmp) { + match fs::remove_file(&tmp) { Ok(()) => {} - Err(e) if e.kind() == io::ErrorKind::NotFound => {} + Err(e) if e.kind() == ErrorKind::NotFound => {} Err(e) => { return Err(FetchError::Io { action: "clean stale", @@ -327,7 +328,7 @@ fn download_to(url: &str, dest: &Path, expected_bytes: u64) -> Result<()> { } written += read as u64; if written > expected_bytes { - std::fs::remove_file(&tmp).ok(); + fs::remove_file(&tmp).ok(); return Err(FetchError::DownloadTooLarge { url: url.to_owned(), expected_bytes, @@ -348,14 +349,14 @@ fn download_to(url: &str, dest: &Path, expected_bytes: u64) -> Result<()> { })?; } if written != expected_bytes { - std::fs::remove_file(&tmp).ok(); + fs::remove_file(&tmp).ok(); return Err(FetchError::DownloadWrongSize { url: url.to_owned(), written, expected_bytes, }); } - std::fs::rename(&tmp, dest).map_err(|source| FetchError::Io { + fs::rename(&tmp, dest).map_err(|source| FetchError::Io { action: "rename download", path: dest.to_path_buf(), source, diff --git a/reftests/src/vectors/manifest.rs b/tests/src/vectors/manifest.rs similarity index 69% rename from reftests/src/vectors/manifest.rs rename to tests/src/vectors/manifest.rs index 9ca7681..cadeeaa 100644 --- a/reftests/src/vectors/manifest.rs +++ b/tests/src/vectors/manifest.rs @@ -5,9 +5,10 @@ //! and symlink absence before trusting the extracted `tests/` tree. use std::collections::BTreeMap; -use std::fs::File; +use std::fs::{self, File}; use std::io::{BufReader, BufWriter, Write as _}; use std::path::{Path, PathBuf}; +use std::result::Result as StdResult; use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; @@ -17,7 +18,7 @@ use crate::error::ManifestError; const MANIFEST_FILENAME: &str = "manifest.json"; /// Manifest operation result. -type Result = std::result::Result; +type Result = StdResult; /// Persisted record of a fetched release. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -60,7 +61,7 @@ impl Manifest { /// crash mid-write never leaves the live manifest truncated. pub(super) fn write(&self, path: &Path) -> Result<()> { if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|source| ManifestError::Io { + fs::create_dir_all(parent).map_err(|source| ManifestError::Io { action: "create directory", path: parent.to_path_buf(), source, @@ -87,7 +88,7 @@ impl Manifest { source, })?; } - std::fs::rename(&tmp, path).map_err(|source| ManifestError::Io { + fs::rename(&tmp, path).map_err(|source| ManifestError::Io { action: "rename", path: path.to_path_buf(), source, @@ -97,7 +98,6 @@ impl Manifest { } /// Return the manifest path inside an extracted release directory. -#[must_use] pub(super) fn manifest_path(tag_dir: &Path) -> PathBuf { tag_dir.join(MANIFEST_FILENAME) } @@ -109,43 +109,3 @@ fn now_epoch_seconds() -> Result { .as_secs(); Ok(secs) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn round_trip_through_disk() { - let dir = crate::testing::TempDir::new("manifest"); - let path = dir.path().join(MANIFEST_FILENAME); - - let mut manifest = Manifest::new("v9.9.9-test".to_owned()).expect("new"); - manifest - .archive_sha256s - .insert("general.tar.gz".to_owned(), "abc123".to_owned()); - - manifest.write(&path).expect("write"); - let read = Manifest::read(&path).expect("read"); - assert_eq!(read.tag, "v9.9.9-test"); - assert_eq!(read.archive_sha256s.len(), 1); - assert_eq!( - read.archive_sha256s - .get("general.tar.gz") - .map(String::as_str), - Some("abc123") - ); - } - - #[test] - fn write_is_atomic_via_tmp_then_rename() { - let dir = crate::testing::TempDir::new("manifest-atomic"); - let path = dir.path().join(MANIFEST_FILENAME); - - let manifest = Manifest::new("v1.2.3-atomic".to_owned()).expect("new"); - manifest.write(&path).expect("write"); - - // After a successful write the sibling `.tmp` is gone. - assert!(!path.with_extension("tmp").exists()); - assert!(path.exists()); - } -} diff --git a/tests/src/vectors/release.rs b/tests/src/vectors/release.rs new file mode 100644 index 0000000..09590ba --- /dev/null +++ b/tests/src/vectors/release.rs @@ -0,0 +1,123 @@ +//! Pinned consensus-spec release cache handling. + +use std::fs; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use std::result::Result as StdResult; + +use crate::error::{ManifestError, ReleaseError}; +use crate::{CONSENSUS_SPECS_TAG, MAINNET_PRESET, MINIMAL_PRESET, TARGET_FORK}; + +use super::FixtureSet; +use super::manifest::{Manifest, manifest_path}; +use super::{archive, fetch}; + +const VECTORS_DIR: &str = "tests/vectors"; + +pub(crate) type Result = StdResult; + +pub(crate) fn tag_dir(fixtures: FixtureSet) -> Result { + let dest = vectors_root(); + let dir = dest.join(CONSENSUS_SPECS_TAG).join(fixtures.cache_dir()); + if valid_cached_release(&dir, fixtures)? { + return Ok(dir); + } + + let manifest = fetch::fetch_release(CONSENSUS_SPECS_TAG, &dir, fixtures)?; + if !valid_cached_release(&dir, fixtures)? { + return Err(ReleaseError::FetchedReleaseIncomplete { tag: manifest.tag }); + } + + Ok(dir) +} + +fn vectors_root() -> PathBuf { + workspace_root().join(VECTORS_DIR) +} + +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("tests crate lives inside workspace root") + .to_path_buf() +} + +fn valid_cached_release(dir: &Path, fixtures: FixtureSet) -> Result { + let manifest_path = manifest_path(dir); + let manifest = match Manifest::read(&manifest_path) { + Ok(manifest) => manifest, + Err(ManifestError::Io { source, .. }) if source.kind() == ErrorKind::NotFound => { + return Ok(false); + } + Err(ManifestError::Json { .. }) => return Ok(false), + Err(err) => return Err(ReleaseError::from(err)), + }; + if manifest.tag != CONSENSUS_SPECS_TAG || manifest.archive_sha256s.is_empty() { + return Ok(false); + } + if tests_path_has_symlink(dir)? { + return Ok(false); + } + if !required_fixture_roots_exist(dir, fixtures) { + return Ok(false); + } + for archive_info in fetch::required_archives(fixtures) { + let Some(cached_hash) = manifest.archive_sha256s.get(archive_info.name) else { + return Ok(false); + }; + if cached_hash != archive_info.sha256 { + return Ok(false); + } + + let path = dir.join(".archives").join(archive_info.name); + if !path.is_file() { + return Ok(false); + } + if path + .metadata() + .map_err(|source| ReleaseError::PathIo { + action: "inspect", + path: path.clone(), + source, + })? + .len() + != archive_info.compressed_bytes + { + return Ok(false); + } + let got = archive::sha256_hex(&path)?; + if got != archive_info.sha256 { + return Ok(false); + } + } + Ok(true) +} + +fn required_fixture_roots_exist(dir: &Path, fixtures: FixtureSet) -> bool { + match fixtures { + FixtureSet::General => dir.join("tests").join("general").is_dir(), + FixtureSet::Mainnet => dir + .join("tests") + .join(MAINNET_PRESET) + .join(TARGET_FORK) + .is_dir(), + FixtureSet::Minimal => dir + .join("tests") + .join(MINIMAL_PRESET) + .join(TARGET_FORK) + .is_dir(), + } +} + +fn tests_path_has_symlink(dir: &Path) -> Result { + let tests = dir.join("tests"); + match fs::symlink_metadata(&tests) { + Ok(_) => archive::contains_symlink(&tests).map_err(ReleaseError::from), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(false), + Err(e) => Err(ReleaseError::PathIo { + action: "inspect", + path: tests, + source: e, + }), + } +} diff --git a/reftests/tests/assets/README.md b/tests/tests/assets/README.md similarity index 100% rename from reftests/tests/assets/README.md rename to tests/tests/assets/README.md diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_empty_list/data.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_empty_list/data.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_empty_list/data.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_empty_list/data.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_empty_list/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_empty_list/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_empty_list/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_empty_list/manifest.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_valid_0/data.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_valid_0/data.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_valid_0/data.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_valid_0/data.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_valid_0/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_valid_0/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_valid_0/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_valid_0/manifest.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_fast_aggregate_verify/bls/eth_fast_aggregate_verify_valid_0/data.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_fast_aggregate_verify/bls/eth_fast_aggregate_verify_valid_0/data.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_fast_aggregate_verify/bls/eth_fast_aggregate_verify_valid_0/data.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_fast_aggregate_verify/bls/eth_fast_aggregate_verify_valid_0/data.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_fast_aggregate_verify/bls/eth_fast_aggregate_verify_valid_0/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_fast_aggregate_verify/bls/eth_fast_aggregate_verify_valid_0/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_fast_aggregate_verify/bls/eth_fast_aggregate_verify_valid_0/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/altair/bls/eth_fast_aggregate_verify/bls/eth_fast_aggregate_verify_valid_0/manifest.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/deneb/kzg/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_0_0/data.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/deneb/kzg/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_0_0/data.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/deneb/kzg/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_0_0/data.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/deneb/kzg/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_0_0/data.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/deneb/kzg/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_0_0/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/deneb/kzg/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_0_0/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/deneb/kzg/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_0_0/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/deneb/kzg/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_0_0/manifest.yaml diff --git a/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/fulu/kzg/verify_cell_kzg_proof_batch/kzg-mainnet/verify_cell_kzg_proof_batch_case_valid_zero_cells/data.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/fulu/kzg/verify_cell_kzg_proof_batch/kzg-mainnet/verify_cell_kzg_proof_batch_case_valid_zero_cells/data.yaml new file mode 100644 index 0000000..afa782e --- /dev/null +++ b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/fulu/kzg/verify_cell_kzg_proof_batch/kzg-mainnet/verify_cell_kzg_proof_batch_case_valid_zero_cells/data.yaml @@ -0,0 +1,6 @@ +input: + commitments: [] + cell_indices: [] + cells: [] + proofs: [] +output: true diff --git a/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/fulu/kzg/verify_cell_kzg_proof_batch/kzg-mainnet/verify_cell_kzg_proof_batch_case_valid_zero_cells/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/fulu/kzg/verify_cell_kzg_proof_batch/kzg-mainnet/verify_cell_kzg_proof_batch_case_valid_zero_cells/manifest.yaml new file mode 100644 index 0000000..77938bd --- /dev/null +++ b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/general/fulu/kzg/verify_cell_kzg_proof_batch/kzg-mainnet/verify_cell_kzg_proof_batch_case_valid_zero_cells/manifest.yaml @@ -0,0 +1,6 @@ +preset: general +fork: fulu +runner: kzg +handler: verify_cell_kzg_proof_batch +suite: kzg-mainnet +case: verify_cell_kzg_proof_batch_case_valid_zero_cells diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/manifest.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/post.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/post.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/post.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/post.ssz_snappy diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/pre.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/pre.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/pre.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/pre.ssz_snappy diff --git a/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/anchor_block.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/anchor_block.ssz_snappy new file mode 100644 index 0000000..7cf4fa3 Binary files /dev/null and b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/anchor_block.ssz_snappy differ diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/pre.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/anchor_state.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/pre.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/anchor_state.ssz_snappy diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/manifest.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/meta.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/meta.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/meta.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/meta.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/steps.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/steps.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/steps.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis/steps.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/networking/get_custody_groups/pyspec_tests/get_custody_groups_1/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/networking/get_custody_groups/pyspec_tests/get_custody_groups_1/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/networking/get_custody_groups/pyspec_tests/get_custody_groups_1/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/networking/get_custody_groups/pyspec_tests/get_custody_groups_1/manifest.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/networking/get_custody_groups/pyspec_tests/get_custody_groups_1/meta.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/networking/get_custody_groups/pyspec_tests/get_custody_groups_1/meta.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/networking/get_custody_groups/pyspec_tests/get_custody_groups_1/meta.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/networking/get_custody_groups/pyspec_tests/get_custody_groups_1/meta.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/attestation.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/attestation.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/attestation.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/attestation.ssz_snappy diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/manifest.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/meta.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/meta.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/meta.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/meta.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/pre.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/pre.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/pre.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/attestation/pyspec_tests/invalid_index/pre.ssz_snappy diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/manifest.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/post.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/post.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/post.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/post.ssz_snappy diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/pre.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/pre.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/pre.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/pre.ssz_snappy diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/voluntary_exit.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/voluntary_exit.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/voluntary_exit.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/operations/voluntary_exit/pyspec_tests/basic/voluntary_exit.ssz_snappy diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/blocks_0.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/blocks_0.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/blocks_0.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/blocks_0.ssz_snappy diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/manifest.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/meta.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/meta.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/meta.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/meta.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/pre.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/pre.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/pre.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/blocks/pyspec_tests/invalid_old_style_deposit_rejected/pre.ssz_snappy diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/manifest.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/post.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/post.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/post.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/post.ssz_snappy diff --git a/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/pre.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/pre.ssz_snappy new file mode 100644 index 0000000..d13de8e Binary files /dev/null and b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/pre.ssz_snappy differ diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/slots.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/slots.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/slots.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/sanity/slots/pyspec_tests/slots_1/slots.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/manifest.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/manifest.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/manifest.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/manifest.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/roots.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/roots.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/roots.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/roots.yaml diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/serialized.ssz_snappy b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/serialized.ssz_snappy similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/serialized.ssz_snappy rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/serialized.ssz_snappy diff --git a/reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/value.yaml b/tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/value.yaml similarity index 100% rename from reftests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/value.yaml rename to tests/tests/assets/consensus-specs-v1.7.0-alpha.11/tests/minimal/gloas/ssz_static/Fork/ssz_random/case_0/value.yaml diff --git a/reftests/tests/assets/manifest.json b/tests/tests/assets/manifest.json similarity index 93% rename from reftests/tests/assets/manifest.json rename to tests/tests/assets/manifest.json index 0220213..33e1f9c 100644 --- a/reftests/tests/assets/manifest.json +++ b/tests/tests/assets/manifest.json @@ -9,6 +9,7 @@ "tests/general/altair/bls/eth_aggregate_pubkeys/bls/eth_aggregate_pubkeys_valid_0", "tests/general/altair/bls/eth_fast_aggregate_verify/bls/eth_fast_aggregate_verify_valid_0", "tests/general/deneb/kzg/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_0_0", + "tests/general/fulu/kzg/verify_cell_kzg_proof_batch/kzg-mainnet/verify_cell_kzg_proof_batch_case_valid_zero_cells", "tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis", "tests/minimal/gloas/fork_choice/get_head/pyspec_tests/genesis", "tests/minimal/gloas/networking/get_custody_groups/pyspec_tests/get_custody_groups_1", @@ -27,6 +28,8 @@ "tests/general/altair/bls/eth_fast_aggregate_verify/bls/eth_fast_aggregate_verify_valid_0/manifest.yaml": "5c282fba6d00569f88bfdc88bc68d371765fab413add09ac589441886045a7a3", "tests/general/deneb/kzg/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_0_0/data.yaml": "205a35f8bd0fe266db8d3b3fa0744e8025a07ffc0f69946afec4f2be38cd6051", "tests/general/deneb/kzg/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_0_0/manifest.yaml": "d9a6189be25f697cc7a048eeb0a877e27511f1277539177c0fd1424f443a47fd", + "tests/general/fulu/kzg/verify_cell_kzg_proof_batch/kzg-mainnet/verify_cell_kzg_proof_batch_case_valid_zero_cells/data.yaml": "a747662f7170410f52d5102cdcb8674bb1824cbb8812210518824b2ef94363a7", + "tests/general/fulu/kzg/verify_cell_kzg_proof_batch/kzg-mainnet/verify_cell_kzg_proof_batch_case_valid_zero_cells/manifest.yaml": "825e9e626a7ebf9fd795a858e146117340b134f670c93e22f27ea6b8c025c578", "tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/manifest.yaml": "468e5b587f73d9988739891d1e5accc15c56fc5f881704a95e6d40647de3fc3b", "tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/post.ssz_snappy": "fb857a6916ec5a0a18726e16d4ec9cf7b9195d64c35a52f68cffced459bcfce6", "tests/minimal/gloas/epoch_processing/effective_balance_updates/pyspec_tests/effective_balance_hysteresis/pre.ssz_snappy": "75236ae541e82fb209cc55eed82d6518f3fba332a5fecb2ac7496831c98835c3",