From 59d2177fd363bc7c0746262c03ad7efe2525f3f2 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:02:52 +0000 Subject: [PATCH] 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 --- light/.gitignore | 2 + light/Cargo.lock | 880 ++++++++++++++++++ light/Cargo.toml | 25 + light/igneum-light-cli/Cargo.toml | 15 + light/igneum-light-cli/src/main.rs | 284 ++++++ light/igneum-light-wasm/Cargo.toml | 12 + light/igneum-light-wasm/src/lib.rs | 98 ++ light/igneum-light/Cargo.toml | 20 + light/igneum-light/src/json.rs | 87 ++ light/igneum-light/src/lib.rs | 250 +++++ light/igneum-light/src/mmr.rs | 293 ++++++ light/igneum-light/src/object.rs | 225 +++++ light/igneum-light/src/statement.rs | 137 +++ light/igneum-light/src/tests.rs | 89 ++ ...fixture-81053-stub-flipped-lock-number.bin | Bin 0 -> 832 bytes .../vectors/fixture-81053-stub-version-2.bin | Bin 0 -> 832 bytes .../vectors/fixture-81053-stub-wrong-key.bin | Bin 0 -> 832 bytes light/vectors/fixture-81053-stub.bin | Bin 0 -> 832 bytes ...xture-81053-timing-flipped-lock-number.bin | Bin 0 -> 832 bytes .../fixture-81053-timing-flipped-proof.bin | Bin 0 -> 832 bytes light/vectors/fixture-81053-timing.bin | Bin 0 -> 832 bytes light/vectors/manifest.json | 15 + light/vectors/public-values-81053.bin | Bin 0 -> 504 bytes .../synthetic-300-block-79146-final.bin | Bin 0 -> 832 bytes .../synthetic-300-block-79146-final.query.bin | Bin 0 -> 532 bytes ...00-block-79146-wrong-leaf-not-in-chain.bin | Bin 0 -> 832 bytes ...ck-79146-wrong-leaf-not-in-chain.query.bin | Bin 0 -> 532 bytes .../synthetic-300-block-79345-not-final.bin | Bin 0 -> 832 bytes ...thetic-300-block-79345-not-final.query.bin | Bin 0 -> 334 bytes light/vectors/synthetic-300-other-chain.bin | Bin 0 -> 832 bytes light/vectors/synthetic-300-stale.bin | Bin 0 -> 832 bytes light/vectors/synthetic-300-timing.bin | Bin 0 -> 832 bytes light/vectors/synthetic-300-timing.query.bin | Bin 0 -> 532 bytes 33 files changed, 2432 insertions(+) create mode 100644 light/.gitignore create mode 100644 light/Cargo.lock create mode 100644 light/Cargo.toml create mode 100644 light/igneum-light-cli/Cargo.toml create mode 100644 light/igneum-light-cli/src/main.rs create mode 100644 light/igneum-light-wasm/Cargo.toml create mode 100644 light/igneum-light-wasm/src/lib.rs create mode 100644 light/igneum-light/Cargo.toml create mode 100644 light/igneum-light/src/json.rs create mode 100644 light/igneum-light/src/lib.rs create mode 100644 light/igneum-light/src/mmr.rs create mode 100644 light/igneum-light/src/object.rs create mode 100644 light/igneum-light/src/statement.rs create mode 100644 light/igneum-light/src/tests.rs create mode 100644 light/vectors/fixture-81053-stub-flipped-lock-number.bin create mode 100644 light/vectors/fixture-81053-stub-version-2.bin create mode 100644 light/vectors/fixture-81053-stub-wrong-key.bin create mode 100644 light/vectors/fixture-81053-stub.bin create mode 100644 light/vectors/fixture-81053-timing-flipped-lock-number.bin create mode 100644 light/vectors/fixture-81053-timing-flipped-proof.bin create mode 100644 light/vectors/fixture-81053-timing.bin create mode 100644 light/vectors/manifest.json create mode 100644 light/vectors/public-values-81053.bin create mode 100644 light/vectors/synthetic-300-block-79146-final.bin create mode 100644 light/vectors/synthetic-300-block-79146-final.query.bin create mode 100644 light/vectors/synthetic-300-block-79146-wrong-leaf-not-in-chain.bin create mode 100644 light/vectors/synthetic-300-block-79146-wrong-leaf-not-in-chain.query.bin create mode 100644 light/vectors/synthetic-300-block-79345-not-final.bin create mode 100644 light/vectors/synthetic-300-block-79345-not-final.query.bin create mode 100644 light/vectors/synthetic-300-other-chain.bin create mode 100644 light/vectors/synthetic-300-stale.bin create mode 100644 light/vectors/synthetic-300-timing.bin create mode 100644 light/vectors/synthetic-300-timing.query.bin diff --git a/light/.gitignore b/light/.gitignore new file mode 100644 index 00000000..386d8fa2 --- /dev/null +++ b/light/.gitignore @@ -0,0 +1,2 @@ +target/ +*/target-remote/ diff --git a/light/Cargo.lock b/light/Cargo.lock new file mode 100644 index 00000000..d65d5c12 --- /dev/null +++ b/light/Cargo.lock @@ -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", +] diff --git a/light/Cargo.toml b/light/Cargo.toml new file mode 100644 index 00000000..cc906d3b --- /dev/null +++ b/light/Cargo.toml @@ -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 diff --git a/light/igneum-light-cli/Cargo.toml b/light/igneum-light-cli/Cargo.toml new file mode 100644 index 00000000..ffcfdeb3 --- /dev/null +++ b/light/igneum-light-cli/Cargo.toml @@ -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 } diff --git a/light/igneum-light-cli/src/main.rs b/light/igneum-light-cli/src/main.rs new file mode 100644 index 00000000..7fed107c --- /dev/null +++ b/light/igneum-light-cli/src/main.rs @@ -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 --out +//! igneum-light make-timing --pv --key-id --out kind 1, curve points that are on the +//! curve and prove nothing: the pairing runs +//! igneum-light verify --object [--query ] --key-id --chain-id [--genesis ] +//! igneum-light time --object --key-id --chain-id [--rounds 20] +//! igneum-light url --object [--query ] [--base https://igneum.network/pocket/] +//! igneum-light vectors --pv --key-id --out-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) { + let mut it = std::env::args().skip(1); + let cmd = it.next().unwrap_or_default(); + let mut m = HashMap::new(); + let mut key: Option = 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::>(); + 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) -> FinalityObject { + FinalityObject { kind: KIND_SP1_GROTH16, key_id, public_values: pv, nonce: [0u8; 32], proof: timing_proof() } +} + +fn read(p: &str) -> Vec { + 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, Vec, 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| { + 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| { + 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); + } + } +} diff --git a/light/igneum-light-wasm/Cargo.toml b/light/igneum-light-wasm/Cargo.toml new file mode 100644 index 00000000..ef96bd9d --- /dev/null +++ b/light/igneum-light-wasm/Cargo.toml @@ -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" } diff --git a/light/igneum-light-wasm/src/lib.rs b/light/igneum-light-wasm/src/lib.rs new file mode 100644 index 00000000..9cebe230 --- /dev/null +++ b/light/igneum-light-wasm/src/lib.rs @@ -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 = 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::::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) +} diff --git a/light/igneum-light/Cargo.toml b/light/igneum-light/Cargo.toml new file mode 100644 index 00000000..5af42b3d --- /dev/null +++ b/light/igneum-light/Cargo.toml @@ -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 = [] diff --git a/light/igneum-light/src/json.rs b/light/igneum-light/src/json.rs new file mode 100644 index 00000000..b38b52e5 --- /dev/null +++ b/light/igneum-light/src/json.rs @@ -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 +} diff --git a/light/igneum-light/src/lib.rs b/light/igneum-light/src/lib.rs new file mode 100644 index 00000000..6004c2a4 --- /dev/null +++ b/light/igneum-light/src/lib.rs @@ -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, + /// The block the answer is about: the query's leaf, or the proof's own last block. + pub block_number: Option, + pub statement: Option, + /// 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, + 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, +} + +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) -> 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::::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) -> 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 { + 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() +} diff --git a/light/igneum-light/src/mmr.rs b/light/igneum-light/src/mmr.rs new file mode 100644 index 00000000..ab145305 --- /dev/null +++ b/light/igneum-light/src/mmr.rs @@ -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 = 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 { + 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 { + 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)); + } + } + } +} diff --git a/light/igneum-light/src/object.rs b/light/igneum-light/src/object.rs new file mode 100644 index 00000000..e95e9686 --- /dev/null +++ b/light/igneum-light/src/object.rs @@ -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, + 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 { + 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 { + 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) -> 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> { + let val = |c: u8| -> Option { + 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 = (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); + } +} diff --git a/light/igneum-light/src/statement.rs b/light/igneum-light/src/statement.rs new file mode 100644 index 00000000..1977c2d0 --- /dev/null +++ b/light/igneum-light/src/statement.rs @@ -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, +} + +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 { + 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 { + 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 + } +} diff --git a/light/igneum-light/src/tests.rs b/light/igneum-light/src/tests.rs new file mode 100644 index 00000000..f591cc1f --- /dev/null +++ b/light/igneum-light/src/tests.rs @@ -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 { + 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"); +} diff --git a/light/vectors/fixture-81053-stub-flipped-lock-number.bin b/light/vectors/fixture-81053-stub-flipped-lock-number.bin new file mode 100644 index 0000000000000000000000000000000000000000..a5d84a017b647564aa0f712da377bb6f580db024 GIT binary patch literal 832 zcmeZtck^dtVEiGp|LeZ6nP=j((`%bcBLfSJ+c}w4dejq(Qk6M#<1LgJz(6n`OflNb zW#Kyi_>RpAh4YWXLX0mloS&^c^R4w+IhM%&1JS32E00b#oU_1kRy4!&>b(!ov(?D^ zH`(m_ze$&T>HL+((aoPN0*@3!;cC&t^X-R+BpSkO~+N%EBA;KJcuWYm4 zlOP_iZh3p|G(D%R>FycxO!5=@1esO4Nbt#c<}&#@Z|ml;<(^)@w;P#%U~av>R()1EgZ5unX*Fkv bdYc(g^*RpAh4YWXLX0mloS&^c^R4w+IhM%&1JS32E00b#oU_1kRy4!&>b(!ov(?D^ zH`(m_ze$&T>HL+((aoPN0*@3!;cC&t^X-R+BpSkO~+N%EBA;KJcuWYm4 zlOP_iZh3p|G(D%R>FycxO!5=@1esO4Nbt#c<}&#@Z|ml;<(^)@w;P#%U~av>R()1EgZ5unX*Fkv bdYhS0^*RpAh4YWXLX0mloS&^c^R4w+IhM%&1JS32E00b#oU_1kRy4!&>b(!ov(?D^ zH`(m_ze$&T>HL+((aoPN0*@3!;cC&t^X-R+BpSkO~+N%EBA;KJcuWYm4 zlOP_iZh3p|G(D%R>FycxO!5=@1esO4Nw1U=%py+AaBr zXZQY$Bl{1?mX;~V*IdebDfW5ta)m}c>G0aZS97Y0PS?R3jhEB literal 0 HcmV?d00001 diff --git a/light/vectors/fixture-81053-stub.bin b/light/vectors/fixture-81053-stub.bin new file mode 100644 index 0000000000000000000000000000000000000000..af052ac05a5c737e10dc24a6518f927365542e0d GIT binary patch literal 832 zcmeZtck^dtVEiGp|LeZ6nP=j((`%bcBLfSJ+c}w4dejq(Qk6M#<1LgJz(6n`OflNb zW#Kyi_>RpAh4YWXLX0mloS&^c^R4w+IhM%&1JS32E00b#oU_1kRy4!&>b(!ov(?D^ zH`(m_ze$&T>HL+((aoPN0*@3!;cC&t^X-R+BpSkO~+N%EBA;KJcuWYm4 zlOP_iZh3p|G(D%R>FycxO!5=@1esO4Nbt#c<}&#@Z|ml;<(^)@w;P#%U~av>R()1EgZ5unX*Fkv bdYhS0^*O8m# literal 0 HcmV?d00001 diff --git a/light/vectors/fixture-81053-timing-flipped-lock-number.bin b/light/vectors/fixture-81053-timing-flipped-lock-number.bin new file mode 100644 index 0000000000000000000000000000000000000000..7cdfde9edabdef836f7d6ea02d17504845f6bfb1 GIT binary patch literal 832 zcmeZtck^dtWc(qt|LeZ6nP=j((`%bcBLfSJ+c}w4dejq(Qk6M#<1LgJz(6n`OflNb zW#Kyi_>RpAh4YWXLX0mloS&^c^R4w+IhM%&1JS32E00b#oU_1kRy4!&>b(!ov(?D^ zH`(m_ze$&T>HL+((aoPN0*@3!;cC&t^X-R+BpSkO~+N%EBA;KJcuWYm4 zlOP_iZh3p|G(D%R>FycxO!5=@1esO4Nbt#c<}&#@Z|ml;<(^)@w;P#%U~av>R()1EgZ5unX*Fkv zdYc(g^*N^$p2X{6Rg|!QyW#Ix)sL$@jZIfR7n=U0 zb^E%{LWd+6?!6b1mn(D1Vn~aNaj8sqR=V;%?$%wmdwXMUPWtwildbbYMEr-+H}ht$ z&Ezqjy2snlxL|YN=g>8`?p#!w9xZetZt3lw+}B>K3mbY*JM=%k_&k%x`Es?-vonlo NziL+z@Df4y0{{*e`Gf!f literal 0 HcmV?d00001 diff --git a/light/vectors/fixture-81053-timing-flipped-proof.bin b/light/vectors/fixture-81053-timing-flipped-proof.bin new file mode 100644 index 0000000000000000000000000000000000000000..a031afe2add26739ba0747c76ba2e59536e396af GIT binary patch literal 832 zcmeZtck^dtWc(qt|LeZ6nP=j((`%bcBLfSJ+c}w4dejq(Qk6M#<1LgJz(6n`OflNb zW#Kyi_>RpAh4YWXLX0mloS&^c^R4w+IhM%&1JS32E00b#oU_1kRy4!&>b(!ov(?D^ zH`(m_ze$&T>HL+((aoPN0*@3!;cC&t^X-R+BpSkO~+N%EBA;KJcuWYm4 zlOP_iZh3p|G(D%R>FycxO!5=@1esO4Nbt#c<}&#@Z|ml;<(^)@w;P#%U~av>R()1EgZ5unX*Fkv zdYhS0^*N^$p2X{6Rg|!QyW#Ix)sL$@jZIfR7n=U0 zb^E%{LWd+6?!6b1mn(D1Vn~aNaj8sqR=V;%?$%wmdwXMUPWtwildbbYMEr-+H}ht$ z&Ezqjy2snlxL|YN=g>8`?p#!w9xZetZt3lw+}B>K3mbY*JM=%k_&k%x`Es?-vonlo OziL+z@Dd^SGXnq*WBG;v literal 0 HcmV?d00001 diff --git a/light/vectors/fixture-81053-timing.bin b/light/vectors/fixture-81053-timing.bin new file mode 100644 index 0000000000000000000000000000000000000000..cef15cb9596c0bf4d5858190aea898afb2d84892 GIT binary patch literal 832 zcmeZtck^dtWc(qt|LeZ6nP=j((`%bcBLfSJ+c}w4dejq(Qk6M#<1LgJz(6n`OflNb zW#Kyi_>RpAh4YWXLX0mloS&^c^R4w+IhM%&1JS32E00b#oU_1kRy4!&>b(!ov(?D^ zH`(m_ze$&T>HL+((aoPN0*@3!;cC&t^X-R+BpSkO~+N%EBA;KJcuWYm4 zlOP_iZh3p|G(D%R>FycxO!5=@1esO4Nbt#c<}&#@Z|ml;<(^)@w;P#%U~av>R()1EgZ5unX*Fkv zdYhS0^*N^$p2X{6Rg|!QyW#Ix)sL$@jZIfR7n=U0 zb^E%{LWd+6?!6b1mn(D1Vn~aNaj8sqR=V;%?$%wmdwXMUPWtwildbbYMEr-+H}ht$ z&Ezqjy2snlxL|YN=g>8`?p#!w9xZetZt3lw+}B>K3mbY*JM=%k_&k%x`Es?-vonlo NziL+z@Df4y0{{fOA!Tmxo_$h>JB^&P1EcbOT0ZJM%I!4sP{Ncj`~RCC|0!g#JW-N= z)~;{*Ep>CjGw-+0+Iz|EXqruJB5QrquIH}j*2JDE#bx!2We4gW+RgfvrX~Ghf99^| zYODHhhX`}*y|T@EPl9;7y5;S;)AXFOrn_g%Gs#cr6J%EHD)rbc;hP`2M%gF&(s!Z# zU-yO0JQJs#UfWz68CYQ4&dIFOqn=ols?3=iZ=nRWmxF;(;E-##eK_4u{*lcKo?gGV8<~G#ZoR%%eO5Vx_Fq?NHD`!=o0(AcKOi*! O3J8rxGPW&dU;qFI9;yZa literal 0 HcmV?d00001 diff --git a/light/vectors/synthetic-300-block-79146-final.bin b/light/vectors/synthetic-300-block-79146-final.bin new file mode 100644 index 0000000000000000000000000000000000000000..b08cc5a34b42c7b198b3e283fdd179cb88407853 GIT binary patch literal 832 zcmeZtck^dtVEiGp|LeZ6nP=j((`%bcBLfSJ+c}w4dejq(Qk6M#<1LgJz(6n`Ofi~% zd^VNEWsb=5i<4?g!VI_QeM?`^yhnDnxW_iJ?~ikW-i5JTS{tA{@kNp1%VRPcCLZse zu&Wj5oX`vYw#~eDcF`^mpgD|f7un`|-?0B;aAn3sDcvo~mZIl<&!s;z4!i%lz|gYq zfMMtDLWYA(+qVb_q@S3R`Cty8V%7|iJ1g$3Ijo=Wap`i@b0iSo-h_P3Sikk0uA0r%HRnGT-~9ElYT*Wm zpMp3bG{k2}Y@AF>0)V)%WYl3`Tx70s!mCzhYox8%7ajxgpxX*Yr?Qj2Pk&>0(|O6^ zGp=1Pdx{mFPC9J$BDHaPbKcveJx6)_W`5jn->nsO$f)Wo#LXthAWr4#oBr+C^#>O^ z*##D;|DNeQJwk00C3h%%nz#R*)YW3HaU*gn(&75L&KQt*Z3%8JWg6&O*q2vy4TILGq9}F zD>`xd48W}5NSzQ}JR9}z-~5y=00000000lD000000001F00anAkJ(F6_MN0waV72y ztZK;xU%Hd)3odIWaZeY9v(iKb!?8i_5O8PX=A4C#Sa_{;Md}s%OPA!4c*NiB*?qLW3$ptKTR4x&V%<%jZy+qO;v7y?py7g5fTZq_ zOYgntIwgwLv0?NNPRyYq6DXlMwA*q@@$+}mxdDGmmYOp9k~B_R+v|oSO*vH0Pgfe% z3+WZnpYQ1UtJnY&=0%hA%wEitiTw@KtH~5(*HyF1h$nnR*MM0NKT`n!(kshGRP?h# zPAazGaV_g>U&U@3pZr}~ZrWrLzH)220aemW0VvhO?J~=6Uf*Fh-T|WTjN1uxSGBM2 z7hncgw*kJ6M1_D`Fv|<%APX;3KD>WgYq8Gg^&gp8EeQt{t2zL$U>s{4OzwSE%08T~ Wh!_d_QS+sgXdt{uL-Tjnm<4naarNl{ literal 0 HcmV?d00001 diff --git a/light/vectors/synthetic-300-block-79146-wrong-leaf-not-in-chain.bin b/light/vectors/synthetic-300-block-79146-wrong-leaf-not-in-chain.bin new file mode 100644 index 0000000000000000000000000000000000000000..b08cc5a34b42c7b198b3e283fdd179cb88407853 GIT binary patch literal 832 zcmeZtck^dtVEiGp|LeZ6nP=j((`%bcBLfSJ+c}w4dejq(Qk6M#<1LgJz(6n`Ofi~% zd^VNEWsb=5i<4?g!VI_QeM?`^yhnDnxW_iJ?~ikW-i5JTS{tA{@kNp1%VRPcCLZse zu&Wj5oX`vYw#~eDcF`^mpgD|f7un`|-?0B;aAn3sDcvo~mZIl<&!s;z4!i%lz|gYq zfMMtDLWYA(+qVb_q@S3R`Cty8V%7|iJ1g$3Ijo=Wap`i@b0iSo-h_P3Sikk0uA0r%HRnGT-~9ElYT*Wm zpMp3bG{k2}Y@AF>0)V)%WYl3`Tx70s!mCzhYox8%7ajxgpxX*Yr?Qj2Pk&>0(|O6^ zGp=1Pdx{mFPC9J$BDHaPbKcveJx6)_W`5jn->nsO$f)Wo#LXthAWr4#oBr+C^#>O^ z*##D;|DNeQJwk10C3h%%nz#R*)YW3HaU*gn(&75L&KQt*Z3%8JWg6&O*q2vy4TILGq9}F zD>`xd48W}5NSzQ}JR9}z-~5y=00000000lD000000001F00anAkJ(F6_MN0waV72y ztZK;xU%Hd)3odIWaZeY9v(iKb!?8i_5O8PX=A4C#Sa_{;Md}s%OPA!4c*NiB*?qLW3$ptKTR4x&V%<%jZy+qO;v7y?py7g5fTZq_ zOYgntIwgwLv0?NNPRyYq6DXlMwA*q@@$+}mxdDGmmYOp9k~B_R+v|oSO*vH0Pgfe% z3+WZnpYQ1UtJnY&=0%hA%wEitiTw@KtH~5(*HyF1h$nnR*MM0NKT`n!(kshGRP?h# zPAazGaV_g>U&U@3pZr}~ZrWrLzH)220aemW0VvhO?J~=6Uf*Fh-T|WTjN1uxSGBM2 z7hncgw*kJ6M1_D`Fv|<%APX;3KD>WgYq8Gg^&gp8EeQt{t2zL$U>s{4OzwSE%08T~ Wh!_d_QS+sgXdt{uL-Tjnm<4ncQT6Ho literal 0 HcmV?d00001 diff --git a/light/vectors/synthetic-300-block-79345-not-final.bin b/light/vectors/synthetic-300-block-79345-not-final.bin new file mode 100644 index 0000000000000000000000000000000000000000..b08cc5a34b42c7b198b3e283fdd179cb88407853 GIT binary patch literal 832 zcmeZtck^dtVEiGp|LeZ6nP=j((`%bcBLfSJ+c}w4dejq(Qk6M#<1LgJz(6n`Ofi~% zd^VNEWsb=5i<4?g!VI_QeM?`^yhnDnxW_iJ?~ikW-i5JTS{tA{@kNp1%VRPcCLZse zu&Wj5oX`vYw#~eDcF`^mpgD|f7un`|-?0B;aAn3sDcvo~mZIl<&!s;z4!i%lz|gYq zfMMtDLWYA(+qVb_q@S3R`Cty8V%7|iJ1g$3Ijo=Wap`i@b0iSo-h_P3Sikk0uA0r%HRnGT-~9ElYT*Wm zpMp3bG{k2}Y@AF>0)V)%WYl3`Tx70s!mCzhYox8%7ajxgpxX*Yr?Qj2Pk&>0(|O6^ zGp=1Pdx{mFPC9J$BDHaPbKcveJx6)_W`5jn->nsO$f)Wo#LXthAWr4#oBr+C^#>O^ z*##D;|DNeQJwk00BnRkI$yExXck*esNN5Ntodly>ICYn)k@g7WddD_Wf~A(7P~*x-P{< z<{BrwYGt-Y+KPSQF%S>BtzdL2JL&uMH-ZHenE(cj+FpG>Sca{uiUF4ot~sz>)kv)g*w zZwYhmIKU^iH!;Prw)vU8ie!fTm55Etr!(F6o}aem=p_EV?bl4$6}Ckkj8WD(9W&P= zCM9%MLI1l%CdT>xswFFQBv#Goym2Pu0iSo-h_P3Sikk0uA0r%HRnGT-~9ElYT*Wm zpMp3bG{k2}Y@AF>0)V)%WYl3`Tx70s!mCzhYox8%7ajxgpxX*Yr?Qj2Pk&>0(|O6^ zGp=1Pdx{mFPC9J$BDHaPbKcveJx6)_W`5jn->nsO$f)Wo#LXthAWr4#oBr+C^#>O^ z*##D;|DNeQJwk+ Bx*`Ao literal 0 HcmV?d00001 diff --git a/light/vectors/synthetic-300-stale.bin b/light/vectors/synthetic-300-stale.bin new file mode 100644 index 0000000000000000000000000000000000000000..bb5074cc4edba0c01b0b0e60648c782c6b3d3a74 GIT binary patch literal 832 zcmeZtck^dtVEiGp|LeZ6nP=j((`%bcBLfSJ+c}w4dejq(Qk6M#<1LgJz(6n`Ofi~% zd^VNEWsb=5i<4?g!VI_QeM?`^yhnDnxW_iJ?~ikW-i5JTS{tA{@kNp1%VRPcCLZse zu&Wj5oX`vYw#~eDcF`^mpgD|f7un`|-?0B;aAn3sDcvo~mZIl<&!s;z4!i%lz|gYq zfMMtDLWYA(+qVb_q@S3R`Cty8V%7|iJ1g$3Ijo=Wap`i@b0iSo-h_P3Sikk0uA0r%HRnGT-~9ElYT*Wm zpMp3bG{k2}Y@AF>0)V)%WYl3`Tx70s!mCzhYox8%7ajxgpxX*Yr?Qj2Pk&>0(|O6^ zGp=1Pdx{mFPC9J$BDHaPbKcveJx6)_W`5jn->nsO$f)Wo#LXthAWr4#oBr+C^#>O^ z*##D;|DNeQJwk0iSo-h_P3Sikk0uA0r%HRnGT-~9ElYT*Wm zpMp3bG{k2}Y@AF>0)V)%WYl3`Tx70s!mCzhYox8%7ajxgpxX*Yr?Qj2Pk&>0(|O6^ zGp=1Pdx{mFPC9J$BDHaPbKcveJx6)_W`5jn->nsO$f)Wo#LXthAWr4#oBr+C^#>O^ z*##D;|DNeQJwk=|EuZLAp!v5`szhhNDuJSZCUHM#S`jgh}>plw|l3=*^UPxZ9%qfc@EiT5TGTm9} z%J;Ziciryojk!7L+gnby&I=LoA4=cMo4GcV$9U=00C3h%%nz#R*)YW3HaU*gn(&75L&KQt*Z3%8JWg6&O*q2vy4TILGq9}F zD>`xd48W}5NSzQ}JR9}z-~5y=00000000lD000000001F00anAkJ(F6_MN0waV72y ztZK;xU%Hd)3odIWaZeY9v(iKb!?8i_5O8PX=A4C#Sa_{;Md}s%OPA!4c*NiB*?qLW3$ptKTR4x&V%<%jZy+qO;v7y?py7g5fTZq_ zOYgntIwgwLv0?NNPRyYq6DXlMwA*q@@$+}mxdDGmmYOp9k~B_R+v|oSO*vH0Pgfe% z3+WZnpYQ1UtJnY&=0%hA%wEitiTw@KtH~5(*HyF1h$nnR*MM0NKT`n!(kshGRP?h# zPAazGaV_g>U&U@3pZr}~ZrWrLzH)220aemW0VvhO?J~=6Uf*Fh-T|WTjN1uxSGBM2 z7hncgw*kJ6M1_D`Fv|<%APX;3KD>WgYq8Gg^&gp8EeQt{t2zL$U>s{4OzwSE%08T~ Wh!_d_QS+sgXdt{uL-Tjnm<4naarNl{ literal 0 HcmV?d00001