igneum-light: the finality object (version 1, 832 bytes), its verifier and the test vectors

light/ is its own workspace: igneum-light (no_std core: the object container, the statement offsets of
BlockOutput and FinExt, the MMR history check, the SP1 6.8.1 Groth16 wrap verified with arkworks under the
compiled-in key, the final-at answer), igneum-light-wasm (a C ABI for the tab, no bindings generator) and
igneum-light-cli (vectors, encodings, native timing). The wrap has not landed, so the chain fixture (the
fin-proof lane's proof 81053, public values 504 bytes) is a kind-0 stub; a kind-1 object with generator points
exercises the pairing and is refused. Native on this Mac: the pairing 0.78 ms in arkworks, 155 ms in
sp1-verifier's substrate-bn (kept as the reference check).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 09:02:52 +00:00
parent 3f0091f2e2
commit 59d2177fd3
33 changed files with 2432 additions and 0 deletions

2
light/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target/
*/target-remote/

880
light/Cargo.lock generated Normal file
View file

@ -0,0 +1,880 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "ark-bn254"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc"
dependencies = [
"ark-ec",
"ark-ff",
"ark-std",
]
[[package]]
name = "ark-crypto-primitives"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0c292754729c8a190e50414fd1a37093c786c709899f29c9f7daccecfa855e"
dependencies = [
"ahash",
"ark-crypto-primitives-macros",
"ark-ec",
"ark-ff",
"ark-relations",
"ark-serialize",
"ark-snark",
"ark-std",
"blake2",
"derivative",
"digest",
"fnv",
"merlin",
"rayon",
"sha2",
]
[[package]]
name = "ark-crypto-primitives-macros"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "ark-ec"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce"
dependencies = [
"ahash",
"ark-ff",
"ark-poly",
"ark-serialize",
"ark-std",
"educe",
"fnv",
"hashbrown",
"itertools",
"num-bigint",
"num-integer",
"num-traits",
"rayon",
"zeroize",
]
[[package]]
name = "ark-ff"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70"
dependencies = [
"ark-ff-asm",
"ark-ff-macros",
"ark-serialize",
"ark-std",
"arrayvec",
"digest",
"educe",
"itertools",
"num-bigint",
"num-traits",
"paste",
"rayon",
"zeroize",
]
[[package]]
name = "ark-ff-asm"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60"
dependencies = [
"quote",
"syn 2.0.119",
]
[[package]]
name = "ark-ff-macros"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3"
dependencies = [
"num-bigint",
"num-traits",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "ark-groth16"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88f1d0f3a534bb54188b8dcc104307db6c56cdae574ddc3212aec0625740fc7e"
dependencies = [
"ark-crypto-primitives",
"ark-ec",
"ark-ff",
"ark-poly",
"ark-relations",
"ark-serialize",
"ark-std",
"rayon",
]
[[package]]
name = "ark-poly"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27"
dependencies = [
"ahash",
"ark-ff",
"ark-serialize",
"ark-std",
"educe",
"fnv",
"hashbrown",
"rayon",
]
[[package]]
name = "ark-relations"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384"
dependencies = [
"ark-ff",
"ark-std",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "ark-serialize"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7"
dependencies = [
"ark-serialize-derive",
"ark-std",
"arrayvec",
"digest",
"num-bigint",
"rayon",
]
[[package]]
name = "ark-serialize-derive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "ark-snark"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d368e2848c2d4c129ce7679a7d0d2d612b6a274d3ea6a13bad4445d61b381b88"
dependencies = [
"ark-ff",
"ark-relations",
"ark-serialize",
"ark-std",
]
[[package]]
name = "ark-std"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a"
dependencies = [
"num-traits",
"rand",
"rayon",
]
[[package]]
name = "arrayvec"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "blake3"
version = "1.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
dependencies = [
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
"cpufeatures 0.3.1",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bytemuck"
version = "1.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "constant_time_eq"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "educe"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417"
dependencies = [
"enum-ordinalize",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "either"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
[[package]]
name = "enum-ordinalize"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677"
dependencies = [
"enum-ordinalize-derive",
]
[[package]]
name = "enum-ordinalize-derive"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "igneum-light"
version = "0.1.0"
dependencies = [
"ark-bn254",
"ark-ec",
"ark-ff",
"ark-groth16",
"sha2",
"sp1-verifier",
]
[[package]]
name = "igneum-light-cli"
version = "0.1.0"
dependencies = [
"igneum-light",
"sha2",
"substrate-bn-succinct-rs",
]
[[package]]
name = "igneum-light-wasm"
version = "0.1.0"
dependencies = [
"igneum-light",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "keccak"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures 0.2.17",
]
[[package]]
name = "lazy_static"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
dependencies = [
"spin",
]
[[package]]
name = "libc"
version = "0.2.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
[[package]]
name = "merlin"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d"
dependencies = [
"byteorder",
"keccak",
"rand_core",
"zeroize",
]
[[package]]
name = "num-bigint"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
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 = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[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.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
dependencies = [
"rand_chacha",
"rand_core",
]
[[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",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "rustc-hex"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6"
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "sp1-verifier"
version = "6.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aca7223cc7a77e42536c7229c8016672737aab07f6149097d9c7a26324b29814"
dependencies = [
"ark-bn254",
"ark-ec",
"ark-ff",
"ark-groth16",
"ark-serialize",
"blake3",
"cfg-if",
"hex",
"lazy_static",
"sha2",
"substrate-bn-succinct-rs",
"thiserror",
]
[[package]]
name = "spin"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
[[package]]
name = "substrate-bn-succinct-rs"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a241fd7c1016fb8ad30fcf5a20986c0c4538e8f15a1b41a1761516299e377ec1"
dependencies = [
"bytemuck",
"byteorder",
"cfg-if",
"crunchy",
"lazy_static",
"num-bigint",
"rand",
"rustc-hex",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-subscriber"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71"
dependencies = [
"tracing-core",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[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 = "zerocopy"
version = "0.8.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zeroize"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
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 2.0.119",
]

25
light/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
# The light client in the pocket (docs/design/finality-object.md, docs/design/phone-app.md "igneum-light"): one Rust
# crate that parses the finality object, verifies its SP1 Groth16 wrap over bn254 and answers "final at checkpoint N"
# from the object and a history path alone; a WebAssembly wrapper for the tab and a CLI for test vectors and timing.
# Its own workspace: nothing here depends on the node or the prover, so a phone or a tab builds it alone.
[workspace]
resolver = "2"
members = ["igneum-light", "igneum-light-wasm", "igneum-light-cli"]
[workspace.package]
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[workspace.dependencies]
# The SP1 Groth16 verifier, the version the aggregator guest is proven with (proving/igneum-prove/Cargo.toml pins
# sp1-sdk =6.8.1); `full` off: no compressed-proof verifier, no recursion machine, so it compiles to wasm32.
sp1-verifier = { version = "=6.8.1", default-features = false }
sha2 = { version = "0.10.8", default-features = false }
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
strip = true

View file

@ -0,0 +1,15 @@
[package]
name = "igneum-light-cli"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Test vectors, encodings and the native timing of igneum-light"
[[bin]]
name = "igneum-light"
path = "src/main.rs"
[dependencies]
igneum-light = { path = "../igneum-light", features = ["std"] }
bn = { package = "substrate-bn-succinct-rs", version = "=0.6.0" }
sha2 = { workspace = true }

View file

@ -0,0 +1,284 @@
//! igneum-light on the command line: the test vectors of docs/design/finality-object.md, the encodings, and the
//! native timing of the verifier (the phone path's proxy on a Mac until a phone build exists).
//!
//! igneum-light make-stub --pv <504 or 340 bytes> --key-id <hex32> --out <file>
//! igneum-light make-timing --pv <file> --key-id <hex32> --out <file> kind 1, curve points that are on the
//! curve and prove nothing: the pairing runs
//! igneum-light verify --object <file> [--query <file>] --key-id <hex32> --chain-id <n> [--genesis <unix s>]
//! igneum-light time --object <file> --key-id <hex32> --chain-id <n> [--rounds 20]
//! igneum-light url --object <file> [--query <file>] [--base https://igneum.network/pocket/]
//! igneum-light vectors --pv <file> --key-id <hex32> --out-dir <dir> the vector set with its manifest
use igneum_light::object::{base64url_encode, PV_LEN_FIN};
use igneum_light::{json::verdict_json, mmr::Mmr, Answer, Clock, FinalityObject, HistoryLeaf, Pinned, ProofVerdict, Query, Statement, Verifier, KIND_SP1_GROTH16};
use bn::Group;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::time::Instant;
fn args() -> (String, HashMap<String, String>) {
let mut it = std::env::args().skip(1);
let cmd = it.next().unwrap_or_default();
let mut m = HashMap::new();
let mut key: Option<String> = None;
for a in it {
if let Some(k) = a.strip_prefix("--") {
if let Some(prev) = key.take() {
m.insert(prev, String::from("1"));
}
key = Some(k.to_string());
} else if let Some(k) = key.take() {
m.insert(k, a);
}
}
if let Some(prev) = key.take() {
m.insert(prev, String::from("1"));
}
(cmd, m)
}
fn hex32(s: &str) -> [u8; 32] {
let s = s.trim().trim_start_matches("0x");
let b = (0..32).map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).expect("hex")).collect::<Vec<_>>();
b.try_into().unwrap()
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn fq_be(f: bn::Fq) -> [u8; 32] {
let mut b = [0u8; 32];
f.to_big_endian(&mut b).unwrap();
b
}
/// A proof whose three points are the curve generators: on the curve, so the verifier loads them and runs the
/// pairing, and wrong, so it refuses. The accept path costs the same pairing.
fn timing_proof() -> [u8; 256] {
let g1 = bn::AffineG1::from_jacobian(bn::G1::one()).unwrap();
let g2 = bn::AffineG2::from_jacobian(bn::G2::one()).unwrap();
let mut p = [0u8; 256];
p[0..32].copy_from_slice(&fq_be(g1.x()));
p[32..64].copy_from_slice(&fq_be(g1.y()));
p[64..96].copy_from_slice(&fq_be(g2.x().imaginary()));
p[96..128].copy_from_slice(&fq_be(g2.x().real()));
p[128..160].copy_from_slice(&fq_be(g2.y().imaginary()));
p[160..192].copy_from_slice(&fq_be(g2.y().real()));
p[192..224].copy_from_slice(&fq_be(g1.x()));
p[224..256].copy_from_slice(&fq_be(g1.y()));
p
}
fn timing_object(key_id: [u8; 32], pv: Vec<u8>) -> FinalityObject {
FinalityObject { kind: KIND_SP1_GROTH16, key_id, public_values: pv, nonce: [0u8; 32], proof: timing_proof() }
}
fn read(p: &str) -> Vec<u8> {
std::fs::read(p).unwrap_or_else(|e| panic!("read {p}: {e}"))
}
fn now_s() -> i64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64
}
/// A synthetic 504-byte statement over a synthetic history, for the vectors that need a history the CLI can prove
/// paths in: `n` chain blocks from `first`, lock at `lock_number`, the tail block `first + n - 1`.
fn synthetic(chain_id: u64, key_id: [u8; 32], first: u64, n: u64, lock_number: u64, stale: bool) -> (Vec<u8>, Vec<HistoryLeaf>, Mmr) {
let mut leaves = Vec::new();
let mut mmr = Mmr::default();
for i in 0..n {
let number = first + i;
let leaf = HistoryLeaf {
number,
block_hash: Sha256::digest(format!("igneum-pocket-vector-block-{number}").as_bytes()).into(),
daa: 100_000 + 3 * i,
table_root: Sha256::digest(format!("table-{number}").as_bytes()).into(),
keys_hash: Sha256::digest(format!("keys-{number}").as_bytes()).into(),
total: 4008,
};
mmr.push(&leaf);
leaves.push(leaf);
}
let last = &leaves[leaves.len() - 1];
let lock = &leaves[(lock_number - first) as usize];
let mut pv = Vec::with_capacity(PV_LEN_FIN);
pv.extend_from_slice(&chain_id.to_be_bytes());
pv.extend_from_slice(&last.number.to_be_bytes());
pv.extend_from_slice(&last.block_hash);
pv.extend_from_slice(&leaves[leaves.len() - 2].block_hash);
pv.extend_from_slice(&1u32.to_be_bytes());
for tag in ["tx", "pre", "post", "receipts"] {
pv.extend_from_slice(&Sha256::digest(format!("{tag}-{}", last.number).as_bytes()));
}
pv.extend_from_slice(&21_000u64.to_be_bytes());
pv.extend_from_slice(&0u64.to_be_bytes());
pv.extend_from_slice(&1u32.to_be_bytes());
pv.extend_from_slice(&0u32.to_be_bytes());
pv.extend_from_slice(&[0u8; 32]);
pv.extend_from_slice(&[0x39u8; 32]);
pv.extend_from_slice(&key_id);
pv.extend_from_slice(&n.to_be_bytes());
assert_eq!(pv.len(), 340);
pv.extend_from_slice(&1u16.to_be_bytes());
pv.extend_from_slice(&last.table_root);
pv.extend_from_slice(&mmr.root());
pv.extend_from_slice(&first.to_be_bytes());
pv.extend_from_slice(&2702u64.to_be_bytes());
pv.extend_from_slice(&lock.block_hash);
pv.extend_from_slice(&lock.number.to_be_bytes());
pv.extend_from_slice(&2808u64.to_be_bytes());
pv.extend_from_slice(&4008u64.to_be_bytes());
pv.extend_from_slice(&2808u64.to_be_bytes());
pv.extend_from_slice(&4008u64.to_be_bytes());
pv.extend_from_slice(&lock.daa.to_be_bytes());
pv.extend_from_slice(&(if stale { 1u16 } else { 0 }).to_be_bytes());
assert_eq!(pv.len(), PV_LEN_FIN);
(pv, leaves, mmr)
}
fn main() {
let (cmd, a) = args();
let get = |k: &str| a.get(k).cloned();
let need = |k: &str| a.get(k).cloned().unwrap_or_else(|| panic!("--{k} is required"));
match cmd.as_str() {
"make-stub" | "make-timing" => {
let pv = read(&need("pv"));
let key = hex32(&need("key-id"));
let obj = if cmd == "make-stub" { FinalityObject::stub(key, pv) } else { timing_object(key, pv) };
let b = obj.encode();
std::fs::write(need("out"), &b).unwrap();
println!("{} bytes, kind {}, sha256 {}", b.len(), obj.kind, hex(&Sha256::digest(&b)));
}
"verify" => {
let object = read(&need("object"));
let q = get("query").map(|p| Query::decode(&read(&p)).expect("query").0);
let pinned = Pinned { key_id: hex32(&need("key-id")), chain_id: need("chain-id").parse().unwrap() };
let clock = get("genesis").map(|g| Clock { now_unix_s: now_s(), genesis_unix_s: g.parse().unwrap(), seconds_per_daa: get("seconds-per-daa").map(|s| s.parse().unwrap()).unwrap_or(1.0) });
let t = Instant::now();
let v = igneum_light::verify(&object, q.as_ref(), &pinned, clock);
let ms = t.elapsed().as_secs_f64() * 1000.0;
println!("{}", verdict_json(&v));
eprintln!("{ms:.3} ms");
}
"time" => {
let object = read(&need("object"));
let pinned = Pinned { key_id: hex32(&need("key-id")), chain_id: need("chain-id").parse().unwrap() };
let rounds: usize = get("rounds").map(|s| s.parse().unwrap()).unwrap_or(20);
let obj = FinalityObject::decode(&object).unwrap();
let t = Instant::now();
let verifier = Verifier::new();
let prepare_ms = t.elapsed().as_secs_f64() * 1000.0;
let (mut full, mut wrap, mut reference) = (Vec::new(), Vec::new(), Vec::new());
for _ in 0..rounds {
let t = Instant::now();
let v = verifier.verify(&object, None, &pinned, None);
full.push(t.elapsed().as_secs_f64() * 1000.0);
assert!(matches!(v.proof, ProofVerdict::Refused(_) | ProofVerdict::Verified | ProofVerdict::Stub));
let t = Instant::now();
let _ = verifier.verify_wrap(&obj);
wrap.push(t.elapsed().as_secs_f64() * 1000.0);
let t = Instant::now();
let _ = igneum_light::verify_sp1_groth16_reference_one_pairing(&obj);
reference.push(t.elapsed().as_secs_f64() * 1000.0);
}
let stats = |v: &mut Vec<f64>| {
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
format!("min {:.2} median {:.2} max {:.2} ms", v[0], v[v.len() / 2], v[v.len() - 1])
};
println!("rounds {rounds}; prepare the key once {prepare_ms:.2} ms; verify (parse, pin, one pairing in arkworks) {}; the pairing alone {}; the reference pairing in substrate-bn {}", stats(&mut full), stats(&mut wrap), stats(&mut reference));
}
"url" => {
let object = read(&need("object"));
let base = get("base").unwrap_or_else(|| String::from("https://igneum.network/pocket/"));
let mut u = format!("{base}#o={}", base64url_encode(&object));
if let Some(q) = get("query") {
u.push_str(&format!("&q={}", base64url_encode(&read(&q))));
}
println!("{u}");
eprintln!("{} characters", u.len());
}
"vectors" => {
let pv = read(&need("pv"));
let key = hex32(&need("key-id"));
let dir = need("out-dir");
std::fs::create_dir_all(&dir).unwrap();
let st = Statement::parse(&pv).expect("the public values parse");
let chain_id = st.chain_id;
let pinned = Pinned { key_id: key, chain_id };
let verifier = Verifier::new();
let mut manifest = Vec::new();
let mut put = |name: &str, bytes: &[u8], query: Option<&[u8]>, expect_proof: &str, expect_answer: Option<Answer>| {
std::fs::write(format!("{dir}/{name}.bin"), bytes).unwrap();
if let Some(q) = query {
std::fs::write(format!("{dir}/{name}.query.bin"), q).unwrap();
}
let qd = query.map(|q| Query::decode(q).unwrap().0);
let v = verifier.verify(bytes, qd.as_ref(), &pinned, None);
let proof = match &v.proof { ProofVerdict::Verified => "verified", ProofVerdict::Stub => "stub", ProofVerdict::Refused(_) => "refused" };
assert_eq!(proof, expect_proof, "{name}: {}", verdict_json(&v));
assert_eq!(v.answer, expect_answer, "{name}: {}", verdict_json(&v));
manifest.push(format!(
"{{\"name\":\"{name}\",\"bytes\":{},\"sha256\":\"{}\",\"query\":{},\"expect\":{{\"proof\":\"{proof}\",\"answer\":{}}},\"verdict\":{}}}",
bytes.len(),
hex(&Sha256::digest(bytes)),
query.map(|q| format!("{{\"bytes\":{},\"sha256\":\"{}\"}}", q.len(), hex(&Sha256::digest(q)))).unwrap_or_else(|| String::from("null")),
expect_answer.map(|x| format!("\"{}\"", x.as_str())).unwrap_or_else(|| String::from("null")),
verdict_json(&v)
));
};
// 1. the fixture from the chain: the fin-proof lane's proof 81053 as a stub object (the wrap pending); its
// own block 81053 lies above the lock at 81049, so the proof's own block is not final
let stub = FinalityObject::stub(key, pv.clone()).encode();
put("fixture-81053-stub", &stub, None, "stub", Some(Answer::NotFinal));
// 2. the timing object: kind 1 with generator points; the pairing runs and refuses
let timing = timing_object(key, pv.clone()).encode();
put("fixture-81053-timing", &timing, None, "refused", None);
// 3. one byte changed in the public values of the stub (the lock number's low byte): a stub parses it
// like any other, which is why a stub never says "verified"; a kind-1 object refuses the same change
let mut flipped = stub.clone();
flipped[40 + 461] ^= 0x01;
put("fixture-81053-stub-flipped-lock-number", &flipped, None, "stub", Some(Answer::NotFinal));
let mut tflipped = timing.clone();
tflipped[40 + 461] ^= 0x01;
put("fixture-81053-timing-flipped-lock-number", &tflipped, None, "refused", None);
// 4. one byte changed in the proof of the timing object: refused
let mut tf = timing.clone();
let last = tf.len() - 1;
tf[last] ^= 0x01;
put("fixture-81053-timing-flipped-proof", &tf, None, "refused", None);
// 5. the key id changed: refused before anything is read
let mut kf = stub.clone();
kf[8] ^= 0x01;
put("fixture-81053-stub-wrong-key", &kf, None, "refused", None);
// 6. the header: version 2
let mut vf = stub.clone();
vf[4] = 2;
put("fixture-81053-stub-version-2", &vf, None, "refused", None);
// 7. a synthetic history of 300 blocks from 79046 with the lock at 79300: a block below the lock is
// final, a block above is not, a block with a wrong leaf is not in this chain, a stale flag is stale
let (spv, leaves, mmr) = synthetic(chain_id, key, 79046, 300, 79300, false);
let sobj = FinalityObject::stub(key, spv.clone()).encode();
let q_final = Query { leaf: leaves[100].clone(), proof: mmr.proof(100).unwrap() }.encode();
put("synthetic-300-block-79146-final", &sobj, Some(&q_final), "stub", Some(Answer::Final));
let q_last = Query { leaf: leaves[299].clone(), proof: mmr.proof(299).unwrap() }.encode();
put("synthetic-300-block-79345-not-final", &sobj, Some(&q_last), "stub", Some(Answer::NotFinal));
let mut bad_leaf = leaves[100].clone();
bad_leaf.daa += 1;
let q_bad = Query { leaf: bad_leaf, proof: mmr.proof(100).unwrap() }.encode();
put("synthetic-300-block-79146-wrong-leaf-not-in-chain", &sobj, Some(&q_bad), "stub", Some(Answer::NotInChain));
let (stale_pv, _, _) = synthetic(chain_id, key, 79046, 300, 79300, true);
put("synthetic-300-stale", &FinalityObject::stub(key, stale_pv).encode(), None, "stub", Some(Answer::Stale));
let (other, _, _) = synthetic(chain_id + 1, key, 79046, 300, 79300, false);
put("synthetic-300-other-chain", &FinalityObject::stub(key, other).encode(), None, "refused", None);
// 8. the timing object over the synthetic statement, with the final query: proof refused, no answer
put("synthetic-300-timing", &timing_object(key, spv).encode(), Some(&q_final), "refused", None);
std::fs::write(format!("{dir}/manifest.json"), format!("{{\"format\":\"igneum-finality-object-vectors-1\",\"object_version\":1,\"sp1_version\":\"{}\",\"key_id\":\"{}\",\"chain_id\":{chain_id},\"vectors\":[\n{}\n]}}\n", igneum_light::SP1_VERSION, hex(&key), manifest.join(",\n"))).unwrap();
println!("{} vectors in {dir}", manifest.len());
}
_ => {
eprintln!("igneum-light make-stub|make-timing|verify|time|url|vectors (see the file head)");
std::process::exit(2);
}
}
}

View file

@ -0,0 +1,12 @@
[package]
name = "igneum-light-wasm"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "igneum-light for the browser tab: a C ABI over WebAssembly, no bindings generator"
[lib]
crate-type = ["cdylib"]
[dependencies]
igneum-light = { path = "../igneum-light" }

View file

@ -0,0 +1,98 @@
//! igneum-light for the tab: a C ABI the page calls through `WebAssembly.instantiate`, no bindings generator, no
//! JavaScript glue beyond `site/pocket/light.js`. Memory travels through `pf_alloc`/`pf_free`; the verdict comes
//! back as JSON (`igneum_light::json::verdict_json`) in a caller-supplied buffer.
//!
//! pf_abi_version() -> 1
//! pf_warm() prepares the verifying key once (one pairing); idempotent
//! pf_alloc(len) -> ptr, pf_free(ptr, len)
//! pf_verify(obj, obj_len, query, query_len, key_id (32 bytes), chain_id (u64), has_clock (0/1), now_unix_s,
//! genesis_unix_s, seconds_per_daa, out, out_cap) -> bytes written, or -(bytes needed) when out_cap is short
//!
//! `query_len` 0 means "the proof's own block". A query that fails to decode refuses the call (written as a refused
//! verdict), never a silent fall-through to the proof's own block.
use igneum_light::{json::verdict_json, Clock, Pinned, ProofVerdict, Query, Verdict, Verifier, TRUST_ROW};
use std::sync::OnceLock;
static VERIFIER: OnceLock<Verifier> = OnceLock::new();
fn verifier() -> &'static Verifier {
VERIFIER.get_or_init(Verifier::new)
}
#[no_mangle]
pub extern "C" fn pf_abi_version() -> u32 {
1
}
/// Prepares the Groth16 verifying key (one pairing) ahead of the first verify; the page calls it at load so the
/// measured verify is the per-object cost alone. Idempotent.
#[no_mangle]
pub extern "C" fn pf_warm() {
let _ = verifier();
}
#[no_mangle]
pub extern "C" fn pf_alloc(len: usize) -> *mut u8 {
let mut v = Vec::<u8>::with_capacity(len.max(1));
let p = v.as_mut_ptr();
std::mem::forget(v);
p
}
/// # Safety
/// `ptr` came from `pf_alloc(len)` and is freed once.
#[no_mangle]
pub unsafe extern "C" fn pf_free(ptr: *mut u8, len: usize) {
if !ptr.is_null() {
drop(Vec::from_raw_parts(ptr, 0, len.max(1)));
}
}
fn write_out(json: &str, out: *mut u8, out_cap: usize) -> i32 {
let b = json.as_bytes();
if b.len() > out_cap {
return -(b.len() as i32);
}
// SAFETY: the caller allocated `out_cap` bytes at `out` through pf_alloc
unsafe { core::ptr::copy_nonoverlapping(b.as_ptr(), out, b.len()) };
b.len() as i32
}
/// # Safety
/// Every pointer names `len` readable bytes allocated through `pf_alloc`; `key_id` names 32; `out` names `out_cap`.
#[no_mangle]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn pf_verify(
obj: *const u8,
obj_len: usize,
query: *const u8,
query_len: usize,
key_id: *const u8,
chain_id: u64,
has_clock: u32,
now_unix_s: f64,
genesis_unix_s: f64,
seconds_per_daa: f64,
out: *mut u8,
out_cap: usize,
) -> i32 {
let object = core::slice::from_raw_parts(obj, obj_len);
let mut key = [0u8; 32];
key.copy_from_slice(core::slice::from_raw_parts(key_id, 32));
let pinned = Pinned { key_id: key, chain_id };
let q = if query_len == 0 {
None
} else {
match Query::decode(core::slice::from_raw_parts(query, query_len)) {
Ok((q, _)) => Some(q),
Err(why) => {
let v = Verdict { proof: ProofVerdict::Refused(format!("block query: {why}")), answer: None, block_number: None, statement: None, age_s: None, trust_row: TRUST_ROW };
return write_out(&verdict_json(&v), out, out_cap);
}
}
};
let clock = (has_clock != 0).then_some(Clock { now_unix_s: now_unix_s as i64, genesis_unix_s: genesis_unix_s as i64, seconds_per_daa });
let v = verifier().verify(object, q.as_ref(), &pinned, clock);
write_out(&verdict_json(&v), out, out_cap)
}

View file

@ -0,0 +1,20 @@
[package]
name = "igneum-light"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Igneum light client core: the finality object, its SP1 Groth16 verification and the final-at answer"
[dependencies]
# `ark` gives the gnark-to-arkworks converters; the pairing itself runs in ark-groth16 (the measured path: one
# multi-Miller loop in arkworks against sp1-verifier's own substrate-bn, kept as the reference check)
sp1-verifier = { workspace = true, features = ["ark"] }
sha2 = { workspace = true }
ark-groth16 = { version = "0.5.0", default-features = false }
ark-bn254 = { version = "0.5.0", default-features = false, features = ["curve"] }
ark-ec = { version = "0.5.0", default-features = false }
ark-ff = { version = "0.5.0", default-features = false }
[features]
default = []
std = []

View file

@ -0,0 +1,87 @@
//! The verdict as JSON for the tab and the CLI, written by hand so the wasm carries no serializer.
use crate::{ProofVerdict, Verdict};
use alloc::string::String;
fn push_str(out: &mut String, s: &str) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
c if (c as u32) < 0x20 => out.push_str(&alloc::format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
}
fn push_hex(out: &mut String, b: &[u8]) {
out.push('"');
for x in b {
out.push_str(&alloc::format!("{x:02x}"));
}
out.push('"');
}
pub fn verdict_json(v: &Verdict) -> String {
let mut o = String::with_capacity(1024);
o.push_str("{\"proof\":");
match &v.proof {
ProofVerdict::Verified => o.push_str("\"verified\",\"reason\":null"),
ProofVerdict::Stub => o.push_str("\"stub\",\"reason\":null"),
ProofVerdict::Refused(why) => {
o.push_str("\"refused\",\"reason\":");
push_str(&mut o, why);
}
}
o.push_str(",\"answer\":");
match v.answer {
Some(a) => push_str(&mut o, a.as_str()),
None => o.push_str("null"),
}
o.push_str(",\"block_number\":");
match v.block_number {
Some(n) => o.push_str(&alloc::format!("{n}")),
None => o.push_str("null"),
}
o.push_str(",\"age_s\":");
match v.age_s {
Some(n) => o.push_str(&alloc::format!("{n}")),
None => o.push_str("null"),
}
o.push_str(",\"trust_row\":");
push_str(&mut o, v.trust_row);
o.push_str(",\"statement\":");
match &v.statement {
None => o.push_str("null"),
Some(s) => {
o.push_str(&alloc::format!("{{\"chain_id\":{},\"number\":{},\"chain_len\":{},\"block_hash\":", s.chain_id, s.number, s.chain_len));
push_hex(&mut o, &s.block_hash);
o.push_str(",\"post_root\":");
push_hex(&mut o, &s.post_root);
o.push_str(",\"agg_vk\":");
push_hex(&mut o, &s.agg_vk);
o.push_str(",\"fin\":");
match &s.fin {
None => o.push_str("null"),
Some(f) => {
o.push_str(&alloc::format!("{{\"history_first\":{},\"stale\":{},\"history_root\":", f.history_first, f.stale()));
push_hex(&mut o, &f.history_root);
o.push_str(",\"table_root\":");
push_hex(&mut o, &f.table_root);
o.push_str(&alloc::format!(
",\"lock\":{{\"index\":{},\"number\":{},\"signed\":{},\"total\":{},\"frozen_signed\":{},\"frozen_total\":{},\"daa\":{},\"hash\":",
f.lock.index, f.lock.number, f.lock.signed, f.lock.total, f.lock.frozen_signed, f.lock.frozen_total, f.lock.daa
));
push_hex(&mut o, &f.lock.hash);
o.push_str("}}");
}
}
o.push('}');
}
}
o.push('}');
o
}

View file

@ -0,0 +1,250 @@
//! Igneum light client, the core (docs/design/finality-object.md; docs/design/finality-in-proof.md section 4).
//!
//! One function answers the pocket question: `verify(object, query, pinned, now)` takes the finality object's bytes,
//! an optional block query (a history leaf and its MMR path), the pinned aggregator id and the reader's clock, and
//! returns a `Verdict`: whether the proof verified (the SP1 Groth16 wrap over bn254 under the pinned key), what the
//! statement says (chain id, block, lock, weight), and the answer for the block asked about: final, not final, stale,
//! not in this chain, or no claim. It asks nothing of any node and holds no state. `no_std` with `alloc`, so the same
//! code runs natively, in WebAssembly in a tab (`igneum-light-wasm`) and behind a foreign-function layer on a phone.
//!
//! Byte offsets inside the public values are those of `BlockOutput::to_bytes` (proving/igneum-prove/core/src/agg.rs)
//! and `FinExt::to_bytes` (proving/igneum-prove/fin/src/lib.rs), big-endian, confirmed by the fin-proof lane on
//! 7 October 2026; the MMR and leaf hashing follow `igneum_fin_core::mmr` as `site/verify/finproof.js` does.
#![cfg_attr(not(any(feature = "std", test)), no_std)]
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use sha2::{Digest, Sha256};
pub mod json;
pub mod mmr;
pub mod object;
pub mod statement;
pub use mmr::{HistoryLeaf, MmrProof, Query};
pub use object::{FinalityObject, ObjectError, KIND_SP1_GROTH16, KIND_STUB};
#[cfg(test)]
mod tests;
pub use statement::{FinExt, Lock, Statement};
/// The SP1 version whose Groth16 verifying key and recursion root are compiled in (sp1-verifier 6.8.1).
pub const SP1_VERSION: &str = "6.8.1";
/// What the reader pins before it trusts an object: the aggregator program id (the SP1 verifying-key hash of the
/// pinned aggregator guest, `vk.bytes32()`) and the chain id. An object under another key or for another chain is
/// refused before anything else is read from it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Pinned {
pub key_id: [u8; 32],
pub chain_id: u64,
}
/// The proof half of the verdict.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProofVerdict {
/// The Groth16 wrap verified under the pinned key: the statement is proven.
Verified,
/// The object carries the stub kind: the wrap has not landed, the statement is unverified. Never "final".
Stub,
/// The object was refused; the reason names the first check that failed.
Refused(String),
}
/// The block half of the verdict.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Answer {
/// The block is on the chain at or below the latest lock the proof carries.
Final,
/// The block is on the chain but above the lock, or the proof carries no lock yet.
NotFinal,
/// The proof chain paused for more than a weight window; the object needs a fresh root (design 4.4).
Stale,
/// The query's leaf and path do not reach the proof's history root.
NotInChain,
/// The public values carry no finality extension (a proof made before the activation).
NoClaim,
}
impl Answer {
pub fn as_str(&self) -> &'static str {
match self {
Answer::Final => "final",
Answer::NotFinal => "not final",
Answer::Stale => "stale",
Answer::NotInChain => "not in this chain",
Answer::NoClaim => "no claim",
}
}
}
/// The whole verdict. `answer` is `None` when the object was refused outright (nothing in it is believed).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Verdict {
pub proof: ProofVerdict,
pub answer: Option<Answer>,
/// The block the answer is about: the query's leaf, or the proof's own last block.
pub block_number: Option<u64>,
pub statement: Option<Statement>,
/// The age of the lock by the reader's clock in seconds, when the reader gave a clock and a genesis time.
pub age_s: Option<i64>,
pub trust_row: &'static str,
}
/// The trust row every surface shows beside a proof-carried lock (design section 4.4, level 0 of 5.2).
pub const TRUST_ROW: &str = "voter set: verified in the proof; blue set: from the prover, vetoed by full nodes (level 0)";
/// The trust row for a stub object.
pub const TRUST_ROW_STUB: &str = "wrap pending: the public values are read, nothing is proven";
/// The reader's clock, for the age line: the current unix time and the chain's genesis time, both in seconds. The
/// DAA score counts seconds of expected block time since genesis at one block a second, so the lock's age by the
/// reader's clock is `now - (genesis + lock_daa)`; the devnet at a different block rate scales it (`seconds_per_daa`).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Clock {
pub now_unix_s: i64,
pub genesis_unix_s: i64,
pub seconds_per_daa: f64,
}
/// The verifier: the Groth16 verifying key of the pinned SP1 version, prepared once (the preparation is itself one
/// pairing, `alpha_g1 x beta_g2`, so a reader keeps one `Verifier` for its lifetime and the per-object cost is one
/// multi-Miller loop and one final exponentiation).
pub struct Verifier {
pvk: ark_groth16::PreparedVerifyingKey<ark_bn254::Bn254>,
}
impl Default for Verifier {
fn default() -> Self {
Self::new()
}
}
impl Verifier {
pub fn new() -> Self {
let vk = sp1_verifier::load_ark_groth16_verifying_key_from_bytes(*sp1_verifier::GROTH16_VK_BYTES).expect("the compiled-in Groth16 verifying key parses");
Self { pvk: ark_groth16::prepare_verifying_key(&vk) }
}
/// The pocket question. Order of checks: the container (magic, version, kind, lengths), the pinned key, the
/// chain id, the proof (kind 1 only), then the query against the history root, then the lock. The first failure
/// refuses the object and nothing later is read.
pub fn verify(&self, object: &[u8], query: Option<&Query>, pinned: &Pinned, clock: Option<Clock>) -> Verdict {
let refused = |why: String| Verdict { proof: ProofVerdict::Refused(why), answer: None, block_number: None, statement: None, age_s: None, trust_row: TRUST_ROW };
let obj = match FinalityObject::decode(object) {
Ok(o) => o,
Err(e) => return refused(e.to_string()),
};
if obj.key_id != pinned.key_id {
return refused(String::from("key id is not the pinned aggregator id"));
}
let st = match Statement::parse(&obj.public_values) {
Some(s) => s,
None => return refused(String::from("public values do not parse as a block statement")),
};
if st.chain_id != pinned.chain_id {
return refused(alloc::format!("chain id {} is not the pinned chain {}", st.chain_id, pinned.chain_id));
}
if st.agg_vk != [0u8; 32] && st.agg_vk != pinned.key_id {
// a continuing proof names the aggregator key it verified; it must be the same pinned program
return refused(String::from("the statement's agg_vk is not the pinned aggregator id"));
}
let proof = match obj.kind {
KIND_STUB => ProofVerdict::Stub,
KIND_SP1_GROTH16 => match self.verify_wrap(&obj) {
Ok(()) => ProofVerdict::Verified,
Err(why) => return refused(why),
},
k => return refused(alloc::format!("unknown proof kind {k}")),
};
let trust_row = if proof == ProofVerdict::Stub { TRUST_ROW_STUB } else { TRUST_ROW };
let Some(fin) = &st.fin else {
return Verdict { proof, answer: Some(Answer::NoClaim), block_number: Some(st.number), statement: Some(st), age_s: None, trust_row };
};
let mut number = st.number;
if let Some(q) = query {
let leaves = st.number - fin.history_first + 1;
if !mmr::verify_history(&q.leaf, &q.proof, &fin.history_root, leaves) {
return Verdict { proof, answer: Some(Answer::NotInChain), block_number: Some(q.leaf.number), statement: Some(st), age_s: None, trust_row };
}
number = q.leaf.number;
}
let age_s = clock.map(|c| c.now_unix_s - c.genesis_unix_s - ((fin.lock.daa as f64) * c.seconds_per_daa) as i64);
let answer = if fin.stale() {
Answer::Stale
} else if fin.lock.index > 0 && number <= fin.lock.number {
Answer::Final
} else {
Answer::NotFinal
};
Verdict { proof, answer: Some(answer), block_number: Some(number), statement: Some(st), age_s, trust_row }
}
/// The wrap check as `sp1_verifier::Groth16Verifier::verify_with_exit_code` does it at 6.8.1 (exit code zero,
/// the recursion root of the version, the public values hashed with SHA-256 and masked to 253 bits), with the
/// pairing in arkworks: public inputs `[key_id, sha256(public_values) & mask, 0, vk_root, nonce]`.
pub fn verify_wrap(&self, obj: &FinalityObject) -> Result<(), String> {
let proof = sp1_verifier::load_ark_proof_from_bytes(&obj.proof).map_err(|e| alloc::format!("the wrap's curve points do not load: {e:?}"))?;
let inputs = sp1_verifier::load_ark_public_inputs_from_bytes(&obj.key_id, &sp1_verifier::hash_public_inputs(&obj.public_values), &[0u8; 32], &sp1_verifier::VK_ROOT_BYTES, &obj.nonce);
match ark_groth16::Groth16::<ark_bn254::Bn254>::verify_proof(&self.pvk, &proof, &inputs) {
Ok(true) => Ok(()),
Ok(false) => Err(String::from("the Groth16 wrap does not verify under the pinned key")),
Err(e) => Err(alloc::format!("the Groth16 wrap does not verify under the pinned key: {e:?}")),
}
}
}
/// The pocket question with a fresh `Verifier` (one extra pairing to prepare the key; a long-lived reader keeps one).
pub fn verify(object: &[u8], query: Option<&Query>, pinned: &Pinned, clock: Option<Clock>) -> Verdict {
Verifier::new().verify(object, query, pinned, clock)
}
/// The SP1 Groth16 proof as the SDK emits it at 6.8.1, rebuilt from the object's 288 carried bytes: the 4-byte
/// prefix (sha256 of the Groth16 verifying key), the exit code (zero: the guest never exits otherwise), the recursion
/// verifying-key root (a constant of the SP1 version), the proof nonce and the 256-byte gnark proof. The prefix and
/// the root are not carried because the verifier pins them; carrying them would let an object name a key the
/// verifier does not hold, which is a refusal either way.
pub fn sp1_proof_bytes(obj: &FinalityObject) -> Vec<u8> {
let vk_hash = Sha256::digest(*sp1_verifier::GROTH16_VK_BYTES);
let mut v = Vec::with_capacity(4 + 96 + 256);
v.extend_from_slice(&vk_hash[..4]);
v.extend_from_slice(&[0u8; 32]);
v.extend_from_slice(&*sp1_verifier::VK_ROOT_BYTES);
v.extend_from_slice(&obj.nonce);
v.extend_from_slice(&obj.proof);
v
}
fn hex32(b: &[u8; 32]) -> String {
let mut s = String::with_capacity(66);
s.push_str("0x");
for x in b {
s.push_str(&alloc::format!("{x:02x}"));
}
s
}
/// The reference check: sp1-verifier's own `Groth16Verifier::verify` (substrate-bn). It hashes the public values
/// with SHA-256 and, on failure, once more with BLAKE3, so a refusal costs two pairings and an acceptance one. The
/// tests hold the arkworks path to this one.
pub fn verify_sp1_groth16_reference(obj: &FinalityObject) -> Result<(), String> {
let full = sp1_proof_bytes(obj);
sp1_verifier::Groth16Verifier::verify(&full, &obj.public_values, &hex32(&obj.key_id), *sp1_verifier::GROTH16_VK_BYTES)
.map_err(|e| alloc::format!("the Groth16 wrap does not verify under the pinned key: {e:?}"))
}
/// The reference check's accept-path cost alone (one pairing in substrate-bn), for timing.
pub fn verify_sp1_groth16_reference_one_pairing(obj: &FinalityObject) -> Result<(), String> {
let pv_hash = sp1_verifier::hash_public_inputs(&obj.public_values);
sp1_verifier::Groth16Verifier::verify_gnark_proof(&obj.proof, &[obj.key_id, pv_hash, [0u8; 32], *sp1_verifier::VK_ROOT_BYTES, obj.nonce], *sp1_verifier::GROTH16_VK_BYTES)
.map_err(|e| alloc::format!("{e:?}"))
}
pub fn sha256(parts: &[&[u8]]) -> [u8; 32] {
let mut h = Sha256::new();
for p in parts {
h.update(p);
}
h.finalize().into()
}

View file

@ -0,0 +1,293 @@
//! The history check (`igneum_fin_core::mmr`, mirrored by `site/verify/finproof.js`): a chain block's leaf and its
//! Merkle mountain range path against the `history_root` the proof carries. Leaf `sha256(0x04 ‖ number_le ‖
//! block_hash ‖ daa_le ‖ table_root ‖ keys_hash ‖ total_le)`, node `sha256(0x02 ‖ left ‖ right)`, peaks bagged
//! from the right with `sha256(0x03 ‖ peak ‖ acc)`.
//!
//! The query's byte form (section 4 of the object spec), appended to an object in a QR bundle or carried as `q=`
//! in the URL:
//!
//! | offset | bytes | field |
//! |---|---|---|
//! | 0 | 4 | magic `IGFQ` |
//! | 4 | 1 | version, 1 |
//! | 5 | 8 | leaf number |
//! | 13 | 32 | leaf block hash |
//! | 45 | 8 | leaf daa |
//! | 53 | 32 | leaf table root |
//! | 85 | 32 | leaf keys hash |
//! | 117 | 8 | leaf total |
//! | 125 | 8 | position (leaf index in the history, zero based) |
//! | 133 | 1 | peak index |
//! | 134 | 1 | peak count p |
//! | 135 | 33 p | peaks, each height (1) then hash (32), heights strictly decreasing |
//! | 135 + 33 p | 1 | sibling count s (equals the chosen peak's height) |
//! | 136 + 33 p | 33 s | siblings, each side (1: 1 = the sibling is on the left) then hash (32) |
use alloc::string::String;
use alloc::vec::Vec;
pub const QUERY_MAGIC: [u8; 4] = *b"IGFQ";
pub const QUERY_VERSION: u8 = 1;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HistoryLeaf {
pub number: u64,
pub block_hash: [u8; 32],
pub daa: u64,
pub table_root: [u8; 32],
pub keys_hash: [u8; 32],
pub total: u64,
}
impl HistoryLeaf {
pub fn hash(&self) -> [u8; 32] {
crate::sha256(&[&[4u8], &self.number.to_le_bytes(), &self.block_hash, &self.daa.to_le_bytes(), &self.table_root, &self.keys_hash, &self.total.to_le_bytes()])
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MmrProof {
pub position: u64,
pub peak_index: usize,
/// (height, hash), left to right, heights strictly decreasing
pub peaks: Vec<(u8, [u8; 32])>,
/// (sibling is on the left, hash), leaf upwards
pub siblings: Vec<(bool, [u8; 32])>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Query {
pub leaf: HistoryLeaf,
pub proof: MmrProof,
}
/// `MmrProof::verify`: the leaf sits at `proof.position` in a history of `leaves` leaves whose peaks bag to `root`.
pub fn verify_history(leaf: &HistoryLeaf, proof: &MmrProof, root: &[u8; 32], leaves: u64) -> bool {
let peaks = &proof.peaks;
if proof.position >= leaves || proof.peak_index >= peaks.len() {
return false;
}
let mut count: u128 = 0;
let mut last: Option<u8> = None;
for (h, _) in peaks {
if let Some(l) = last {
if l <= *h {
return false;
}
}
if *h >= 64 {
return false;
}
last = Some(*h);
count += 1u128 << *h;
}
if count != leaves as u128 {
return false;
}
let before: u128 = peaks[..proof.peak_index].iter().map(|(h, _)| 1u128 << *h).sum();
let height = peaks[proof.peak_index].0;
if (proof.position as u128) < before {
return false;
}
let mut idx = proof.position as u128 - before;
if idx >= (1u128 << height) || proof.siblings.len() != height as usize {
return false;
}
let mut node = leaf.hash();
for (left, sib) in &proof.siblings {
if *left != (idx & 1 == 1) {
return false;
}
node = if *left { crate::sha256(&[&[2u8], sib, &node]) } else { crate::sha256(&[&[2u8], &node, sib]) };
idx >>= 1;
}
if node != peaks[proof.peak_index].1 {
return false;
}
let mut acc = peaks[peaks.len() - 1].1;
for i in (0..peaks.len() - 1).rev() {
acc = crate::sha256(&[&[3u8], &peaks[i].1, &acc]);
}
acc == *root
}
impl Query {
pub fn encode(&self) -> Vec<u8> {
let mut v = Vec::with_capacity(136 + 33 * (self.proof.peaks.len() + self.proof.siblings.len()));
v.extend_from_slice(&QUERY_MAGIC);
v.push(QUERY_VERSION);
v.extend_from_slice(&self.leaf.number.to_be_bytes());
v.extend_from_slice(&self.leaf.block_hash);
v.extend_from_slice(&self.leaf.daa.to_be_bytes());
v.extend_from_slice(&self.leaf.table_root);
v.extend_from_slice(&self.leaf.keys_hash);
v.extend_from_slice(&self.leaf.total.to_be_bytes());
v.extend_from_slice(&self.proof.position.to_be_bytes());
v.push(self.proof.peak_index as u8);
v.push(self.proof.peaks.len() as u8);
for (h, p) in &self.proof.peaks {
v.push(*h);
v.extend_from_slice(p);
}
v.push(self.proof.siblings.len() as u8);
for (left, s) in &self.proof.siblings {
v.push(*left as u8);
v.extend_from_slice(s);
}
v
}
/// Decodes a query from the front of `b`; returns it with the bytes it used.
pub fn decode(b: &[u8]) -> Result<(Self, usize), String> {
if b.len() < 135 {
return Err(String::from("query is shorter than its fixed part"));
}
if b[0..4] != QUERY_MAGIC {
return Err(String::from("not a block query (magic)"));
}
if b[4] != QUERY_VERSION {
return Err(alloc::format!("query version {} is not 1", b[4]));
}
let u64_at = |i: usize| u64::from_be_bytes(b[i..i + 8].try_into().unwrap());
let h_at = |i: usize| -> [u8; 32] { b[i..i + 32].try_into().unwrap() };
let leaf = HistoryLeaf { number: u64_at(5), block_hash: h_at(13), daa: u64_at(45), table_root: h_at(53), keys_hash: h_at(85), total: u64_at(117) };
let position = u64_at(125);
let peak_index = b[133] as usize;
let np = b[134] as usize;
let mut o = 135;
if b.len() < o + 33 * np + 1 {
return Err(String::from("query is shorter than its peaks"));
}
let mut peaks = Vec::with_capacity(np);
for _ in 0..np {
peaks.push((b[o], h_at(o + 1)));
o += 33;
}
let ns = b[o] as usize;
o += 1;
if b.len() < o + 33 * ns {
return Err(String::from("query is shorter than its siblings"));
}
let mut siblings = Vec::with_capacity(ns);
for _ in 0..ns {
if b[o] > 1 {
return Err(String::from("a sibling side byte is not 0 or 1"));
}
siblings.push((b[o] == 1, h_at(o + 1)));
o += 33;
}
Ok((Query { leaf, proof: MmrProof { position, peak_index, peaks, siblings } }, o))
}
}
/// A small in-memory MMR, for tests and for the CLI's vectors: the same construction as `igneum_fin_core::mmr::Mmr`
/// (append leaves; peaks are perfect binary trees by height; a proof is the path inside the leaf's peak).
#[derive(Clone, Debug, Default)]
pub struct Mmr {
leaves: Vec<[u8; 32]>,
}
impl Mmr {
pub fn push(&mut self, leaf: &HistoryLeaf) {
self.leaves.push(leaf.hash());
}
pub fn leaves(&self) -> u64 {
self.leaves.len() as u64
}
/// The peaks are the set bits of the leaf count, highest first: one perfect tree per bit.
fn peak_ranges(&self) -> Vec<(u8, usize, usize)> {
let n = self.leaves.len();
let mut out = Vec::new();
let mut start = 0;
for h in (0..63u8).rev() {
if n & (1usize << h) != 0 {
out.push((h, start, start + (1 << h)));
start += 1 << h;
}
}
out
}
fn subtree(&self, lo: usize, hi: usize) -> [u8; 32] {
if hi - lo == 1 {
return self.leaves[lo];
}
let mid = lo + (hi - lo) / 2;
crate::sha256(&[&[2u8], &self.subtree(lo, mid), &self.subtree(mid, hi)])
}
pub fn peaks(&self) -> Vec<(u8, [u8; 32])> {
self.peak_ranges().iter().map(|(h, lo, hi)| (*h, self.subtree(*lo, *hi))).collect()
}
pub fn root(&self) -> [u8; 32] {
let peaks = self.peaks();
let mut acc = peaks[peaks.len() - 1].1;
for i in (0..peaks.len() - 1).rev() {
acc = crate::sha256(&[&[3u8], &peaks[i].1, &acc]);
}
acc
}
pub fn proof(&self, position: u64) -> Option<MmrProof> {
let ranges = self.peak_ranges();
let pos = position as usize;
let (peak_index, &(h, lo, hi)) = ranges.iter().enumerate().find(|(_, (_, lo, hi))| pos >= *lo && pos < *hi)?;
let mut siblings = Vec::with_capacity(h as usize);
let (mut l, mut r) = (lo, hi);
let mut path = Vec::new();
while r - l > 1 {
let mid = l + (r - l) / 2;
if pos < mid {
path.push((false, mid, r));
r = mid;
} else {
path.push((true, l, mid));
l = mid;
}
}
for (left, a, b) in path.into_iter().rev() {
siblings.push((left, self.subtree(a, b)));
}
Some(MmrProof { position, peak_index, peaks: self.peaks(), siblings })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn leaf(n: u64) -> HistoryLeaf {
HistoryLeaf { number: 1000 + n, block_hash: [n as u8; 32], daa: 5000 + n, table_root: [1; 32], keys_hash: [2; 32], total: 99 }
}
#[test]
fn every_leaf_of_every_size_verifies_and_a_flip_fails() {
for n in 1..40u64 {
let mut m = Mmr::default();
for i in 0..n {
m.push(&leaf(i));
}
let root = m.root();
for i in 0..n {
let p = m.proof(i).unwrap();
assert!(verify_history(&leaf(i), &p, &root, n), "n {n} i {i}");
let mut bad = leaf(i);
bad.daa += 1;
assert!(!verify_history(&bad, &p, &root, n));
assert!(!verify_history(&leaf(i), &p, &root, n + 1));
let q = Query { leaf: leaf(i), proof: p.clone() };
let (back, used) = Query::decode(&q.encode()).unwrap();
assert_eq!(back, q);
assert_eq!(used, q.encode().len());
}
let mut p = m.proof(0).unwrap();
if let Some(s) = p.siblings.first_mut() {
s.1[0] ^= 1;
assert!(!verify_history(&leaf(0), &p, &root, n));
}
}
}
}

View file

@ -0,0 +1,225 @@
//! The finality object, version 1 (docs/design/finality-object.md section 2): a fixed header, the pinned key id,
//! the aggregator's public values and the 288 carried bytes of the SP1 Groth16 wrap.
//!
//! | offset | bytes | field |
//! |---|---|---|
//! | 0 | 4 | magic `IGFO` |
//! | 4 | 1 | version, 1 |
//! | 5 | 1 | kind: 0 stub (no wrap), 1 SP1 6.8.1 Groth16 over bn254 |
//! | 6 | 2 | public values length, big-endian: 340 (no finality claim) or 504 (with the extension) |
//! | 8 | 32 | key id: the aggregator program's SP1 verifying-key hash |
//! | 40 | n | public values, `BlockOutput::to_bytes` |
//! | 40 + n | 32 | proof nonce (bound as a public input by the wrap circuit) |
//! | 72 + n | 256 | gnark Groth16 proof: A (64), B (128), C (64), uncompressed big-endian |
//!
//! 832 bytes at n = 504, 668 at n = 340. Every byte is bound: the header by the parser, the key id and the public
//! values as public inputs of the pairing, the nonce likewise, the proof by the pairing itself.
use alloc::string::String;
use alloc::vec::Vec;
pub const MAGIC: [u8; 4] = *b"IGFO";
pub const VERSION: u8 = 1;
pub const KIND_STUB: u8 = 0;
pub const KIND_SP1_GROTH16: u8 = 1;
pub const HEADER_LEN: usize = 8;
pub const KEY_ID_LEN: usize = 32;
pub const NONCE_LEN: usize = 32;
pub const PROOF_LEN: usize = 256;
/// The two public-values lengths the aggregator emits (agg.rs `BlockOutput::LEN` and `LEN2`).
pub const PV_LEN_PLAIN: usize = 340;
pub const PV_LEN_FIN: usize = 504;
/// The object's length with the finality extension: 832.
pub const LEN_FIN: usize = HEADER_LEN + KEY_ID_LEN + PV_LEN_FIN + NONCE_LEN + PROOF_LEN;
pub const LEN_PLAIN: usize = HEADER_LEN + KEY_ID_LEN + PV_LEN_PLAIN + NONCE_LEN + PROOF_LEN;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FinalityObject {
pub kind: u8,
pub key_id: [u8; 32],
pub public_values: Vec<u8>,
pub nonce: [u8; 32],
pub proof: [u8; 256],
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ObjectError {
TooShort(usize),
BadMagic,
BadVersion(u8),
BadKind(u8),
BadPublicValuesLength(usize),
BadLength { got: usize, want: usize },
}
impl ObjectError {
pub fn to_string(&self) -> String {
match self {
ObjectError::TooShort(n) => alloc::format!("object is {n} bytes, shorter than the header"),
ObjectError::BadMagic => String::from("not a finality object (magic)"),
ObjectError::BadVersion(v) => alloc::format!("object version {v} is not 1"),
ObjectError::BadKind(k) => alloc::format!("object kind {k} is not 0 (stub) or 1 (SP1 Groth16)"),
ObjectError::BadPublicValuesLength(n) => alloc::format!("public values length {n} is not 340 or 504"),
ObjectError::BadLength { got, want } => alloc::format!("object is {got} bytes, the header says {want}"),
}
}
}
impl FinalityObject {
pub fn len(&self) -> usize {
HEADER_LEN + KEY_ID_LEN + self.public_values.len() + NONCE_LEN + PROOF_LEN
}
pub fn encode(&self) -> Vec<u8> {
let mut v = Vec::with_capacity(self.len());
v.extend_from_slice(&MAGIC);
v.push(VERSION);
v.push(self.kind);
v.extend_from_slice(&(self.public_values.len() as u16).to_be_bytes());
v.extend_from_slice(&self.key_id);
v.extend_from_slice(&self.public_values);
v.extend_from_slice(&self.nonce);
v.extend_from_slice(&self.proof);
v
}
pub fn decode(b: &[u8]) -> Result<Self, ObjectError> {
if b.len() < HEADER_LEN {
return Err(ObjectError::TooShort(b.len()));
}
if b[0..4] != MAGIC {
return Err(ObjectError::BadMagic);
}
if b[4] != VERSION {
return Err(ObjectError::BadVersion(b[4]));
}
let kind = b[5];
if kind != KIND_STUB && kind != KIND_SP1_GROTH16 {
return Err(ObjectError::BadKind(kind));
}
let n = u16::from_be_bytes([b[6], b[7]]) as usize;
if n != PV_LEN_PLAIN && n != PV_LEN_FIN {
return Err(ObjectError::BadPublicValuesLength(n));
}
let want = HEADER_LEN + KEY_ID_LEN + n + NONCE_LEN + PROOF_LEN;
if b.len() != want {
return Err(ObjectError::BadLength { got: b.len(), want });
}
let mut key_id = [0u8; 32];
key_id.copy_from_slice(&b[8..40]);
let public_values = b[40..40 + n].to_vec();
let mut nonce = [0u8; 32];
nonce.copy_from_slice(&b[40 + n..72 + n]);
let mut proof = [0u8; 256];
proof.copy_from_slice(&b[72 + n..72 + n + 256]);
Ok(Self { kind, key_id, public_values, nonce, proof })
}
/// The stub object the node serves until the wrap lands: the public values the chain carried, the wrap bytes
/// zero, the kind marked.
pub fn stub(key_id: [u8; 32], public_values: Vec<u8>) -> Self {
Self { kind: KIND_STUB, key_id, public_values, nonce: [0u8; 32], proof: [0u8; 256] }
}
}
/// The URL form: the object's bytes in base64url with no padding, in a fragment so no server sees them.
pub fn base64url_encode(b: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut s = String::with_capacity((b.len() + 2) / 3 * 4);
for chunk in b.chunks(3) {
let n = chunk.len();
let v = (chunk[0] as u32) << 16 | (if n > 1 { chunk[1] as u32 } else { 0 }) << 8 | (if n > 2 { chunk[2] as u32 } else { 0 });
s.push(T[(v >> 18) as usize & 63] as char);
s.push(T[(v >> 12) as usize & 63] as char);
if n > 1 {
s.push(T[(v >> 6) as usize & 63] as char);
}
if n > 2 {
s.push(T[v as usize & 63] as char);
}
}
s
}
pub fn base64url_decode(s: &str) -> Option<Vec<u8>> {
let val = |c: u8| -> Option<u32> {
Some(match c {
b'A'..=b'Z' => (c - b'A') as u32,
b'a'..=b'z' => (c - b'a') as u32 + 26,
b'0'..=b'9' => (c - b'0') as u32 + 52,
b'-' => 62,
b'_' => 63,
_ => return None,
})
};
let bytes = s.trim_end_matches('=').as_bytes();
let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
for chunk in bytes.chunks(4) {
let n = chunk.len();
if n == 1 {
return None;
}
let mut v = 0u32;
for (i, c) in chunk.iter().enumerate() {
v |= val(*c)? << (18 - 6 * i);
}
out.push((v >> 16) as u8);
if n > 2 {
out.push((v >> 8) as u8);
}
if n > 3 {
out.push(v as u8);
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_and_lengths() {
let o = FinalityObject::stub([7u8; 32], alloc::vec![1u8; PV_LEN_FIN]);
let b = o.encode();
assert_eq!(b.len(), LEN_FIN);
assert_eq!(LEN_FIN, 832);
assert_eq!(LEN_PLAIN, 668);
assert_eq!(FinalityObject::decode(&b).unwrap(), o);
let p = FinalityObject::stub([7u8; 32], alloc::vec![1u8; PV_LEN_PLAIN]);
assert_eq!(FinalityObject::decode(&p.encode()).unwrap(), p);
}
#[test]
fn every_header_byte_is_checked() {
let o = FinalityObject::stub([7u8; 32], alloc::vec![1u8; PV_LEN_FIN]).encode();
let mut b = o.clone();
b[0] = b'X';
assert_eq!(FinalityObject::decode(&b), Err(ObjectError::BadMagic));
let mut b = o.clone();
b[4] = 2;
assert_eq!(FinalityObject::decode(&b), Err(ObjectError::BadVersion(2)));
let mut b = o.clone();
b[5] = 9;
assert_eq!(FinalityObject::decode(&b), Err(ObjectError::BadKind(9)));
let mut b = o.clone();
b[7] = 0xf7;
assert_eq!(FinalityObject::decode(&b), Err(ObjectError::BadPublicValuesLength(0x01f7)));
let mut b = o.clone();
b.push(0);
assert_eq!(FinalityObject::decode(&b), Err(ObjectError::BadLength { got: 833, want: 832 }));
assert_eq!(FinalityObject::decode(&b[..5]), Err(ObjectError::TooShort(5)));
}
#[test]
fn base64url_round_trip() {
for n in 0..70usize {
let v: Vec<u8> = (0..n).map(|i| (i * 37 + 11) as u8).collect();
let s = base64url_encode(&v);
assert!(!s.contains('='));
assert_eq!(base64url_decode(&s).unwrap(), v, "{n}");
}
assert_eq!(base64url_decode("A"), None);
assert_eq!(base64url_decode("A!"), None);
}
}

View file

@ -0,0 +1,137 @@
//! The aggregator's public values: `BlockOutput` (340 bytes) and the finality extension `FinExt` (164 bytes), the
//! byte order of `BlockOutput::to_bytes` and `FinExt::to_bytes`, every integer big-endian.
use alloc::vec::Vec;
pub const BLOCK_STATEMENT_LEN: usize = 340;
pub const FIN_EXT_LEN: usize = 164;
pub const FLAG_STALE: u16 = 1;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Lock {
pub index: u64,
pub hash: [u8; 32],
pub number: u64,
pub signed: u64,
pub total: u64,
pub frozen_signed: u64,
pub frozen_total: u64,
pub daa: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FinExt {
pub fin_version: u16,
pub table_root: [u8; 32],
pub history_root: [u8; 32],
pub history_first: u64,
pub lock: Lock,
pub flags: u16,
}
impl FinExt {
pub fn stale(&self) -> bool {
self.flags & FLAG_STALE != 0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Statement {
pub chain_id: u64,
pub number: u64,
pub block_hash: [u8; 32],
pub parent_hash: [u8; 32],
pub post_root: [u8; 32],
pub shard_vk: [u8; 32],
pub agg_vk: [u8; 32],
pub chain_len: u64,
pub fin: Option<FinExt>,
}
fn u64_at(b: &[u8], i: usize) -> u64 {
u64::from_be_bytes(b[i..i + 8].try_into().unwrap())
}
fn u16_at(b: &[u8], i: usize) -> u16 {
u16::from_be_bytes([b[i], b[i + 1]])
}
fn h_at(b: &[u8], i: usize) -> [u8; 32] {
b[i..i + 32].try_into().unwrap()
}
impl Statement {
/// `None` unless the bytes are exactly 340 or 504 long and, at 504, carry `fin_version` 1.
pub fn parse(b: &[u8]) -> Option<Self> {
let fin = match b.len() {
BLOCK_STATEMENT_LEN => None,
n if n == BLOCK_STATEMENT_LEN + FIN_EXT_LEN => {
let e = &b[BLOCK_STATEMENT_LEN..];
let fin_version = u16_at(e, 0);
if fin_version != 1 {
return None;
}
Some(FinExt {
fin_version,
table_root: h_at(e, 2),
history_root: h_at(e, 34),
history_first: u64_at(e, 66),
lock: Lock {
index: u64_at(e, 74),
hash: h_at(e, 82),
number: u64_at(e, 114),
signed: u64_at(e, 122),
total: u64_at(e, 130),
frozen_signed: u64_at(e, 138),
frozen_total: u64_at(e, 146),
daa: u64_at(e, 154),
},
flags: u16_at(e, 162),
})
}
_ => return None,
};
let st = Self {
chain_id: u64_at(b, 0),
number: u64_at(b, 8),
block_hash: h_at(b, 16),
parent_hash: h_at(b, 48),
post_root: h_at(b, 148),
shard_vk: h_at(b, 268),
agg_vk: h_at(b, 300),
chain_len: u64_at(b, 332),
fin,
};
if let Some(f) = &st.fin {
// the history counts from history_first to this block; a first above the block is malformed
if f.history_first > st.number {
return None;
}
}
Some(st)
}
/// The history's leaf count: one per chain block from `history_first` to `number`.
pub fn history_leaves(&self) -> Option<u64> {
self.fin.as_ref().map(|f| self.number - f.history_first + 1)
}
pub fn to_bytes_len(&self) -> usize {
if self.fin.is_some() { BLOCK_STATEMENT_LEN + FIN_EXT_LEN } else { BLOCK_STATEMENT_LEN }
}
#[allow(clippy::wrong_self_convention)]
pub fn describe(&self) -> Vec<(&'static str, alloc::string::String)> {
let mut v = alloc::vec![
("chain_id", alloc::format!("{}", self.chain_id)),
("number", alloc::format!("{}", self.number)),
("chain_len", alloc::format!("{}", self.chain_len)),
];
if let Some(f) = &self.fin {
v.push(("lock_index", alloc::format!("{}", f.lock.index)));
v.push(("lock_number", alloc::format!("{}", f.lock.number)));
v.push(("lock_signed", alloc::format!("{}", f.lock.signed)));
v.push(("lock_total", alloc::format!("{}", f.lock.total)));
v.push(("stale", alloc::format!("{}", f.stale())));
}
v
}
}

View file

@ -0,0 +1,89 @@
//! The two pairing paths agree on the one fixture the repository can make before the wrap lands: generator points
//! that load and prove nothing (refused by both), and every refusal that must come before the pairing.
use crate::object::PV_LEN_FIN;
use crate::*;
use alloc::vec;
fn pv(chain_id: u64, key: [u8; 32]) -> Vec<u8> {
let mut v = vec![0u8; PV_LEN_FIN];
v[0..8].copy_from_slice(&chain_id.to_be_bytes());
v[8..16].copy_from_slice(&81053u64.to_be_bytes());
v[300..332].copy_from_slice(&key);
v[332..340].copy_from_slice(&8u64.to_be_bytes());
v[340..342].copy_from_slice(&1u16.to_be_bytes());
v[406..414].copy_from_slice(&79046u64.to_be_bytes());
v[414..422].copy_from_slice(&2702u64.to_be_bytes());
v[454..462].copy_from_slice(&81049u64.to_be_bytes());
v
}
/// bn254's generators in gnark's uncompressed big-endian order (x1 ‖ x0 ‖ y1 ‖ y0 for G2), from EIP-197.
fn generator_proof() -> [u8; 256] {
fn be(dec: &str) -> [u8; 32] {
// decimal to 32-byte big-endian without a bignum crate
let mut out = [0u8; 32];
for d in dec.bytes() {
let mut carry = (d - b'0') as u32;
for b in out.iter_mut().rev() {
let v = (*b as u32) * 10 + carry;
*b = (v & 0xff) as u8;
carry = v >> 8;
}
}
out
}
let mut p = [0u8; 256];
p[0..32].copy_from_slice(&be("1"));
p[32..64].copy_from_slice(&be("2"));
p[64..96].copy_from_slice(&be("11559732032986387107991004021392285783925812861821192530917403151452391805634"));
p[96..128].copy_from_slice(&be("10857046999023057135944570762232829481370756359578518086990519993285655852781"));
p[128..160].copy_from_slice(&be("4082367875863433681332203403145435568316851327593401208105741076214120093531"));
p[160..192].copy_from_slice(&be("8495653923123431417604973247489272438418190587263600148770280649306958101930"));
p[192..224].copy_from_slice(&be("1"));
p[224..256].copy_from_slice(&be("2"));
p
}
#[test]
fn both_pairing_paths_refuse_the_generator_proof_and_agree() {
let key = [0x12u8; 32];
let obj = FinalityObject { kind: KIND_SP1_GROTH16, key_id: key, public_values: pv(4463, key), nonce: [0u8; 32], proof: generator_proof() };
let v = Verifier::new();
assert!(v.verify_wrap(&obj).is_err(), "arkworks must refuse generator points");
assert!(verify_sp1_groth16_reference(&obj).is_err(), "substrate-bn must refuse generator points");
assert!(verify_sp1_groth16_reference_one_pairing(&obj).is_err());
let verdict = v.verify(&obj.encode(), None, &Pinned { key_id: key, chain_id: 4463 }, None);
assert!(matches!(verdict.proof, ProofVerdict::Refused(_)), "{verdict:?}");
assert_eq!(verdict.answer, None);
// a point off the curve is refused before any pairing
let mut off = obj.clone();
off.proof[1] ^= 1;
let why = v.verify_wrap(&off).unwrap_err();
assert!(why.contains("curve points"), "{why}");
}
#[test]
fn refusals_before_the_pairing_and_the_stub_answers() {
let key = [0x12u8; 32];
let pinned = Pinned { key_id: key, chain_id: 4463 };
let v = Verifier::new();
let stub = FinalityObject::stub(key, pv(4463, key));
let r = v.verify(&stub.encode(), None, &pinned, None);
assert_eq!(r.proof, ProofVerdict::Stub);
assert_eq!(r.answer, Some(Answer::NotFinal), "81053 lies above the lock at 81049");
assert_eq!(r.trust_row, TRUST_ROW_STUB);
let other = FinalityObject::stub(key, pv(4464, key));
assert!(matches!(v.verify(&other.encode(), None, &pinned, None).proof, ProofVerdict::Refused(ref w) if w.contains("chain id")));
let wrong_key = Pinned { key_id: [9u8; 32], chain_id: 4463 };
assert!(matches!(v.verify(&stub.encode(), None, &wrong_key, None).proof, ProofVerdict::Refused(ref w) if w.contains("pinned aggregator")));
let mut plain = FinalityObject::stub(key, pv(4463, key));
plain.public_values.truncate(340);
assert_eq!(v.verify(&plain.encode(), None, &pinned, None).answer, Some(Answer::NoClaim));
let mut stale = FinalityObject::stub(key, pv(4463, key));
stale.public_values[503] = 1;
assert_eq!(v.verify(&stale.encode(), None, &pinned, None).answer, Some(Answer::Stale));
let clock = Clock { now_unix_s: 1_000_000, genesis_unix_s: 800_000, seconds_per_daa: 1.0 };
let aged = v.verify(&stub.encode(), None, &pinned, Some(clock));
assert_eq!(aged.age_s, Some(200_000), "lock daa 0 in this synthetic statement");
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,15 @@
{"format":"igneum-finality-object-vectors-1","object_version":1,"sp1_version":"6.8.1","key_id":"12bff5be5699cc5e2b677d837559517033870903248c2761726523096d5f3822","chain_id":4463,"vectors":[
{"name":"fixture-81053-stub","bytes":832,"sha256":"5174955671d86375ae7f96417673ca55dbc77498a4e825a585f03b72a0eab371","query":null,"expect":{"proof":"stub","answer":"not final"},"verdict":{"proof":"stub","reason":null,"answer":"not final","block_number":81053,"age_s":null,"trust_row":"wrap pending: the public values are read, nothing is proven","statement":{"chain_id":4463,"number":81053,"chain_len":8,"block_hash":"040acfe3dc3ca820cfe2565433d200cf9b2399ed3bcd1e04598fc05bcb1379c5","post_root":"be1ac38932436b515a6ef939e28dc50b4299c26094563fffb3e3fc710439c874","agg_vk":"12bff5be5699cc5e2b677d837559517033870903248c2761726523096d5f3822","fin":{"history_first":79046,"stale":false,"history_root":"71ea9db64404391ea74cd595c0395d0cc82b36620140f0b5b5c41bf4e39d2d33","table_root":"10c2458b19e20cbbbf68c4bfc01d7576201f7cd26ee916f393a720812e1b577d","lock":{"index":2702,"number":81049,"signed":2808,"total":4008,"frozen_signed":0,"frozen_total":0,"daa":100003,"hash":"dba699a61f7e4b3b8356a7494afbb73237f00385d7ad279a77002bfd451b2643"}}}}},
{"name":"fixture-81053-timing","bytes":832,"sha256":"40ee84e88112a25ef796dbc693495896a9e92ccad50ebda4c17ac248db9b0854","query":null,"expect":{"proof":"refused","answer":null},"verdict":{"proof":"refused","reason":"the Groth16 wrap does not verify under the pinned key","answer":null,"block_number":null,"age_s":null,"trust_row":"voter set: verified in the proof; blue set: from the prover, vetoed by full nodes (level 0)","statement":null}},
{"name":"fixture-81053-stub-flipped-lock-number","bytes":832,"sha256":"d7adc96d66690fcf8b8eaf311ce3b7929fda50e1c122b5a035e6c6f005234f2a","query":null,"expect":{"proof":"stub","answer":"not final"},"verdict":{"proof":"stub","reason":null,"answer":"not final","block_number":81053,"age_s":null,"trust_row":"wrap pending: the public values are read, nothing is proven","statement":{"chain_id":4463,"number":81053,"chain_len":8,"block_hash":"040acfe3dc3ca820cfe2565433d200cf9b2399ed3bcd1e04598fc05bcb1379c5","post_root":"be1ac38932436b515a6ef939e28dc50b4299c26094563fffb3e3fc710439c874","agg_vk":"12bff5be5699cc5e2b677d837559517033870903248c2761726523096d5f3822","fin":{"history_first":79046,"stale":false,"history_root":"71ea9db64404391ea74cd595c0395d0cc82b36620140f0b5b5c41bf4e39d2d33","table_root":"10c2458b19e20cbbbf68c4bfc01d7576201f7cd26ee916f393a720812e1b577d","lock":{"index":2702,"number":81048,"signed":2808,"total":4008,"frozen_signed":0,"frozen_total":0,"daa":100003,"hash":"dba699a61f7e4b3b8356a7494afbb73237f00385d7ad279a77002bfd451b2643"}}}}},
{"name":"fixture-81053-timing-flipped-lock-number","bytes":832,"sha256":"0a8242bddd23f2bf8834aa175bcff8e3906612c558a3b8357dd1b3658f88c0e8","query":null,"expect":{"proof":"refused","answer":null},"verdict":{"proof":"refused","reason":"the Groth16 wrap does not verify under the pinned key","answer":null,"block_number":null,"age_s":null,"trust_row":"voter set: verified in the proof; blue set: from the prover, vetoed by full nodes (level 0)","statement":null}},
{"name":"fixture-81053-timing-flipped-proof","bytes":832,"sha256":"439c74ffe46ac8285c8916d91ba026f7a460a15fc1b3624be3b1cff6c27ca176","query":null,"expect":{"proof":"refused","answer":null},"verdict":{"proof":"refused","reason":"the wrap's curve points do not load: G1CompressionError","answer":null,"block_number":null,"age_s":null,"trust_row":"voter set: verified in the proof; blue set: from the prover, vetoed by full nodes (level 0)","statement":null}},
{"name":"fixture-81053-stub-wrong-key","bytes":832,"sha256":"8def8ed165ce36e377bbdde684710433ea98444748b4de0e452e4374345e8ffd","query":null,"expect":{"proof":"refused","answer":null},"verdict":{"proof":"refused","reason":"key id is not the pinned aggregator id","answer":null,"block_number":null,"age_s":null,"trust_row":"voter set: verified in the proof; blue set: from the prover, vetoed by full nodes (level 0)","statement":null}},
{"name":"fixture-81053-stub-version-2","bytes":832,"sha256":"8d297ff5fdbe60249062a72fe456e1be70745fe01a70e2298b54867fe517fd52","query":null,"expect":{"proof":"refused","answer":null},"verdict":{"proof":"refused","reason":"object version 2 is not 1","answer":null,"block_number":null,"age_s":null,"trust_row":"voter set: verified in the proof; blue set: from the prover, vetoed by full nodes (level 0)","statement":null}},
{"name":"synthetic-300-block-79146-final","bytes":832,"sha256":"0bc7a808c5a2447e679c2d26b048f18d646911c8c57e75d256058f59ea81b19b","query":{"bytes":532,"sha256":"56a131b1756b004e5db771753960d71afc5a29317236c803aa9af6f366c1e4f7"},"expect":{"proof":"stub","answer":"final"},"verdict":{"proof":"stub","reason":null,"answer":"final","block_number":79146,"age_s":null,"trust_row":"wrap pending: the public values are read, nothing is proven","statement":{"chain_id":4463,"number":79345,"chain_len":300,"block_hash":"e69504449c14a7d1927d745631b42ef667a083bc1d9b1748b616f7e36c52ee56","post_root":"5ad709f448c45ab7524f09206de6a2fbc35fbcacd5044516e773069debc81648","agg_vk":"12bff5be5699cc5e2b677d837559517033870903248c2761726523096d5f3822","fin":{"history_first":79046,"stale":false,"history_root":"8ae98c7320e592c33ae8658197836eed62bcc50d8e99f1bf3f8b2a5ac2327af5","table_root":"a23728c84a7d1cb5593d16f40c301752db2032ca6b62f797ec31d943a4c3cc45","lock":{"index":2702,"number":79300,"signed":2808,"total":4008,"frozen_signed":2808,"frozen_total":4008,"daa":100762,"hash":"97f6c6d7e0d0890710a027fb994397582409b61559753cb46adfab318b448b18"}}}}},
{"name":"synthetic-300-block-79345-not-final","bytes":832,"sha256":"0bc7a808c5a2447e679c2d26b048f18d646911c8c57e75d256058f59ea81b19b","query":{"bytes":334,"sha256":"f89d1b1f0713cb2c741292281dfa4866c239f9d226b42571c0eda6fc8407e266"},"expect":{"proof":"stub","answer":"not final"},"verdict":{"proof":"stub","reason":null,"answer":"not final","block_number":79345,"age_s":null,"trust_row":"wrap pending: the public values are read, nothing is proven","statement":{"chain_id":4463,"number":79345,"chain_len":300,"block_hash":"e69504449c14a7d1927d745631b42ef667a083bc1d9b1748b616f7e36c52ee56","post_root":"5ad709f448c45ab7524f09206de6a2fbc35fbcacd5044516e773069debc81648","agg_vk":"12bff5be5699cc5e2b677d837559517033870903248c2761726523096d5f3822","fin":{"history_first":79046,"stale":false,"history_root":"8ae98c7320e592c33ae8658197836eed62bcc50d8e99f1bf3f8b2a5ac2327af5","table_root":"a23728c84a7d1cb5593d16f40c301752db2032ca6b62f797ec31d943a4c3cc45","lock":{"index":2702,"number":79300,"signed":2808,"total":4008,"frozen_signed":2808,"frozen_total":4008,"daa":100762,"hash":"97f6c6d7e0d0890710a027fb994397582409b61559753cb46adfab318b448b18"}}}}},
{"name":"synthetic-300-block-79146-wrong-leaf-not-in-chain","bytes":832,"sha256":"0bc7a808c5a2447e679c2d26b048f18d646911c8c57e75d256058f59ea81b19b","query":{"bytes":532,"sha256":"fded005f2fc69c85217fd389ea1e4252c6bff9fc097e647e1ce28940a3c175a0"},"expect":{"proof":"stub","answer":"not in this chain"},"verdict":{"proof":"stub","reason":null,"answer":"not in this chain","block_number":79146,"age_s":null,"trust_row":"wrap pending: the public values are read, nothing is proven","statement":{"chain_id":4463,"number":79345,"chain_len":300,"block_hash":"e69504449c14a7d1927d745631b42ef667a083bc1d9b1748b616f7e36c52ee56","post_root":"5ad709f448c45ab7524f09206de6a2fbc35fbcacd5044516e773069debc81648","agg_vk":"12bff5be5699cc5e2b677d837559517033870903248c2761726523096d5f3822","fin":{"history_first":79046,"stale":false,"history_root":"8ae98c7320e592c33ae8658197836eed62bcc50d8e99f1bf3f8b2a5ac2327af5","table_root":"a23728c84a7d1cb5593d16f40c301752db2032ca6b62f797ec31d943a4c3cc45","lock":{"index":2702,"number":79300,"signed":2808,"total":4008,"frozen_signed":2808,"frozen_total":4008,"daa":100762,"hash":"97f6c6d7e0d0890710a027fb994397582409b61559753cb46adfab318b448b18"}}}}},
{"name":"synthetic-300-stale","bytes":832,"sha256":"2887b2b7dc2f21f5d780c8747eaae495f7af4a8507a76eff5c4d0cf2284ad468","query":null,"expect":{"proof":"stub","answer":"stale"},"verdict":{"proof":"stub","reason":null,"answer":"stale","block_number":79345,"age_s":null,"trust_row":"wrap pending: the public values are read, nothing is proven","statement":{"chain_id":4463,"number":79345,"chain_len":300,"block_hash":"e69504449c14a7d1927d745631b42ef667a083bc1d9b1748b616f7e36c52ee56","post_root":"5ad709f448c45ab7524f09206de6a2fbc35fbcacd5044516e773069debc81648","agg_vk":"12bff5be5699cc5e2b677d837559517033870903248c2761726523096d5f3822","fin":{"history_first":79046,"stale":true,"history_root":"8ae98c7320e592c33ae8658197836eed62bcc50d8e99f1bf3f8b2a5ac2327af5","table_root":"a23728c84a7d1cb5593d16f40c301752db2032ca6b62f797ec31d943a4c3cc45","lock":{"index":2702,"number":79300,"signed":2808,"total":4008,"frozen_signed":2808,"frozen_total":4008,"daa":100762,"hash":"97f6c6d7e0d0890710a027fb994397582409b61559753cb46adfab318b448b18"}}}}},
{"name":"synthetic-300-other-chain","bytes":832,"sha256":"70d94f4573ae9e8d186c6adb18e93df44718e42f0d2dba1c46f848f3f070a9d4","query":null,"expect":{"proof":"refused","answer":null},"verdict":{"proof":"refused","reason":"chain id 4464 is not the pinned chain 4463","answer":null,"block_number":null,"age_s":null,"trust_row":"voter set: verified in the proof; blue set: from the prover, vetoed by full nodes (level 0)","statement":null}},
{"name":"synthetic-300-timing","bytes":832,"sha256":"65fce1a7c0113208806e9543c4a1ca0d40c8d31380a4d589564adc658d0d71f6","query":{"bytes":532,"sha256":"56a131b1756b004e5db771753960d71afc5a29317236c803aa9af6f366c1e4f7"},"expect":{"proof":"refused","answer":null},"verdict":{"proof":"refused","reason":"the Groth16 wrap does not verify under the pinned key","answer":null,"block_number":null,"age_s":null,"trust_row":"voter set: verified in the proof; blue set: from the prover, vetoed by full nodes (level 0)","statement":null}}
]}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.