From 56bdadecb2ddf7736b89b1ebf2dcbe037a72c1a5 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 12 Nov 2026 18:36:21 +0100 Subject: [PATCH 1/8] feat(ffi)!: build the EIP-8288 backend on pinned upstream leanVM with the guest in src/ffi (ABI 12) src/ffi becomes the source of the native EIP-8288 backend instead of a copy of Nethermind's tools/lean-ffi. It depends on unmodified leanEthereum/leanVM at e4dcb8d5436c9bf76398d890c57c59613af12956 and carries the EIP-8288 aggregation guest in crates/: rec_aggregation and signatures vendored from upstream with NiceTry v2 signatures (0x10), generic leanSTARK claims (0x11) keyed by upstream's program digest, mixed recursive merge and discard, and Keccak-256 dependency commitments split by scheme. ABI 12 adds nlean_verify_leanstark and seven limits; AGGREGATED_VK is f7cf74585d02043051f0575c23c6b441b0b08ac9e273f5e989c5984b8a4ac0e5. The crate version 0.12.0 carries the ABI. fixtures can also prove a leanSTARK dependency on a message. Each vendored crate keeps upstream's MIT and Apache-2.0 texts; about.toml clarifies the leanVM crates and the notices are regenerated. Validated in NethermindEth/nethermind#15829. Co-Authored-By: Claude Opus 5.5 --- THIRD-PARTY-NOTICES | 1251 +---- src/ffi/Cargo.lock | 2404 +-------- src/ffi/Cargo.toml | 57 +- src/ffi/LeanFfi.targets | 17 - src/ffi/README.md | 135 +- src/ffi/about.toml | 54 +- src/ffi/crates/README.md | 22 + src/ffi/crates/rec_aggregation/Cargo.toml | 32 + src/ffi/crates/rec_aggregation/LICENSE-APACHE | 201 + src/ffi/crates/rec_aggregation/LICENSE-MIT | 21 + .../rec_aggregation/examples/agg_bench.rs | 149 + .../rec_aggregation/guests/lean_ethereum.py | 4472 +++++++++++++++++ .../rec_aggregation/src/aggregation/cells.rs | 38 + .../rec_aggregation/src/aggregation/child.rs | 288 ++ .../src/aggregation/claim_pool.rs | 165 + .../src/aggregation/coverage.rs | 127 + .../src/aggregation/deferred.rs | 354 ++ .../rec_aggregation/src/aggregation/error.rs | 144 + .../src/aggregation/generic.rs | 187 + .../src/aggregation/generic_tests.rs | 402 ++ .../rec_aggregation/src/aggregation/guest.rs | 67 + .../rec_aggregation/src/aggregation/hints.rs | 91 + .../rec_aggregation/src/aggregation/mod.rs | 80 + .../rec_aggregation/src/aggregation/node.rs | 375 ++ .../src/aggregation/placeholders.rs | 932 ++++ .../rec_aggregation/src/aggregation/proof.rs | 232 + .../src/aggregation/statement.rs | 138 + .../rec_aggregation/src/aggregation/tests.rs | 662 +++ .../crates/rec_aggregation/src/hash_chain.rs | 126 + src/ffi/crates/rec_aggregation/src/lib.rs | 14 + .../rec_aggregation/src/signers_cache.rs | 306 ++ src/ffi/crates/signatures/Cargo.toml | 17 + src/ffi/crates/signatures/LICENSE-APACHE | 201 + src/ffi/crates/signatures/LICENSE-MIT | 21 + .../signatures/src/lean_sphincs/fors.rs | 115 + .../signatures/src/lean_sphincs/hash.rs | 121 + .../crates/signatures/src/lean_sphincs/mod.rs | 106 + .../signatures/src/lean_sphincs/scheme.rs | 374 ++ .../lean_sphincs/sphincs-v2-reference-0.json | 17 + .../signatures/src/lean_sphincs/wots.rs | 165 + src/ffi/crates/signatures/src/lib.rs | 9 + src/ffi/examples/fixtures.rs | 50 +- src/ffi/examples/lean_startup_stub.rs | 10 +- src/ffi/src/lib.rs | 1722 ++----- 44 files changed, 11488 insertions(+), 4983 deletions(-) delete mode 100644 src/ffi/LeanFfi.targets create mode 100644 src/ffi/crates/README.md create mode 100644 src/ffi/crates/rec_aggregation/Cargo.toml create mode 100644 src/ffi/crates/rec_aggregation/LICENSE-APACHE create mode 100644 src/ffi/crates/rec_aggregation/LICENSE-MIT create mode 100644 src/ffi/crates/rec_aggregation/examples/agg_bench.rs create mode 100644 src/ffi/crates/rec_aggregation/guests/lean_ethereum.py create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/cells.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/child.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/claim_pool.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/coverage.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/deferred.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/error.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/generic.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/generic_tests.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/guest.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/hints.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/mod.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/node.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/placeholders.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/proof.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/statement.rs create mode 100644 src/ffi/crates/rec_aggregation/src/aggregation/tests.rs create mode 100644 src/ffi/crates/rec_aggregation/src/hash_chain.rs create mode 100644 src/ffi/crates/rec_aggregation/src/lib.rs create mode 100644 src/ffi/crates/rec_aggregation/src/signers_cache.rs create mode 100644 src/ffi/crates/signatures/Cargo.toml create mode 100644 src/ffi/crates/signatures/LICENSE-APACHE create mode 100644 src/ffi/crates/signatures/LICENSE-MIT create mode 100644 src/ffi/crates/signatures/src/lean_sphincs/fors.rs create mode 100644 src/ffi/crates/signatures/src/lean_sphincs/hash.rs create mode 100644 src/ffi/crates/signatures/src/lean_sphincs/mod.rs create mode 100644 src/ffi/crates/signatures/src/lean_sphincs/scheme.rs create mode 100644 src/ffi/crates/signatures/src/lean_sphincs/sphincs-v2-reference-0.json create mode 100644 src/ffi/crates/signatures/src/lean_sphincs/wots.rs create mode 100644 src/ffi/crates/signatures/src/lib.rs diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 58a41b6..ca04d39 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -21,7 +21,7 @@ licenses follow. -------------------------------------------------------------------------------- Apache License 2.0 (Apache-2.0) -Used by: fiat_shamir 0.1.0, flock 0.1.0, lean_compiler 0.1.0, lean_da 0.1.0, lean_vm 0.1.0, parallel 0.1.0, pcs 0.1.0, primitives 0.1.0, rec_aggregation 0.1.0, sphincs 0.1.0, xmss 0.1.0, zk_alloc 0.1.0 +Used by: bench 0.1.0, fiat_shamir 0.1.0, keccak_gkr 0.1.0, lean_compiler 0.1.0, lean_da 0.1.0, leanvm_core 0.1.0, parallel 0.1.0, pcs 0.1.0, primitives 0.1.0, signatures 0.1.0, verifier 0.1.0, rec_aggregation 0.1.0 Apache License Version 2.0, January 2004 @@ -226,212 +226,6 @@ Used by: fiat_shamir 0.1.0, flock 0.1.0, lean_compiler 0.1.0, lean_da 0.1.0, lea and limitations under the License. -------------------------------------------------------------------------------- Apache License 2.0 (Apache-2.0) -Used by: ethereum_serde_utils 0.8.1 - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2022 Sigma Prime Pty Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - --------------------------------------------------------------------------------- -Apache License 2.0 (Apache-2.0) Used by: zerocopy 0.8.59 Apache License @@ -580,684 +374,66 @@ Used by: zerocopy 0.8.59 agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2023 The Fuchsia Authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - --------------------------------------------------------------------------------- -Apache License 2.0 (Apache-2.0) -Used by: hex 0.4.3 - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - --------------------------------------------------------------------------------- -Apache License 2.0 (Apache-2.0) -Used by: typenum 1.20.1 - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2014 Paho Lurie-Gregg - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. --------------------------------------------------------------------------------- -Apache License 2.0 (Apache-2.0) -Used by: ppv-lite86 0.2.21 - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -END OF TERMS AND CONDITIONS + END OF TERMS AND CONDITIONS -APPENDIX: How to apply the Apache License to your work. + APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Copyright 2019 The CryptoCorrosion Contributors + Copyright 2023 The Fuchsia Authors -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -------------------------------------------------------------------------------- Apache License 2.0 (Apache-2.0) -Used by: cfg-if 1.0.5, either 1.18.0, hashbrown 0.17.1, itertools 0.14.0, lazy_static 1.5.1, log 0.4.34, once_cell 1.21.4, regex-automata 0.4.18, regex-syntax 0.8.11, smallvec 1.16.2, thread_local 1.1.10, unicode-segmentation 1.13.3, unicode-xid 0.2.6 +Used by: ppv-lite86 0.2.21 Apache License Version 2.0, January 2004 @@ -1447,13 +623,13 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright 2019 The CryptoCorrosion Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -1463,7 +639,7 @@ limitations under the License. -------------------------------------------------------------------------------- Apache License 2.0 (Apache-2.0) -Used by: block-buffer 0.12.1, cpufeatures 0.2.17, cpufeatures 0.3.1, crypto-common 0.2.2, digest 0.11.3, hybrid-array 0.4.15, keccak 0.2.2, sha3 0.11.0 +Used by: cfg-if 1.0.5, lazy_static 1.5.1, log 0.4.35, once_cell 1.21.4, regex-automata 0.4.18, regex-syntax 0.8.11, smallvec 1.16.5, thread_local 1.1.10, tikv-jemalloc-sys 0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8, tikv-jemallocator 0.7.0 Apache License Version 2.0, January 2004 @@ -1659,7 +835,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -2067,7 +1243,7 @@ limitations under the License. -------------------------------------------------------------------------------- Apache License 2.0 (Apache-2.0) -Used by: alloy-primitives 1.7.3, const-hex 1.19.3, ethereum_ssz 0.10.4, itoa 1.0.18, libc 0.2.189, paste 1.0.15, pin-project-lite 0.2.17, proc-macro2 1.0.107, quote 1.0.47, rand 0.9.5, rand_chacha 0.9.0, serde 1.0.229, serde_core 1.0.229, serde_derive 1.0.229, serde_json 1.0.151, syn 2.0.119, syn 3.0.6, thiserror-impl 2.0.21, thiserror 2.0.21, unicode-ident 1.0.26 +Used by: libc 0.2.189, pin-project-lite 0.2.17, proc-macro2 1.0.107, quote 1.0.47, rand 0.9.5, rand_chacha 0.9.0, syn 2.0.119, syn 3.0.10, thiserror-impl 2.0.21, thiserror 2.0.21, unicode-ident 1.0.26 Apache License Version 2.0, January 2004 @@ -2145,7 +1321,7 @@ limitations under the License. -------------------------------------------------------------------------------- Apache License 2.0 (Apache-2.0) -Used by: fiat_shamir 0.1.0, flock 0.1.0, lean_compiler 0.1.0, lean_da 0.1.0, lean_vm 0.1.0, parallel 0.1.0, pcs 0.1.0, primitives 0.1.0, rec_aggregation 0.1.0, sphincs 0.1.0, xmss 0.1.0, zk_alloc 0.1.0 +Used by: bench 0.1.0, fiat_shamir 0.1.0, keccak_gkr 0.1.0, lean_compiler 0.1.0, lean_da 0.1.0, leanvm_core 0.1.0, parallel 0.1.0, pcs 0.1.0, primitives 0.1.0, signatures 0.1.0, verifier 0.1.0, rec_aggregation 0.1.0 MIT License @@ -2168,162 +1344,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------------------------------- -Creative Commons Zero v1.0 Universal (CC0-1.0) -Used by: tiny-keccak 2.0.2 - -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. - --------------------------------------------------------------------------------- -MIT License (MIT) -Used by: bytes 1.12.1 - -Copyright (c) 2018 Carl Lerche - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - -------------------------------------------------------------------------------- MIT License (MIT) Used by: sharded-slab 0.1.7 @@ -2404,11 +1424,11 @@ DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- MIT License (MIT) -Used by: convert_case 0.10.0 +Used by: nethermind-lean 0.12.0, nethermind-lean 0.12.0 MIT License -Copyright (c) 2025 rutrum +Copyright (c) 2026 leanEthereum Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -2429,7 +1449,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- MIT License (MIT) -Used by: nethermind-lean 0.1.0, ruint-macro 1.2.1, ruint 1.20.1, tracing-forest 0.3.1 +Used by: tracing-forest 0.3.1 MIT License @@ -2450,34 +1470,6 @@ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------------------------------- -MIT License (MIT) -Used by: zmij 1.0.23 - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - -------------------------------------------------------------------------------- MIT License (MIT) Used by: ansi_term 0.12.1 @@ -2530,84 +1522,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------------------------------- -MIT License (MIT) -Used by: memchr 2.8.3 - -The MIT License (MIT) - -Copyright (c) 2015 Andrew Gallant - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - --------------------------------------------------------------------------------- -MIT License (MIT) -Used by: derive_more-impl 2.1.1, derive_more 2.1.1 - -The MIT License (MIT) - -Copyright (c) 2016 Jelte Fennema - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - --------------------------------------------------------------------------------- -MIT License (MIT) -Used by: crunchy 0.2.4 - -The MIT License (MIT) - -Copyright 2017-2023 Eira Fransham. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -------------------------------------------------------------------------------- MIT License (MIT) Used by: nu-ansi-term 0.50.3 @@ -2635,32 +1549,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------------------------------- -MIT License (MIT) -Used by: bincode 1.3.3 - -The MIT License (MIT) - -Copyright (c) 2014 Ty Overby - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -------------------------------------------------------------------------------- Unicode License v3 (Unicode-3.0) Used by: unicode-ident 1.0.26 @@ -2705,26 +1593,3 @@ not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder. --------------------------------------------------------------------------------- -zlib License (Zlib) -Used by: foldhash 0.2.0 - -Copyright (c) 2024 Orson Peters - -This software is provided 'as-is', without any express or implied warranty. In -no event will the authors be held liable for any damages arising from the use of -this software. - -Permission is granted to anyone to use this software for any purpose, including -commercial applications, and to alter it and redistribute it freely, subject to -the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim - that you wrote the original software. If you use this software in a product, - an acknowledgment in the product documentation would be appreciated but is - not required. - -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. diff --git a/src/ffi/Cargo.lock b/src/ffi/Cargo.lock index 44c062c..753efc1 100644 --- a/src/ffi/Cargo.lock +++ b/src/ffi/Cargo.lock @@ -12,2020 +12,308 @@ dependencies = [ ] [[package]] -name = "alloy-primitives" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5e9dbd49258ac3ab893a481d46be29b58be7f734dcac46cd80b6b13ee36566c" -dependencies = [ - "alloy-rlp", - "bytes", - "cfg-if", - "const-hex", - "derive_more", - "fixed-cache", - "foldhash", - "hashbrown 0.17.1", - "indexmap 2.14.2", - "itoa", - "k256", - "keccak-asm", - "paste", - "proptest", - "rand 0.9.5", - "rapidhash", - "ruint", - "rustc-hash", - "secp256k1", - "serde", - "sha3", -] - -[[package]] -name = "alloy-rlp" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24671b1f62edcf0f9b62994c7bf72cd621a04a4b99f5020ece1a647b40e2f103" -dependencies = [ - "arrayvec", - "bytes", -] - -[[package]] -name = "android_system_properties" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" -dependencies = [ - "libc", -] - -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - -[[package]] -name = "ark-ff" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" -dependencies = [ - "ark-ff-asm 0.3.0", - "ark-ff-macros 0.3.0", - "ark-serialize 0.3.0", - "ark-std 0.3.0", - "derivative", - "num-bigint", - "num-traits", - "paste", - "rustc_version 0.3.3", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" -dependencies = [ - "ark-ff-asm 0.4.2", - "ark-ff-macros 0.4.2", - "ark-serialize 0.4.2", - "ark-std 0.4.0", - "derivative", - "digest 0.10.7", - "itertools 0.10.5", - "num-bigint", - "num-traits", - "paste", - "rustc_version 0.4.1", - "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 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "arrayvec", - "digest 0.10.7", - "educe", - "itertools 0.13.0", - "num-bigint", - "num-traits", - "paste", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" -dependencies = [ - "ark-ff-asm 0.6.0", - "ark-ff-macros 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "educe", - "num-bigint", - "num-traits", - "zeroize", -] - -[[package]] -name = "ark-ff-asm" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-asm" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[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-asm" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ark-ff-macros" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" -dependencies = [ - "num-bigint", - "num-traits", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-macros" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[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-ff-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ark-serialize" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" -dependencies = [ - "ark-std 0.3.0", - "digest 0.9.0", -] - -[[package]] -name = "ark-serialize" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" -dependencies = [ - "ark-std 0.4.0", - "digest 0.10.7", - "num-bigint", -] - -[[package]] -name = "ark-serialize" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" -dependencies = [ - "ark-std 0.5.0", - "arrayvec", - "digest 0.10.7", - "num-bigint", -] - -[[package]] -name = "ark-serialize" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" -dependencies = [ - "ark-serialize-derive", - "ark-std 0.6.0", - "digest 0.10.7", - "num-bigint", - "serde_with", -] - -[[package]] -name = "ark-serialize-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ark-std" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" -dependencies = [ - "num-traits", - "rand 0.8.8", -] - -[[package]] -name = "ark-std" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" -dependencies = [ - "num-traits", - "rand 0.8.8", -] - -[[package]] -name = "ark-std" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" -dependencies = [ - "num-traits", - "rand 0.8.8", -] - -[[package]] -name = "ark-std" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" -dependencies = [ - "num-traits", - "rand 0.8.8", -] - -[[package]] -name = "arrayvec" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" - -[[package]] -name = "auto_impl" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bitcoin-consensus-encoding" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9daa31138eb443d5751b207f3f64154e2bb09cd59960562ccc7a7112be38147f" -dependencies = [ - "bitcoin-internals", - "hex-conservative 1.3.0", - "serde", -] - -[[package]] -name = "bitcoin-internals" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8bea3a9f0cfece4564e37cb49a38cc245ca5184719e50d7d0dda3268722c4e2" - -[[package]] -name = "bitcoin-io" -version = "0.1.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" -dependencies = [ - "bitcoin-consensus-encoding", -] - -[[package]] -name = "bitcoin_hashes" -version = "0.14.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" -dependencies = [ - "bitcoin-io", - "hex-conservative 0.2.3", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" - -[[package]] -name = "bitvec" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[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 = "block-buffer" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -dependencies = [ - "serde", -] - -[[package]] -name = "cc" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" -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 = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] - -[[package]] -name = "const-hex" -version = "1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e59eef12462b0f9b0a3620219be5d639afd79fe39dff0a42c3997061f9298b4" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "proptest", - "serde_core", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const_format" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" -dependencies = [ - "const_format_proc_macros", - "konst", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "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 = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "crypto-common" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "defmt" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "defmt-parser" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" -dependencies = [ - "thiserror", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "serde_core", -] - -[[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 = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version 0.4.1", - "syn 2.0.119", - "unicode-xid", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "const-oid", - "crypto-common 0.1.6", - "subtle", -] - -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer 0.12.1", - "crypto-common 0.2.2", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[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.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[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 = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "ethereum_serde_utils" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38df44a7a271ab43835678f9215b53cc2523e4714a215da6643d83dc110245da" -dependencies = [ - "alloy-primitives", - "hex", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "ethereum_ssz" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e462875ad8693755ea8913d6e905715c76ea4836e2254e18c9cf0f7a8f8c2a13" -dependencies = [ - "alloy-primitives", - "ethereum_serde_utils", - "itertools 0.14.0", - "serde", - "serde_derive", - "smallvec", - "typenum", -] - -[[package]] -name = "fastrlp" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "fastrlp" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "fiat_shamir" -version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" -dependencies = [ - "parallel", - "primitives", - "serde", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" - -[[package]] -name = "fixed-cache" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe63500644ef0269fe6b744e7e5dc5c20b5eebf3d881bc2be53f194636f6583" -dependencies = [ - "equivalent", - "rapidhash", -] - -[[package]] -name = "fixed-hash" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" -dependencies = [ - "byteorder", - "rand 0.8.8", - "rustc-hex", - "static_assertions", -] - -[[package]] -name = "flock" -version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" -dependencies = [ - "fiat_shamir", - "parallel", - "pcs", - "primitives", - "zk_alloc", -] - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "foldhash", - "serde", - "serde_core", -] - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hex-conservative" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3fef046dca3ca91ee1408a8c1b80ab777e80a4d308d1bf4e7adb3fcb047e08" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hex-conservative" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271e0d19bcb473b6675739a2b536076b24a082316cb5199ad918edce10c599e8" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hybrid-array" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" -dependencies = [ - "typenum", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "impl-codec" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" -dependencies = [ - "parity-scale-codec", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jiff" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" -dependencies = [ - "defmt", - "jiff-core", - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-link", -] - -[[package]] -name = "jiff-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" -dependencies = [ - "defmt", - "log", -] - -[[package]] -name = "jiff-static" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" -dependencies = [ - "jiff-core", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - -[[package]] -name = "js-sys" -version = "0.3.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "sha2", -] - -[[package]] -name = "keccak" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", -] - -[[package]] -name = "keccak-asm" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f32890f646914a263e39064295005972f0e95b928254061b2aca98445f304ee9" -dependencies = [ - "cfg-if", - "digest 0.10.7", - "sha3-asm", -] - -[[package]] -name = "konst" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" -dependencies = [ - "konst_macro_rules", -] - -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - -[[package]] -name = "lazy_static" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" - -[[package]] -name = "lean_compiler" -version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" -dependencies = [ - "lean_vm", - "primitives", -] - -[[package]] -name = "lean_da" -version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" -dependencies = [ - "fiat_shamir", - "parallel", - "pcs", - "primitives", - "serde", - "tracing", -] - -[[package]] -name = "lean_vm" -version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" -dependencies = [ - "fiat_shamir", - "flock", - "parallel", - "pcs", - "primitives", - "tracing", - "zk_alloc", -] - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "log" -version = "0.4.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "nethermind-lean" -version = "0.1.0" -dependencies = [ - "bincode", - "lean_compiler", - "lean_vm", - "libc", - "parallel", - "primitives", - "rec_aggregation", - "sphincs", - "tiny-keccak", - "zk_alloc", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys", -] - -[[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-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[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", - "libm", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "parallel" -version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" -dependencies = [ - "libc", -] - -[[package]] -name = "parity-scale-codec" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" -dependencies = [ - "arrayvec", - "bitvec", - "byte-slice-cast", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "rustversion", - "serde", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pcs" -version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" -dependencies = [ - "fiat_shamir", - "parallel", - "primitives", - "serde", - "tracing", - "zk_alloc", -] - -[[package]] -name = "pest" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - -[[package]] -name = "portable-atomic-util" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "primitive-types" -version = "0.12.2" +name = "ansi_term" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "fixed-hash", - "impl-codec", - "uint", + "winapi", ] [[package]] -name = "primitives" +name = "bench" version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" +source = "git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956#e4dcb8d5436c9bf76398d890c57c59613af12956" dependencies = [ "libc", - "parallel", - "serde", + "primitives", + "tikv-jemallocator", "tracing-forest", "tracing-subscriber", - "zk_alloc", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", ] [[package]] -name = "proc-macro2" -version = "1.0.107" +name = "cc" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "4ff2a8126fff0f8c839080d619675a585e61720bb12d2baceae45769fd66f155" dependencies = [ - "unicode-ident", + "find-msvc-tools", + "shlex", ] [[package]] -name = "proptest" -version = "1.11.0" +name = "cfg-if" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bitflags 2.13.2", - "num-traits", - "rand 0.9.5", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "unarray", -] +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +name = "fiat_shamir" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956#e4dcb8d5436c9bf76398d890c57c59613af12956" dependencies = [ - "proc-macro2", + "parallel", + "primitives", ] [[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "radium" -version = "0.7.0" +name = "find-msvc-tools" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" [[package]] -name = "rand" -version = "0.8.8" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ + "cfg-if", "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", - "serde", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", + "r-efi", + "wasip2", ] [[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +name = "keccak_gkr" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956#e4dcb8d5436c9bf76398d890c57c59613af12956" dependencies = [ - "getrandom 0.3.4", - "serde", + "fiat_shamir", + "parallel", + "pcs", + "primitives", + "tracing", ] [[package]] -name = "rand_xorshift" -version = "0.4.0" +name = "lazy_static" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" [[package]] -name = "rapidhash" -version = "4.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" +name = "lean_compiler" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956#e4dcb8d5436c9bf76398d890c57c59613af12956" dependencies = [ - "rustversion", + "leanvm_core", + "primitives", ] [[package]] -name = "rec_aggregation" +name = "lean_da" version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" +source = "git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956#e4dcb8d5436c9bf76398d890c57c59613af12956" dependencies = [ - "bincode", - "flock", - "lean_compiler", - "lean_da", - "lean_vm", + "fiat_shamir", "parallel", "pcs", "primitives", - "rand 0.9.5", - "serde", - "sphincs", "tracing", - "xmss", -] - -[[package]] -name = "ref-cast" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", ] [[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +name = "leanvm_core" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956#e4dcb8d5436c9bf76398d890c57c59613af12956" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "fiat_shamir", + "keccak_gkr", + "parallel", + "pcs", + "primitives", + "tracing", + "verifier", ] [[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "rfc6979" -version = "0.4.0" +name = "libc" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] -name = "rlp" -version = "0.5.2" +name = "log" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" -dependencies = [ - "bytes", - "rustc-hex", -] +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" [[package]] -name = "ruint" -version = "1.20.1" +name = "matchers" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2973657b5127d510e230f5c63d2d106af9c8f79393d8b9f4647323e8196bdde5" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "alloy-rlp", - "ark-ff 0.3.0", - "ark-ff 0.4.2", - "ark-ff 0.5.0", - "ark-ff 0.6.0", - "bytes", - "fastrlp 0.3.1", - "fastrlp 0.4.0", - "num-bigint", - "num-integer", - "num-traits", - "parity-scale-codec", - "primitive-types", - "proptest", - "rand 0.8.8", - "rand 0.9.5", - "rlp", - "ruint-macro", - "serde_core", - "valuable", - "zeroize", + "regex-automata", ] [[package]] -name = "ruint-macro" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - -[[package]] -name = "rustc_version" -version = "0.3.3" +name = "memchr" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver 0.11.0", -] +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +name = "nethermind-lean" +version = "0.12.0" dependencies = [ - "semver 1.0.28", + "bench", + "fiat_shamir", + "lean_compiler", + "leanvm_core", + "parallel", + "primitives", + "rec_aggregation", + "signatures 0.1.0", + "tikv-jemallocator", ] [[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "schemars" -version = "0.9.0" +name = "nu-ansi-term" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", + "windows-sys", ] [[package]] -name = "schemars" -version = "1.2.2" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +name = "parallel" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956#e4dcb8d5436c9bf76398d890c57c59613af12956" dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", + "libc", ] [[package]] -name = "secp256k1" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +name = "pcs" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956#e4dcb8d5436c9bf76398d890c57c59613af12956" dependencies = [ - "bitcoin_hashes", - "rand 0.9.5", - "secp256k1-sys", + "fiat_shamir", + "parallel", + "primitives", + "tracing", ] [[package]] -name = "secp256k1-sys" -version = "0.11.0" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" -dependencies = [ - "cc", -] +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "semver" -version = "0.11.0" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "semver-parser", + "zerocopy", ] [[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +name = "primitives" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956#e4dcb8d5436c9bf76398d890c57c59613af12956" +dependencies = [ + "parallel", +] [[package]] -name = "semver-parser" -version = "0.10.3" +name = "proc-macro2" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ - "pest", + "unicode-ident", ] [[package]] -name = "serde" -version = "1.0.229" +name = "quote" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ - "serde_core", - "serde_derive", + "proc-macro2", ] [[package]] -name = "serde_core" -version = "1.0.229" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "serde_derive" -version = "1.0.229" +name = "rand" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", + "rand_chacha", + "rand_core", ] [[package]] -name = "serde_json" -version = "1.0.151" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "ppv-lite86", + "rand_core", ] [[package]] -name = "serde_with" -version = "3.24.0" +name = "rand_core" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "base64", - "bs58", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.14.2", - "jiff", - "schemars 0.9.0", - "schemars 1.2.2", - "serde_core", - "serde_json", - "time", + "getrandom", ] [[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +name = "rec_aggregation" +version = "0.1.0" dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", + "fiat_shamir", + "keccak_gkr", + "lean_compiler", + "lean_da", + "leanvm_core", + "parallel", + "pcs", + "primitives", + "rand", + "signatures 0.1.0", + "tracing", + "verifier", ] [[package]] -name = "sha3" -version = "0.11.0" +name = "regex-automata" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ - "digest 0.11.3", - "keccak", + "aho-corasick", + "memchr", + "regex-syntax", ] [[package]] -name = "sha3-asm" -version = "0.1.9" +name = "regex-syntax" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471668161349031e3d415412f996b030c477488eec267cc3cadae3d06c0a367f" -dependencies = [ - "cc", - "cfg-if", -] +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "sharded-slab" @@ -2043,70 +331,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" - -[[package]] -name = "sphincs" +name = "signatures" version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" dependencies = [ "parallel", "primitives", - "rand 0.9.5", - "serde", + "rand", + "signatures 0.1.0 (git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956)", ] [[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +name = "signatures" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956#e4dcb8d5436c9bf76398d890c57c59613af12956" dependencies = [ - "base64ct", - "der", + "parallel", + "primitives", + "rand", ] [[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[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" +name = "smallvec" +version = "1.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "149309498a5f7334b12ddae0cad8aba81ce785fe91755068867e017cf98fee46" [[package]] name = "syn" @@ -2121,21 +369,15 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "4ae9ccbea3a7b0b09443036490c8f34a8b1b760fd345e5c0dae0aac718faea79" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "thiserror" version = "2.0.21" @@ -2153,7 +395,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.10", ] [[package]] @@ -2166,78 +408,23 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinyvec" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.15+spec-1.1.0" +name = "tikv-jemalloc-sys" +version = "0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +checksum = "1a2825c78386b4ae0314074867860ba9577875de945f05992c38815cbec327f0" dependencies = [ - "indexmap 2.14.2", - "toml_datetime", - "toml_parser", - "winnow", + "cc", + "libc", ] [[package]] -name = "toml_parser" -version = "1.1.3+spec-1.1.0" +name = "tikv-jemallocator" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "249f09e49ab1609436f34c776e84231bead18d6a955f119f939bdc1d847561bd" dependencies = [ - "winnow", + "libc", + "tikv-jemalloc-sys", ] [[package]] @@ -2314,54 +501,12 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uint" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unicode-ident" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "valuable" version = "0.1.1" @@ -2369,16 +514,9 @@ 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 = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +name = "verifier" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanVM?rev=e4dcb8d5436c9bf76398d890c57c59613af12956#e4dcb8d5436c9bf76398d890c57c59613af12956" [[package]] name = "wasip2" @@ -2389,51 +527,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasm-bindgen" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 3.0.6", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" -dependencies = [ - "unicode-ident", -] - [[package]] name = "winapi" version = "0.3.9" @@ -2456,65 +549,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -2524,42 +564,12 @@ dependencies = [ "windows-link", ] -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "xmss" -version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" -dependencies = [ - "ethereum_ssz", - "parallel", - "primitives", - "rand 0.9.5", - "serde", -] - [[package]] name = "zerocopy" version = "0.8.59" @@ -2579,37 +589,3 @@ dependencies = [ "quote", "syn 2.0.119", ] - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" -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", -] - -[[package]] -name = "zk_alloc" -version = "0.1.0" -source = "git+https://github.com/Marchhill/leanVM?rev=e2bc4212013087c0823188e975b224ade286d878#e2bc4212013087c0823188e975b224ade286d878" -dependencies = [ - "libc", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/src/ffi/Cargo.toml b/src/ffi/Cargo.toml index 1154c27..c7a75df 100644 --- a/src/ffi/Cargo.toml +++ b/src/ffi/Cargo.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: MIT [package] name = "nethermind-lean" -version = "0.1.0" +version = "0.12.0" edition = "2024" rust-version = "1.99" license = "MIT" @@ -13,21 +13,52 @@ name = "nethermind_lean" crate-type = ["cdylib", "rlib"] [dependencies] -sphincs = { git = "https://github.com/Marchhill/leanVM", rev = "e2bc4212013087c0823188e975b224ade286d878" } -rec_aggregation = { git = "https://github.com/Marchhill/leanVM", rev = "e2bc4212013087c0823188e975b224ade286d878", features = ["eip8288-mixed"] } -lean_vm = { git = "https://github.com/Marchhill/leanVM", rev = "e2bc4212013087c0823188e975b224ade286d878" } -lean_compiler = { git = "https://github.com/Marchhill/leanVM", rev = "e2bc4212013087c0823188e975b224ade286d878" } -primitives = { git = "https://github.com/Marchhill/leanVM", rev = "e2bc4212013087c0823188e975b224ade286d878" } -parallel = { git = "https://github.com/Marchhill/leanVM", rev = "e2bc4212013087c0823188e975b224ade286d878" } -zk_alloc = { git = "https://github.com/Marchhill/leanVM", rev = "e2bc4212013087c0823188e975b224ade286d878" } -bincode = "1.3" -tiny-keccak = { version = "2.0", features = ["keccak"] } - -[target.'cfg(all(target_os = "linux", target_env = "gnu"))'.dependencies] -libc = "0.2" +# dlopen cannot place jemalloc's default initial-exec TLS in a host process such as dotnet. +tikv-jemallocator = { version = "0.7", features = ["disable_initial_exec_tls"] } +rec_aggregation.workspace = true +signatures.workspace = true +primitives.workspace = true +parallel.workspace = true +bench.workspace = true +lean_compiler.workspace = true +leanvm_core.workspace = true +fiat_shamir.workspace = true + +[workspace] +members = ["crates/*"] +resolver = "3" + +[workspace.package] +version = "0.1.0" +edition = "2024" + +[workspace.lints.clippy] +needless_range_loop = "allow" +too_many_arguments = "allow" + +# Unmodified leanEthereum/leanVM, pinned. crates/rec_aggregation is vendored from the same revision and carries the +# EIP-8288 guest; crates/signatures supplies NiceTry v2 as lean_sphincs. +[workspace.dependencies] +primitives = { git = "https://github.com/leanEthereum/leanVM", rev = "e4dcb8d5436c9bf76398d890c57c59613af12956" } +fiat_shamir = { git = "https://github.com/leanEthereum/leanVM", rev = "e4dcb8d5436c9bf76398d890c57c59613af12956" } +pcs = { git = "https://github.com/leanEthereum/leanVM", rev = "e4dcb8d5436c9bf76398d890c57c59613af12956" } +keccak_gkr = { git = "https://github.com/leanEthereum/leanVM", rev = "e4dcb8d5436c9bf76398d890c57c59613af12956" } +leanvm_core = { git = "https://github.com/leanEthereum/leanVM", rev = "e4dcb8d5436c9bf76398d890c57c59613af12956" } +lean_compiler = { git = "https://github.com/leanEthereum/leanVM", rev = "e4dcb8d5436c9bf76398d890c57c59613af12956" } +bench = { git = "https://github.com/leanEthereum/leanVM", rev = "e4dcb8d5436c9bf76398d890c57c59613af12956" } +lean_da = { git = "https://github.com/leanEthereum/leanVM", rev = "e4dcb8d5436c9bf76398d890c57c59613af12956" } +parallel = { git = "https://github.com/leanEthereum/leanVM", rev = "e4dcb8d5436c9bf76398d890c57c59613af12956" } +verifier = { git = "https://github.com/leanEthereum/leanVM", rev = "e4dcb8d5436c9bf76398d890c57c59613af12956" } +upstream_signatures = { package = "signatures", git = "https://github.com/leanEthereum/leanVM", rev = "e4dcb8d5436c9bf76398d890c57c59613af12956" } +rec_aggregation = { path = "crates/rec_aggregation" } +signatures = { path = "crates/signatures" } +rand = "0.9" +tracing = "0.1.26" [profile.release] opt-level = 3 +lto = "thin" +codegen-units = 1 [[example]] name = "lean_startup_stub" diff --git a/src/ffi/LeanFfi.targets b/src/ffi/LeanFfi.targets deleted file mode 100644 index b72fa8a..0000000 --- a/src/ffi/LeanFfi.targets +++ /dev/null @@ -1,17 +0,0 @@ - - - $(MSBuildThisFileDirectory)Cargo.toml - $(MSBuildThisFileDirectory)target/release - nethermind_lean.dll - libnethermind_lean.dylib - libnethermind_lean.so - - - - - - - - - diff --git a/src/ffi/README.md b/src/ffi/README.md index 01c4707..b644cd6 100644 --- a/src/ffi/README.md +++ b/src/ffi/README.md @@ -1,11 +1,12 @@ # EIP-8288 native Lean integration -This adapter uses a temporary [leanVM mixed-recursion fork](https://github.com/Marchhill/leanVM/tree/e2bc4212013087c0823188e975b224ade286d878), -based on the Daisugi-compatible `f33f31bf7c1191667e29a68a3acae63b9164c1c6` revision. -The fork adds one recursive guest for Keccak SPHINCS claims, generic leanVM programs and its own child proofs; -`Cargo.lock` pins the exact dependency revision. Upstream replacement belongs behind the -existing `ILeanProofVerifier` interface and thin C ABI. Proofs, guest key and native bounds -must change together when the guest changes. No hash-based proof substitute is accepted. +This adapter proves on unmodified [leanEthereum/leanVM](https://github.com/leanEthereum/leanVM), pinned to +`e4dcb8d5436c9bf76398d890c57c59613af12956` in `Cargo.toml`; `Cargo.lock` pins every other dependency. The EIP-8288 +recursive guest and NiceTry v2 signatures live in `crates/`, vendored from the same revision with EIP-8288 changes on +top (see `crates/README.md`). The guest verifies leanSPHINCS (NiceTry v2) signatures, generic leanVM execution proofs +and its own child proofs. Upstream replacement belongs behind the existing `ILeanProofVerifier` interface and thin C +ABI. Proofs, guest key and native bounds must change together when the guest changes. No hash-based proof substitute +is accepted. ## Build and test @@ -13,46 +14,29 @@ Rust 1.99+ is required for the pinned upstream APIs. ```sh export LEANVM_NUM_THREADS=1 -(cd src/ffi && cargo test --release --locked -- --test-threads=1) -dotnet run --project src/Nethermind/Nethermind.Crypto.LeanFfi.Test/Nethermind.Crypto.LeanFfi.Test.csproj -c release -p:BuildLeanFfi=true -dotnet publish src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj -c release -p:BuildLeanFfi=true +(cd src/ffi && cargo test --release --locked --workspace -- --test-threads=1) ``` -Cargo runs from `src/ffi` so `.cargo/config.toml` applies `-C target-cpu=native`: the -field arithmetic needs carry-less multiply (`pclmulqdq` on x86_64, `aes` on aarch64) and is -about 68× slower without it. An environment `RUSTFLAGS` replaces those flags instead of adding -to them, so it must keep `target-cpu=native` (or the features); the crate refuses to build -otherwise. Because the library targets the build host's CPU, a `BuildLeanFfi` publish can crash -with an illegal instruction (SIGILL) on an older CPU: build on the oldest CPU that will run it, or -set `RUSTFLAGS` to that CPU plus the carry-less multiply feature (for example -`-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq`). Proving uses leanVM's arena allocator, which recycles the prover's transient buffers -across proofs; it allows one proof per process at a time, which the backend's prover lock ensures. -No arena phase opens while a verification runs; a verification waits only for an opening in progress (including the prover's input preparation, which verifies any leanSTARK inputs), then runs -alongside the proof node on its own slab rather than for the node (1–3 s on a 12-core arm64 host, about 7 s on 2 cores). -The arena keeps its touched slabs resident between proofs. leanVM also switches glibc's heap trimming and -large-allocation mmap off process-wide when it enables the arena; at the first proof the backend restores the -operator's `GLIBC_TUNABLES`/`MALLOC_*_` values or glibc's defaults, so the host's own freed memory still returns to -the kernel. glibc's adaptive mmap threshold stays off. - -`BuildLeanFfi=true` builds and copies the host native library into consumer build/publish -outputs and rejects a different target RID. Build on each target platform. The release Docker -images instead cross-compile the library for x86-64-v3 with PCLMULQDQ or arm64 with AES/PMULL and -ship it beside the binary. Ordinary builds do not require Rust. -A chain with a scheduled EIP-8288 fork checks the CPU features, native ABI and guest key at startup -and fails with installation instructions if the backend is unavailable. Other chains do -not load it. Verification also fails closed; proving requires the library. - -`cargo run --release --locked --example fixtures -- `, run from `src/ffi`, -produces genuine test signatures/proofs and the pinned recursive verification key. The live -FFI tests build these fixtures with `BuildLeanFfi=true` and fail if the native library cannot load. -The main solution compiles the test project without requiring Rust. Without the opt-in property, -the native suite is explicitly skipped. With it, the backend and fixtures are required and -missing libraries fail the run. - -The C ABI is version 10, with Cdecl calls, 32-byte hashes/keys and `size_t` buffer lengths. -`nlean_limits` writes nine u32 values (proof bytes, dependencies, recursive children, -SPHINCS witness bytes, leanSTARK claims per list, instructions, operand offset, aggregation -input bytes, mixed guest proof bytes); startup checks these against the managed protocol +Cargo runs from `src/ffi` so `.cargo/config.toml` applies `-C target-cpu=native` to local builds: the field +arithmetic needs carry-less multiply (`pclmulqdq` on x86_64, `aes` on aarch64) and is about 68× slower without it. +An environment `RUSTFLAGS` replaces those flags instead of adding to them, so it must keep `target-cpu=native` (or +the features); the crate refuses to build otherwise. **Build Lean** sets the packages' portable baselines this way +(`-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq`, and `+aes` on arm64). The library allocates through +jemalloc, the allocator upstream's prover timings assume (built with `disable_initial_exec_tls`, so `dlopen` from +the .NET host can load it); it serves only this library's allocations and keeps a proof's peak mapped for the next +one. One proof runs per process at a time, which the backend's prover lock ensures. + +A consumer on a chain with a scheduled EIP-8288 fork checks the native ABI, guest key and limits at startup and +fails if the backend is unavailable. Verification fails closed; proving requires the library. + +`cargo run --release --locked --example fixtures -- `, run from `src/ffi`, produces genuine test +signatures and proofs and prints the pinned recursive verification key; `fixtures --sign-message ` signs +one message. **Build Lean** packages the vectors, this tool and the `lean_startup_stub` example as +`Nethermind.Crypto.Lean.Testing` for consumers' native suites. + +The C ABI is version 12, with Cdecl calls, 32-byte hashes/keys and `size_t` buffer lengths. +`nlean_limits` writes seven u32 values (proof bytes, dependencies, recursive children, +SPHINCS witness bytes, aggregation input bytes, leanSTARK claims per list, leanSTARK instructions); startup checks these against the managed protocol bounds before accepting work. Verification/proving return 1 on success, 0 on failure and -1 for a contained panic. After -1, `nlean_last_error(out, len)` on the same thread copies up to `len` bytes of the panic message and returns its full length. Callers initialize proof output @@ -75,7 +59,7 @@ pinned key are prototype protocol choices, pending finalized EIP-8288 encodings. The serialized mixed guest proof is the whole `stark_proof`; an empty dependency list has an empty `stark_proof`. Blocks, wrappers and inclusion-list proofs reject a `stark_proof` above that local bound before decoding or verification; measured proofs of this guest are -265–326 KB. These bounds are not process memory guarantees. +246–360 KB. These bounds are not process memory guarantees. The current EIP-8288 companion places `stark_proof` in the block body and commits to `keccak256(stark_proof)` and `block_deps_hash` as two flat 32-byte header fields. Header synchronization therefore @@ -98,14 +82,11 @@ resolved dependency. * **SPHINCS witness:** public key (32 bytes), signature (6176 bytes). As in EIP-8288, the dependency key is the public key itself, root then public parameter. Both the key and the signature over `data_hash` are checked. -* **leanSTARK witness (`0x11`, a deviation, see below):** bytecode blob length, canonical - bytecode, then fixed-integer bincode serialization of the upstream CPU proof (no trailing - bytes). The dependency key is Keccak-256 of the 30-byte tag `ethereum/8288/leanSTARK/vk/v1` - plus one zero byte, followed by the canonical program encoding. `data_hash` is the VM's 32-byte - public commitment, packed into two 128-bit field cells. At most 2048 instructions and operand - offsets through 65535 are accepted. Each instruction is a 45-byte record: opcode byte, four - u32 operands, three u64 immediate limbs, then a u32 BLAKE2s metadata operand; opcodes 0–6 are - XOR, MUL, SET, DEREF, JUMP, BLAKE2s, SHA3, and unused fields are zero. +* **leanSTARK witness (`0x11`, a deviation, see below):** `kgen` (one byte, at most 11), the program's stacked + bytecode table (`16 · 2^kgen` little-endian u64 words, zero padding included, as upstream's `bytecode_table`), then + the upstream execution proof (`Proof::to_bytes`). The dependency key is upstream's program digest and Fiat-Shamir + IV, `keccak256("leanvm-v1" || table)`, so the key binds the program before the proof's first challenge. + `data_hash` is the program's 32-byte public input, packed into two 128-bit field cells. At most 2048 instructions. * **aggregation input:** direct count then `(96-byte dependency, witness blob)` pairs; child count then `(dependency count/triples, proof blob)` pairs; discard count/triples. * **aggregate:** the EIP-8288 `stark_proof`, one mixed guest proof without dependencies @@ -127,11 +108,11 @@ EIP-8288 (EIPs#12475/#12590) reserves `LEANSTARK_SCHEME` (`0x11`) until `get_dep defined for it. This implementation enables it anyway, as its one deliberate deviation from the EIP, with the pinned guest's generic leanVM CPU proof as its leanSTARK profile: -* A list of only leanSPHINCS (`0x10`) dependencies hashes exactly as the EIP specifies: - BLAKE2s-256 over the canonical dependencies' 64-byte `data_hash || verification_key` entries. +* `DEPS_HASH` is Keccak-256 (the EIP leaves it TBD). A list of only leanSPHINCS (`0x10`) dependencies hashes exactly + as the EIP specifies: Keccak-256 over the canonical dependencies' 64-byte `data_hash || verification_key` entries. Blocks, wrappers and inclusion lists without leanSTARK dependencies are unaffected. * A list with any leanSTARK dependency commits to - `BLAKE2s(get_deps_hash(leanSPHINCS subset) || get_deps_hash(leanSTARK subset) || domain)`, + `keccak256(get_deps_hash(leanSPHINCS subset) || get_deps_hash(leanSTARK subset) || domain)`, where each inner digest is the spec form over that scheme's entries and `domain` is `eip8288/leanstark-deps` zero-padded to 32 bytes. The 96-byte preimage is never a list of 64-byte entries, so no leanSPHINCS-only list shares a digest with it, and the same data and @@ -144,38 +125,34 @@ EIP, with the pinned guest's generic leanVM CPU proof as its leanSTARK profile: statement over it to the prover, a received mode-1 wrapper over it is invalid and an inclusion-list proof over it imposes no obligation, both without penalty or verification. -Startup checks ABI 10, the pinned key and the nine bounds together. New devnets require fresh +Startup checks ABI 12, the pinned key and the seven bounds together. New devnets require fresh chain and database namespaces. The latest companion specification has no block dependency quota: blocks are bounded by gas, mode-0 wrappers by `MAX_LEANSIG_DEPS_PER_WRAPPER`, `MAX_LEANSTARK_DEPS_PER_WRAPPER` and `MAX_WRAPPER_BYTES`, and mode-1 wrappers by `MAX_WRAPPER_BYTES` and the dependency-list limit of the circuit behind `AGGREGATED_VK`, which EIP-8288 fixes from benchmarks before activation. This prototype guest's limit is 256 dependencies, at most 16 of them leanSTARK; a received mode-1 wrapper over it is invalid and discarded before verification, without penalizing its sender. Blocks have no such limit in the draft, so this key cannot prove every block the draft permits and this prototype is not full-capacity conformance. Raising the bound changes the guest and so `AGGREGATED_VK`. Local inability to prove or retain a set is not an inclusion-list omission excuse. Full activation requires a verifier and proof format supporting every set the fork's gas rules permit, with the complete construction, including recursive merges and discards, benchmarked against the slot deadline. ## Proving resources -This Keccak profile has a much larger raw proving witness than the former BLAKE2s -profile. The adapter verifies every input, proves at most four unique selected signatures -per leaf, and combines at most two children at each recursive node. Discards are applied -to declared claims at every level. Larger signature sets use genuine recursive proofs -rather than one large raw witness. Small steps bound witness growth without guaranteeing -process memory usage; proving calls are serialized within the native backend. The pinned -Rust toolchain is selected from this directory, including MSBuild's Cargo invocations. -CI compiles for its host CPU and restores compiled artifacts only when CPU features, -architecture, toolchain and locked dependencies match. +One `nlean_prove_recursive` call proves its whole selected list as one guest node over its direct witnesses and child +proofs; the managed builder decides how statements are split, merged and reused. Every input is verified, including +witnesses a child already covers or a discard drops. Proving calls are serialized within the native backend. Measured +on a 32-core x86_64 host: a 40-signature leaf proves in about 0.43 s, a two-child merge in 0.28 s, sixteen small +leanSTARK dependencies in 1.4 s, and every proof verifies in 5-7 ms (`crates/rec_aggregation/examples/agg_bench.rs`). +Peak resident memory reached 4.2 GiB in those runs; nothing here bounds it. The pinned Rust toolchain is selected from +this directory, including MSBuild's Cargo invocations. CI compiles for its host CPU and restores compiled artifacts +only when CPU features, architecture, toolchain and locked dependencies match. ## Proof verification limits -Before decoding, a zero-allocation pass checks proof vector lengths and transcript geometry -against available bytes and the selected guest profile. Decoding bounds -are prototype acceptance choices beyond the unrestricted EIP; interoperating nodes must +Before decoding, upstream's wire decoder checks every length against the available bytes and its own phase, query and +height caps. Decoding bounds are prototype acceptance choices beyond the unrestricted EIP; interoperating nodes must share the pinned guest, format and bounds. -The mixed profile uses PCS inverse-rate log 1. Mixed child proofs permit committed-polynomial -log size at most 27 and bus log size at most 26. The host and guest enforce these geometry -bounds, in addition to dependency and serialized-byte limits. A valid unrestricted upstream proof outside this profile is not admitted. +The guest proves at PCS inverse-rate log 1 and compiles in fixed capacities for the child and leanSTARK proofs it +verifies (`MU_CAP`, `STREAM_CAP` in `crates/rec_aggregation/src/aggregation/child.rs`). The native leanSTARK check +applies the same capacities, so a valid unrestricted upstream proof outside them is not admitted. -The upstream project and temporary guest remain experimental and unaudited. The output -cap and bounded native inputs do not bound native workspace or RSS. The adapter's -four-direct/two-child fold policy reduces individual work without guaranteeing that every -256-claim workload fits available RAM or a slot deadline. +The upstream project and this guest remain experimental and unaudited. The output cap and bounded native inputs do +not bound native workspace or RSS. ## Running an EIP-8288 node @@ -324,7 +301,11 @@ it gets one outstanding request instead of four. It is never disconnected for it Every EIP-8288 `AGGREGATION_INTERVAL` (one second), a tracked aggregation worker produces mode-1 wrappers over bounded subsets until every pending dependency-bearing transaction is covered or -one interval of proving has elapsed; the next cycle resumes the rotation where it stopped. Ticks +one interval of proving has elapsed; the next cycle resumes the rotation where it stopped. Only +transactions executable at the head are candidates: no nonce gap from the sender's state nonce, and +fees per gas covering the next block's base fee after two maximal decreases (49/64 of it on mainnet), +and per blob gas the blob base fee after two blocks without blobs; +the others stay pooled with their direct witnesses until they qualify. Ticks coalesce while proving. Subsets are transaction-hash-prefix buckets: the candidates in canonical order start as one bucket, and one that does not fit is subdivided by successive hash bits. Buckets do not depend on arrival order, but splits depend on the held proofs through the proving-cost diff --git a/src/ffi/about.toml b/src/ffi/about.toml index b79a8d2..325a0e1 100644 --- a/src/ffi/about.toml +++ b/src/ffi/about.toml @@ -4,8 +4,17 @@ ignore-dev-dependencies = true accepted = ["Apache-2.0", "MIT", "BSD-2-Clause", "BSD-3-Clause", "Zlib", "CC0-1.0", "Unlicense", "Unicode-3.0"] -# leanVM (Marchhill/leanVM, fork of leanEthereum/leanVM) crates declare no license field; the repository ships -# LICENSE-MIT and LICENSE-APACHE, read here as the Rust convention MIT OR Apache-2.0. +# leanEthereum/leanVM crates declare no license field; the repository ships LICENSE-MIT and LICENSE-APACHE, read here +# as the Rust convention MIT OR Apache-2.0. +[bench.clarify] +license = "MIT OR Apache-2.0" +[[bench.clarify.git]] +path = "LICENSE-MIT" +checksum = "a6c93f24f002f1f83604f5b728f6f7ef58dcfd9ab4d7201971b0dc8ceb36e09f" +[[bench.clarify.git]] +path = "LICENSE-APACHE" +checksum = "ef978bc43daeb1015261f2a57aaad1b76e564836209beca219f2f8d485950f37" + [fiat_shamir.clarify] license = "MIT OR Apache-2.0" [[fiat_shamir.clarify.git]] @@ -15,12 +24,12 @@ checksum = "a6c93f24f002f1f83604f5b728f6f7ef58dcfd9ab4d7201971b0dc8ceb36e09f" path = "LICENSE-APACHE" checksum = "ef978bc43daeb1015261f2a57aaad1b76e564836209beca219f2f8d485950f37" -[flock.clarify] +[keccak_gkr.clarify] license = "MIT OR Apache-2.0" -[[flock.clarify.git]] +[[keccak_gkr.clarify.git]] path = "LICENSE-MIT" checksum = "a6c93f24f002f1f83604f5b728f6f7ef58dcfd9ab4d7201971b0dc8ceb36e09f" -[[flock.clarify.git]] +[[keccak_gkr.clarify.git]] path = "LICENSE-APACHE" checksum = "ef978bc43daeb1015261f2a57aaad1b76e564836209beca219f2f8d485950f37" @@ -42,12 +51,12 @@ checksum = "a6c93f24f002f1f83604f5b728f6f7ef58dcfd9ab4d7201971b0dc8ceb36e09f" path = "LICENSE-APACHE" checksum = "ef978bc43daeb1015261f2a57aaad1b76e564836209beca219f2f8d485950f37" -[lean_vm.clarify] +[leanvm_core.clarify] license = "MIT OR Apache-2.0" -[[lean_vm.clarify.git]] +[[leanvm_core.clarify.git]] path = "LICENSE-MIT" checksum = "a6c93f24f002f1f83604f5b728f6f7ef58dcfd9ab4d7201971b0dc8ceb36e09f" -[[lean_vm.clarify.git]] +[[leanvm_core.clarify.git]] path = "LICENSE-APACHE" checksum = "ef978bc43daeb1015261f2a57aaad1b76e564836209beca219f2f8d485950f37" @@ -78,39 +87,30 @@ checksum = "a6c93f24f002f1f83604f5b728f6f7ef58dcfd9ab4d7201971b0dc8ceb36e09f" path = "LICENSE-APACHE" checksum = "ef978bc43daeb1015261f2a57aaad1b76e564836209beca219f2f8d485950f37" -[rec_aggregation.clarify] +[signatures.clarify] license = "MIT OR Apache-2.0" -[[rec_aggregation.clarify.git]] +[[signatures.clarify.git]] path = "LICENSE-MIT" checksum = "a6c93f24f002f1f83604f5b728f6f7ef58dcfd9ab4d7201971b0dc8ceb36e09f" -[[rec_aggregation.clarify.git]] +[[signatures.clarify.git]] path = "LICENSE-APACHE" checksum = "ef978bc43daeb1015261f2a57aaad1b76e564836209beca219f2f8d485950f37" -[sphincs.clarify] +[verifier.clarify] license = "MIT OR Apache-2.0" -[[sphincs.clarify.git]] +[[verifier.clarify.git]] path = "LICENSE-MIT" checksum = "a6c93f24f002f1f83604f5b728f6f7ef58dcfd9ab4d7201971b0dc8ceb36e09f" -[[sphincs.clarify.git]] +[[verifier.clarify.git]] path = "LICENSE-APACHE" checksum = "ef978bc43daeb1015261f2a57aaad1b76e564836209beca219f2f8d485950f37" -[xmss.clarify] -license = "MIT OR Apache-2.0" -[[xmss.clarify.git]] -path = "LICENSE-MIT" -checksum = "a6c93f24f002f1f83604f5b728f6f7ef58dcfd9ab4d7201971b0dc8ceb36e09f" -[[xmss.clarify.git]] -path = "LICENSE-APACHE" -checksum = "ef978bc43daeb1015261f2a57aaad1b76e564836209beca219f2f8d485950f37" - -[zk_alloc.clarify] +# The vendored crates under crates/ derive from leanEthereum/leanVM and keep its license files. +[rec_aggregation.clarify] license = "MIT OR Apache-2.0" -[[zk_alloc.clarify.git]] +[[rec_aggregation.clarify.files]] path = "LICENSE-MIT" checksum = "a6c93f24f002f1f83604f5b728f6f7ef58dcfd9ab4d7201971b0dc8ceb36e09f" -[[zk_alloc.clarify.git]] +[[rec_aggregation.clarify.files]] path = "LICENSE-APACHE" checksum = "ef978bc43daeb1015261f2a57aaad1b76e564836209beca219f2f8d485950f37" - diff --git a/src/ffi/crates/README.md b/src/ffi/crates/README.md new file mode 100644 index 0000000..50a8577 --- /dev/null +++ b/src/ffi/crates/README.md @@ -0,0 +1,22 @@ +# Vendored leanVM crates + +`rec_aggregation` and `signatures` derive from [leanEthereum/leanVM](https://github.com/leanEthereum/leanVM) at +`e4dcb8d5436c9bf76398d890c57c59613af12956`, the revision `../Cargo.toml` pins for every other leanVM crate. Upstream +licenses them under MIT or Apache-2.0 (each crate's `LICENSE-MIT`, `LICENSE-APACHE`). The core (`leanvm_core`, `verifier`, +`lean_compiler`, `fiat_shamir`, `pcs`, `primitives`) stays an unmodified git dependency. + +Changes against upstream, all EIP-8288 specific: + +- `signatures::lean_sphincs`: NiceTry v2 (SPHINCS- v2) parameters and encoding, the wallet's signature scheme. The vendored + copy drops leanXMSS; test-only mock signers come from the upstream crate. +- `rec_aggregation`: the aggregation guest (`guests/lean_ethereum.py`) commits to the EIP-8288 dependency digest with + Keccak-256, verifies NiceTry v2 signatures (scheme `0x10`) and leanVM execution proofs of programs up to 2^11 + instructions (scheme `0x11`, `src/aggregation/generic.rs`), and merges or discards child aggregates. + +`crates/upstream.sh diff` prints every change against upstream at the pinned revision; `crates/upstream.sh update ` +moves the crates and the pin to a newer upstream revision by three-way merge and lists what needs review. After an +update, run the workspace tests and `fixtures`, and when `AGGREGATED_VK` changes, bump the package version and the +consumers' pinned key together. + +Every change to a guest changes `AGGREGATED_VK`; `../examples/fixtures.rs` prints it and the native tests check it +against `Eip8288Constants.AggregatedVk`. diff --git a/src/ffi/crates/rec_aggregation/Cargo.toml b/src/ffi/crates/rec_aggregation/Cargo.toml new file mode 100644 index 0000000..9376f6e --- /dev/null +++ b/src/ffi/crates/rec_aggregation/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "rec_aggregation" +version.workspace = true +edition.workspace = true +publish = false + +[lints] +workspace = true + +[dependencies] +primitives.workspace = true +fiat_shamir.workspace = true +parallel.workspace = true +pcs.workspace = true +keccak_gkr.workspace = true +leanvm_core.workspace = true +lean_compiler.workspace = true +signatures.workspace = true +lean_da.workspace = true +verifier.workspace = true +rand = { workspace = true, optional = true } +tracing.workspace = true + +[features] +# Malicious-host switches for the rejection tests. Never enable in a prover that ships. +forge = [] +# `signers_cache`: cached benchmark signers over leanXMSS mock keys, for tests and the CLI. +signers-cache = ["signatures/mock", "dep:rand"] + +[dev-dependencies] +signatures = { workspace = true, features = ["mock"] } +rand.workspace = true diff --git a/src/ffi/crates/rec_aggregation/LICENSE-APACHE b/src/ffi/crates/rec_aggregation/LICENSE-APACHE new file mode 100644 index 0000000..ea7ef84 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for describing the origin of the Work and + reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Support. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or support. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Succinct Labs, Benedikt Bunz, and William Wang + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing permissions + and limitations under the License. \ No newline at end of file diff --git a/src/ffi/crates/rec_aggregation/LICENSE-MIT b/src/ffi/crates/rec_aggregation/LICENSE-MIT new file mode 100644 index 0000000..cafe401 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 leanEthereum + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/ffi/crates/rec_aggregation/examples/agg_bench.rs b/src/ffi/crates/rec_aggregation/examples/agg_bench.rs new file mode 100644 index 0000000..b95cdc1 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/examples/agg_bench.rs @@ -0,0 +1,149 @@ +//! Shape and cost of fixed aggregation workloads, at log_inv_rate 1. `agg_bench sigs [n]` proves two disjoint +//! leanSPHINCS leaves of `n` signatures and their 2-children merge; `agg_bench generic` proves leaves of leanSTARK +//! dependencies (1 small, 1 of 2^11 instructions, 16 small, and 40 signatures + 4 small). + +use lean_compiler::{compile, parse}; +use leanvm_core::cpu::{Program, bytecode_table, prove}; +use rec_aggregation::aggregation::unified_guest; +use rec_aggregation::{EthereumProof, RawGeneric, aggregate_with_stats, generic_verification_key, generic_witness}; +use signatures::lean_sphincs; +use std::time::Instant; + +const RATE: usize = 1; + +type RawSphincs = ( + lean_sphincs::LeanSphincsPublicKey, + lean_sphincs::Message, + lean_sphincs::LeanSphincsSignature, +); + +fn signers(n: usize, salt: u8) -> Vec { + (0..n) + .map(|i| { + let mut seed = [salt; 32]; + seed[..8].copy_from_slice(&(i as u64).to_le_bytes()); + let (sk, pk) = lean_sphincs::key_gen_from_seed(seed); + let mut message = [7; 32]; + message[0] = salt; + message[31] = i as u8; + (pk, message, lean_sphincs::sign(&sk, &message).expect("signs")) + }) + .collect() +} + +/// Publishes `keccak256` of a hinted 64-byte preimage. +const PREIMAGE: &str = "\ +def main(): + pre = StackBuf(4) + hint_witness(pre, \"pre\") + c = StackBuf(2) + keccak(pre[0:2], pre[2:4], c) + p = 1 + p[1] = c[0] + p[GEN] = c[1] + return +"; + +/// Accepts any canonical public input after 900 multiply-adds: 2^11 instructions. +fn wide_source() -> String { + let mut src = String::from("def main():\n h = StackBuf(2)\n hint_witness(h, \"pi\")\n acc = h[0]\n"); + src += &" acc = acc * h[1] + h[0]\n".repeat(900); + src + " p = 1\n p[1] = h[0]\n p[GEN] = h[1]\n return\n" +} + +fn cells(digest: &[u8; 32]) -> [primitives::field::F192; 2] { + fiat_shamir::merkle::hash_to_scalars(digest) +} + +fn dependency(program: &Program, witness: (&str, Vec), data_hash: [u8; 32]) -> RawGeneric { + let table: Vec = bytecode_table(program.prog()).iter().map(|w| w.0).collect(); + let mut program = program.clone(); + program.set_witness(witness.0, vec![witness.1]); + let (proof, _) = prove(&program, cells(&data_hash), RATE).expect("the dependency proves"); + let vk = generic_verification_key(&table).expect("within the cap"); + (data_hash, vk, generic_witness(&table, &proof).expect("a witness")) +} + +fn preimage_deps(n: usize) -> Vec { + let program = compile(&parse(PREIMAGE).expect("parse")); + (0..n) + .map(|i| { + let pre = [i as u8 + 1; 64]; + let pre_cells = pre.chunks(16).map(|c| { + primitives::field::F192::new( + u64::from_le_bytes(c[..8].try_into().unwrap()), + u64::from_le_bytes(c[8..].try_into().unwrap()), + 0, + ) + }); + dependency(&program, ("pre", pre_cells.collect()), primitives::hash::hash(&pre)) + }) + .collect() +} + +fn report(name: &str, run: impl FnOnce() -> (EthereumProof, leanvm_core::cpu::Stats)) -> EthereumProof { + let start = Instant::now(); + let (proof, stats) = run(); + let prove = start.elapsed().as_secs_f64(); + let start = Instant::now(); + proof.verify().expect("verifies"); + let verify = start.elapsed().as_secs_f64() * 1e3; + println!( + "{name}: cycles {}, prove {prove:.3} s, verify {verify:.2} ms, proof {} B, log_mem {}, committed 2^{:.2}", + stats.cycles, + proof.proof_bytes().len(), + stats.log_mem, + (stats.committed as f64).log2() + ); + proof +} + +fn main() { + leanvm_core::init_prover(); + let mode = std::env::args().nth(1).unwrap_or_else(|| "sigs".into()); + rec_aggregation::warm_up(); + let guest = unified_guest(); + let vk: String = rec_aggregation::aggregated_vk().iter().map(|b| format!("{b:02x}")).collect(); + println!( + "guest code_len {}, bytecode log {}, aggregated vk {vk}", + guest.code_len(), + guest.prog().len().trailing_zeros() + ); + let leaf = |deps: Vec, sigs: Vec| { + move || aggregate_with_stats(&[], deps, sigs, &[], None, RATE).expect("proves") + }; + match mode.as_str() { + "sigs" => { + let n: usize = std::env::args().nth(2).map_or(40, |a| a.parse().expect("n")); + let left = report(&format!("leaf {n} sigs"), leaf(vec![], signers(n, 1))); + let right = report(&format!("leaf {n} sigs (2nd)"), leaf(vec![], signers(n, 2))); + report("merge 2 children", || { + aggregate_with_stats(&[left, right], vec![], vec![], &[], None, RATE).expect("proves") + }); + } + "generic" => { + let wide = compile(&parse(&wide_source()).expect("parse")); + let small = compile(&parse(PREIMAGE).expect("parse")).prog().len(); + println!("small program {small} instructions, wide program {}", wide.prog().len()); + let deps = preimage_deps(16); + report("leaf 1 small generic", leaf(deps[..1].to_vec(), vec![])); + let data_hash = [9; 32]; + let wide_dep = dependency(&wide, ("pi", cells(&data_hash).to_vec()), data_hash); + report("leaf 1 generic of 2^11 instructions", leaf(vec![wide_dep], vec![])); + report("leaf 16 small generic", leaf(deps.clone(), vec![])); + report( + "leaf 40 sigs + 4 small generic", + leaf(deps[..4].to_vec(), signers(40, 3)), + ); + } + _ => panic!("mode: sigs [n] | generic"), + } + let status = std::fs::read_to_string("/proc/self/status").unwrap_or_default(); + println!( + "{}", + status + .lines() + .find(|l| l.starts_with("VmHWM")) + .unwrap_or("VmHWM unknown") + ); +} diff --git a/src/ffi/crates/rec_aggregation/guests/lean_ethereum.py b/src/ffi/crates/rec_aggregation/guests/lean_ethereum.py new file mode 100644 index 0000000..ea2dadf --- /dev/null +++ b/src/ffi/crates/rec_aggregation/guests/lean_ethereum.py @@ -0,0 +1,4472 @@ +# The recursive aggregation guest: zkDSL, not runnable Python (see +# crates/lean_compiler/zkDSL.md). One node of an aggregation tree verifies raw leanSTARK +# (0x11) proofs of other programs, leanSPHINCS signatures and sub-proofs OF THIS SAME +# BYTECODE, and publishes the statement digest binding its dependency claims and at most one LeanDA root: that of a +# blob matrix it checks directly, or one of its children's, passed on. +# Reading order: `main` is the +# node, `verify_sub` the in-circuit copy of `verifier::verify_execution`, and +# `open_stacked` the WHIR opening it dispatches into. +# +# Every `*_PLACEHOLDER` below is filled by the host at compile time +# (rec_aggregation::aggregation::placeholder_map), so one source serves every inner +# shape. Names follow doc/leanvm/preamble/macros.tex. +from snark_lib import * + +# ---------------------------------------------------------------- proof stream +# The proof stream rides ONE padded witness hint (the guest walks only the prefix +# the shape dictates); binding always comes from the per-word absorbs. +STREAM_CAP = STREAM_CAP_PLACEHOLDER +MIN_LOG_MEM = MIN_LOG_MEM_PLACEHOLDER +MAX_LOG_MEM = MAX_LOG_MEM_PLACEHOLDER +INV_GEN = INV_GEN_PLACEHOLDER + +# ------------------------------------------------------------- the field, GF(2^192) +# Tower F192 = F64[Y]/(Y^3+Y+1). Y_TOWER embeds Y for reassembling +# e192(lo,hi,top) = lo + hi*Y + top*Y², and Y_INV also deduces a top limb at the +# opening boundary once the low and high limbs are transmitted. COORD_BASIS is the +# coordinate basis e_i of F192 (which spans the WHOLE field, unlike the g-power +# basis GEN**i, which spans only F64): hint_decompose_bits emits a word's +# coordinate bits, so a value reconstructs as Σ b_i·COORD_BASIS[i]. +FIELD_BITS = 192 +# A query position's path bits are dispatched on QW at a time (`walk_path`), and the windows one +# squeezed word's positions take fill at most QUERY_WINDOWS_CAP cells (`decode_query_windows`). +QW = QW_PLACEHOLDER +QUERY_WINDOWS_CAP = QUERY_WINDOWS_CAP_PLACEHOLDER +BASE_FIELD_BITS = 64 +Y_TOWER = Y_TOWER_PLACEHOLDER +Y_INV = Y_INV_PLACEHOLDER +COORD_BASIS = COORD_BASIS_PLACEHOLDER +# Six challenges compose the F2-linear map that batches the 192 transposed +# ring-switch coordinates. +RING_MAP_SHIFTS = [32, 16, 8, 4, 2, 1] +RS_CHUNK = 8 # ring-switch rows (target) or coordinates (weight) per loop iteration +RS_CHUNK_BITS = 3 +RES_BATCH = 8 # residual queries per loop iteration +RS_COUNT_BITS = 6 # a ring-switch coordinate count is below 2^RS_COUNT_BITS +# Exponent bit-widths, wider than the 2^30 caps need: a count's log is below +# COUNT_BITS; a structural size (sums of 2^kappa, packing offsets) fits SIZE_BITS +# bits; and a structural LOG announced as an integer word (log_mem, log_inv_rate), +# raised to a g-power by g_power_of_word, is below SIZE_BITS, so LOG_WORD_BITS bits +# make the reconstruction the bound (a larger log cannot reproduce itself from them). +COUNT_BITS = 33 +SIZE_BITS = 34 +LOG_WORD_BITS = 6 + +# ------------------------------------------------------------------- Fiat-Shamir +# An absorbed scalar's fourth lane is its domain tag (fs_pack), so a role is never +# smuggled through the data lanes. The capacity a sponge carries is FS_CELLS cells. +DS_OBSERVE = 1 +DS_POW_NONCE = 4 +FS_CELLS = 5 + +# ----------------------------------------------------------- loop-carried chains +# A loop whose state is several fields keeps ONE heap run of N cells per iteration, +# so a step is one pointer multiply rather than one per field. +PAIR_SLOTS = 2 # a two-field chain +# A record's Fiat-Shamir sponge is its FS_CELLS capacity cells, from field *_FS on. +# A sponge with a full rate waiting: its capacity, then those eight rate cells. +PEND_SLOTS = 13 +# One sumcheck round of the bus GKR or the table batch. +ROUND_FS = 0 +ROUND_CURSOR = 5 +ROUND_CLAIM = 6 +ROUND_SLOTS = 7 +# One product layer of the bus GKR. +LAYER_FS = 0 +LAYER_CURSOR = 5 +LAYER_PUSH = 6 +LAYER_PULL = 7 +LAYER_COUNT = 8 +LAYER_ROW = 9 +LAYER_POS = 10 +LAYER_HALF = 11 # g^(k // 4) at radix-four layer k +LAYER_PARITY = 12 # g^(k % 4) +LAYER_PEND = 13 # the four rate cells the layer's first permutation takes: the roots', then zero +LAYER_SLOTS = 17 +# One OOD sample of a WHIR level. +OOD_BETA = 0 +OOD_Y = 1 +OOD_C0 = 2 +OOD_C2 = 3 +OOD_SLOTS = 4 + +# ---------------------------------------------------- the bus: sides and blocks +# GKR sides. The layer counts mu_s are hinted and certified from the block kappas; +# GKR_ROUNDS_CAP caps the per-tree round positions (triangle rounds plus one slot +# per layer) and GKR_POINTS_CAP the point triangle (rows x MU_CAP). +PUSH_SIDE = 0 +PULL_SIDE = 1 +COUNT_SIDE = 2 +N_GKR_SIDES = 3 +GKR_ROUNDS_CAP = GKR_ROUNDS_CAP_PLACEHOLDER +MU_CAP = MU_CAP_PLACEHOLDER +GKR_POINTS_CAP = GKR_POINTS_CAP_PLACEHOLDER +# Bus blocks, flattened across the 3 sides (side s covers blocks +# [SIDE_BLOCK_START[s], SIDE_BLOCK_START[s+1])). The block STRUCTURE is +# protocol-fixed and baked: each block's coord range [BLOCK_COORD_OFF, +# +BLOCK_COORD_COUNT), per coord its COORD_TYPE kind (mirroring leaf.rs::Coord), +# COORD_CONST (the const value, a product's or gcol's g^k, else 0), and the kappa +# SOURCE map (BLOCK_KAPPA_SRC/ADJ: 0 = const adj, 1 = log_mem, 2+t = tau_t). The +# block SHAPES are all reconstructed at runtime from the certified logs: kappa +# directly, the selector bits by pinned advice-decompositions. BLOCK_TABLE names +# the table a block's flush belongs to, or NO_TABLE for the framework blocks +# (boundary, memory seed/finalize, bytecode seed/finalize); it is also what marks a +# block as owned, an owned block's fingerprint being settled by the table sumcheck +# off its table's column evaluations. +COORD_KIND_CONST = 0 +COORD_KIND_COL = 1 +COORD_KIND_GCOL = 2 +COORD_KIND_INDEX = 3 +COORD_KIND_PUBLIC = 4 +COORD_KIND_PROD = 5 +NO_TABLE = NO_TABLE_PLACEHOLDER +SIDE_BLOCK_START = SIDE_BLOCK_START_PLACEHOLDER +N_BLOCKS = N_BLOCKS_PLACEHOLDER +BLOCK_KAPPA_SRC = BLOCK_KAPPA_SRC_PLACEHOLDER +BLOCK_KAPPA_ADJ = BLOCK_KAPPA_ADJ_PLACEHOLDER +BLOCK_TABLE = BLOCK_TABLE_PLACEHOLDER +BLOCK_SIDE = BLOCK_SIDE_PLACEHOLDER +N_BLOCK_GROUPS = N_BLOCK_GROUPS_PLACEHOLDER +BLOCK_GROUP_START = BLOCK_GROUP_START_PLACEHOLDER # a block group's first block, then N_BLOCKS +SIDE_GROUP_START = SIDE_GROUP_START_PLACEHOLDER # a side's first block group, then N_BLOCK_GROUPS +BLOCK_COORD_OFF = BLOCK_COORD_OFF_PLACEHOLDER +BLOCK_COORD_COUNT = BLOCK_COORD_COUNT_PLACEHOLDER +COORD_TYPE = COORD_TYPE_PLACEHOLDER +COORD_CONST = COORD_CONST_PLACEHOLDER +# The pull boundary's final pc g^(2^kbc - 1), read off the runtime bytecode log, and the +# kappa source of that log (after log_mem and the table logs). +FINAL_PC_COORD = FINAL_PC_COORD_PLACEHOLDER +KBC_SRC = KBC_SRC_PLACEHOLDER +# A table coord's value on its table's padding row (doc sec:e2e-pad), zero for a +# framework coord: a block's rows past its table's announced count flush nothing, +# their leaves being 1, so the decompose adds back what its form sums there. +COORD_PADDING = COORD_PADDING_PLACEHOLDER +# Claim dedup: push/pull share their GKR point, so a column read by two blocks with +# the same kappa (across OR within the sides) is streamed and opened ONCE. +# COORD_FRESH = 1 on the first occurrence (read the stream, fill pool slot +# COORD_CLAIM_SLOT), 0 on a duplicate (reuse that slot). +COORD_FRESH = COORD_FRESH_PLACEHOLDER +COORD_FRESH_POS = COORD_FRESH_POS_PLACEHOLDER # a fresh coordinate's place among the words the decomposition absorbs +N_FRESH = N_FRESH_PLACEHOLDER +COORD_CLAIM_SLOT = COORD_CLAIM_SLOT_PLACEHOLDER +# A TABLE block's coordinates, flattened into TERMS: coord c is +# Σ_{j < COORD_TERM_COUNT[c]} term(COORD_TERM_OFF[c] + j), each term a TERM_TYPE +# kind over that table's LOCAL column indices TERM_COL_A/TERM_COL_B, scaled by +# TERM_CONST. Those coords raise no claim (the table sumcheck settles them), which +# is what lets one carry a value the row DERIVES from its columns: an XOR/MUL +# result, a DEREF store, a JUMP successor. A framework coord has no terms. +COORD_TERM_OFF = COORD_TERM_OFF_PLACEHOLDER +COORD_TERM_COUNT = COORD_TERM_COUNT_PLACEHOLDER +TERM_TYPE = TERM_TYPE_PLACEHOLDER +TERM_CONST = TERM_CONST_PLACEHOLDER +TERM_COL_A = TERM_COL_A_PLACEHOLDER +TERM_COL_B = TERM_COL_B_PLACEHOLDER +COORD_DIFF_OFF = COORD_DIFF_OFF_PLACEHOLDER # a pull coordinate less its push twin's, as terms +COORD_DIFF_COUNT = COORD_DIFF_COUNT_PLACEHOLDER +N_BUS_CLAIMS = N_BUS_CLAIMS_PLACEHOLDER +INDEX_MLE_FACTORS = INDEX_MLE_FACTORS_PLACEHOLDER # 1 + g^(2^i) +# Committed-coordinate claims (Col/GCol coords across all sides) and the deferred +# bytecode values (Public coords). +N_CLAIMS = N_CLAIMS_PLACEHOLDER +# A bus tuple's coordinates index the 2^N_TUPLE_BITS fingerprint slots (doc +# sec:gp). The stacked bytecode has BYTECODE_COLS encoding columns, stacked along +# LOG2_BYTECODE_COLS selector bits into ONE multilinear; push and pull share their +# GKR point, so the columns are opened ONCE. +N_TUPLE_BITS = 4 +N_TUPLE_SLOTS = 16 +BYTECODE_COLS = BYTECODE_COLS_PLACEHOLDER +LOG2_BYTECODE_COLS = LOG2_BYTECODE_COLS_PLACEHOLDER + +# --------------------------------------------------------------- the six tables +# The table sumcheck's batch carries EVERY committed column of a table, because its +# bus forms read the flushed ones and its constraint the rest; TABLE_COLS_CAP caps +# the evaluation frame. ETA_OFFSET[t] starts table t's disjoint range of zc_xi +# powers; the three bus forms take ETA_FORM_BASE + side, the SAME three powers for +# every table, and that sharing is what makes the batch's target derivable from the +# three leaf claims. FLOORS[t] is the table's least log height (the Keccak reduction +# is sized for at least one block of 2^9 steps). +TABLE_JUMP = 4 +TABLE_KECCAK = 5 +N_TABLES = N_TABLES_PLACEHOLDER +FLOORS = FLOORS_PLACEHOLDER +N_TABLE_COLS = N_TABLE_COLS_PLACEHOLDER +TABLE_COL_BASE = TABLE_COL_BASE_PLACEHOLDER # a table's first column among all the evaluations +N_COL_EVALS = N_COL_EVALS_PLACEHOLDER +TABLE_COLS_CAP = TABLE_COLS_CAP_PLACEHOLDER +ETA_OFFSET = ETA_OFFSET_PLACEHOLDER +ETA_FORM_BASE = ETA_FORM_BASE_PLACEHOLDER +N_ETA_POWS = N_ETA_POWS_PLACEHOLDER + +# ------------------------------------------------- Keccak, by GKR over its rounds +# q_keccak holds a step's state every KECCAK_DEPTH rounds but its output, a lane per +# slot and a word per step; a pack is KECCAK_PACK consecutive steps, embedded in a polynomial of +# degree under KECCAK_EMBED_BITS whose products have KECCAK_PROD_BITS coefficients. +# The *_START / *_IDX pairs are index lists: row i is IDX[START[i] .. START[i + 1]]. +KECCAK_DEPTH = KECCAK_DEPTH_PLACEHOLDER +KECCAK_SEGMENTS = KECCAK_SEGMENTS_PLACEHOLDER +KECCAK_SEG_LOG = KECCAK_SEG_LOG_PLACEHOLDER +KECCAK_PACK = KECCAK_PACK_PLACEHOLDER +KECCAK_PACK_LOG = 4 +KECCAK_EMBED_BITS = KECCAK_EMBED_BITS_PLACEHOLDER +KECCAK_PROD_BITS = KECCAK_PROD_BITS_PLACEHOLDER +KECCAK_PROJ_A = KECCAK_PROJ_A_PLACEHOLDER +KECCAK_PROJ_B = KECCAK_PROJ_B_PLACEHOLDER +KECCAK_PROJ_OUT = KECCAK_PROJ_OUT_PLACEHOLDER +KECCAK_PROJ_CELLS = KECCAK_PROJ_CELLS_PLACEHOLDER +KECCAK_FAC_START = KECCAK_FAC_START_PLACEHOLDER +KECCAK_FAC_IDX = KECCAK_FAC_IDX_PLACEHOLDER +KECCAK_FAC_DEG = KECCAK_FAC_DEG_PLACEHOLDER +KECCAK_EMB_A = KECCAK_EMB_A_PLACEHOLDER +KECCAK_EMB_B = KECCAK_EMB_B_PLACEHOLDER +KECCAK_EMB_OUT = KECCAK_EMB_OUT_PLACEHOLDER +KECCAK_FAC_N = KECCAK_FAC_N_PLACEHOLDER +KECCAK_EMB_CELLS = KECCAK_EMB_CELLS_PLACEHOLDER +KECCAK_IOTA_START = KECCAK_IOTA_START_PLACEHOLDER +KECCAK_IOTA_IDX = KECCAK_IOTA_IDX_PLACEHOLDER +KECCAK_PI = KECCAK_PI_PLACEHOLDER +KECCAK_RHO = KECCAK_RHO_PLACEHOLDER +KECCAK_SHIFT_USED = KECCAK_SHIFT_USED_PLACEHOLDER +KECCAK_LANES = 25 +KECCAK_Z_BITS = 6 # a bit position is z + 64 * lane +KECCAK_LANE_BITS = 5 +KECCAK_LOCAL_BITS = KECCAK_Z_BITS + KECCAK_LANE_BITS +LANE_OFF = 2 ** KECCAK_LANE_BITS - 2 # where a frame eqtree's last level starts +KECCAK_IOTA_BITS = 7 # iota flips slices 2^i - 1 of lane 0 only +QKECCAK_SLOT_LOG = QKECCAK_SLOT_LOG_PLACEHOLDER +# A claim's record: its value, the sum of its instance weights, pointers to its +# point, its lane table, the local point whose slice coordinates' eq is its slice +# table, and its weight on each position iota can flip (slice 2^i - 1 of lane 0), +# then the instance weights. +KC_VALUE = 0 +KC_KAPPA = 1 +KC_POINT = 2 +KC_LANES = 3 +KC_LOCAL = 4 +KC_FLIPS = 5 +KC_MU = 6 +KC_SIZE = KC_MU + KECCAK_PACK +# One round of a layer's sumcheck over packs and segments. +KR_FS = 0 +KR_CURSOR = 5 +KR_CLAIM = 6 +KR_SCALE = 7 +KR_SLOTS = 8 +# A layer's round records lie field by field, KR_CAP apart, one per KR_STEP rounds +# and one per round of the remainder: a layer has tau - 3 < 32 rounds, so at most +# 3 + 7 of them. +KR_STEP_LOG = 3 +KR_STEP = 2 ** KR_STEP_LOG +KR_CAP = 16 + +# ------------------------------------------------- the stacked WHIR opening +# The opening is dispatched by the certified committed log-size m through `match`, +# one arm per m, and by the announced rate inside it. The LIG_* tables carry one row +# per (rate, m), emitted from the same level_shapes the prover uses: scalars index as +# TBL[m_idx], per-level values as TBL[m_idx * LIG_MAX_LEVELS + lvl], where m_idx is +# the rate-major candidate index rate * LIG_N_LOG_SIZES + m - LIG_MIN_LOG_SIZE. +LIG_MIN_LOG_SIZE = LIG_MIN_LOG_SIZE_PLACEHOLDER +LIG_N_LOG_SIZES = LIG_N_LOG_SIZES_PLACEHOLDER +LIG_N_RATES = LIG_N_RATES_PLACEHOLDER +# Committed-column kappa sources (0 = const COL_KAPPA_ADJ, 1 = log_mem, 2+t = tau_t) +# and the PCS floor for the stacked size. +N_COMMITTED_COLS = N_COMMITTED_COLS_PLACEHOLDER +N_COLUMN_LOGS = N_COLUMN_LOGS_PLACEHOLDER +COL_KAPPA_SRC = COL_KAPPA_SRC_PLACEHOLDER +COL_KAPPA_ADJ = COL_KAPPA_ADJ_PLACEHOLDER +N_COL_GROUPS = N_COL_GROUPS_PLACEHOLDER +COL_GROUP_START = COL_GROUP_START_PLACEHOLDER # a column group's first column, then N_COMMITTED_COLS +PCS_MIN_MU = PCS_MIN_MU_PLACEHOLDER +# Global maxima; StackBuf frame sizes are parse-time, so they must be baked. +LIG_MAX_LEVELS = LIG_MAX_LEVELS_PLACEHOLDER +# A rate's tables follow the previous rate's, every size and level of it. +LIG_RATE_STRIDE = LIG_N_LOG_SIZES * LIG_MAX_LEVELS +LIG_MAX_OOD_SAMPLES = LIG_MAX_OOD_SAMPLES_PLACEHOLDER +LIG_LOG_MSG_COLS_CAP = LIG_LOG_MSG_COLS_CAP_PLACEHOLDER +YR_LOG_CAP = YR_LOG_CAP_PLACEHOLDER +MAX_STACK_LOG = LIG_MIN_LOG_SIZE + LIG_N_LOG_SIZES - 1 +SEL_NIBBLES = (MAX_STACK_LOG + 3) // 4 # four selector bits share one table lookup +SEL_EQ_STRIDE = 64 # a nibble's eq tables over its top 1..4 coords, strided +EQ_CHAIN_ARMS = SIZE_BITS + QKECCAK_SLOT_LOG + 1 # eq prefix chain lengths +SEL_TAB_NIBBLES = (SIZE_BITS + 3) // 4 # nibbles of a bus selector run +SEL_TAB_EQ_STRIDE = 32 # a nibble's eq tables over its low 1..4 coords, padded +RANK_KEYS_AT = SIZE_BITS + 16 * SEL_TAB_NIBBLES # where exponent_tables keeps the rank keys +RANK_STRIDE = N_BLOCKS + N_COMMITTED_COLS # above any block or column index +RANK_KEY_BOUND = SIZE_BITS * RANK_STRIDE # above any step between two rank keys +SIDE_TAB_AT = RANK_KEYS_AT + SIZE_BITS +LIG_N_LEVELS = LIG_N_LEVELS_PLACEHOLDER +LIG_YR_LEVEL = LIG_YR_LEVEL_PLACEHOLDER +LIG_YR_LOG_LEN = LIG_YR_LOG_LEN_PLACEHOLDER +LIG_YR_LEN = LIG_YR_LEN_PLACEHOLDER +LIG_TOTAL_FOLDS = LIG_TOTAL_FOLDS_PLACEHOLDER +LIG_MAX_INTERLEAVE = LIG_MAX_INTERLEAVE_PLACEHOLDER +LIG_CAP_DEPTH = LIG_CAP_DEPTH_PLACEHOLDER +LIG_QUERY_GRIND_BITS = LIG_QUERY_GRIND_BITS_PLACEHOLDER +LIG_LEVEL_POSITIONS_CAP = LIG_LEVEL_POSITIONS_CAP_PLACEHOLDER +VANISH_BY_COLS = VANISH_BY_COLS_PLACEHOLDER # the subspace-vanishing values of n columns at n(n+1)/2 +VANISH_INVS_BY_COLS = VANISH_INVS_BY_COLS_PLACEHOLDER # and their inverses +LIG_OOD_SAMPLES = LIG_OOD_SAMPLES_PLACEHOLDER +LIG_QUERIES = LIG_QUERIES_PLACEHOLDER +LIG_FOLDS = LIG_FOLDS_PLACEHOLDER +LIG_INTERLEAVE = LIG_INTERLEAVE_PLACEHOLDER +LIG_LEAF_CELLS = LIG_LEAF_CELLS_PLACEHOLDER +LIG_PACKED_ROW_CAP = LIG_PACKED_ROW_CAP_PLACEHOLDER +LIG_ROW_CAP = LIG_ROW_CAP_PLACEHOLDER +LIG_PATH_CAP = LIG_PATH_CAP_PLACEHOLDER +LIG_TREE_DEPTH = LIG_TREE_DEPTH_PLACEHOLDER +LIG_SQUEEZES = LIG_SQUEEZES_PLACEHOLDER +LIG_LOG_MSG_COLS = LIG_LOG_MSG_COLS_PLACEHOLDER +LIG_RESIDUAL_FOLD_OFF = LIG_RESIDUAL_FOLD_OFF_PLACEHOLDER +LIG_RESIDUAL_PREFIX_LEN = LIG_RESIDUAL_PREFIX_LEN_PLACEHOLDER +LIG_FOLDS_OFF = LIG_FOLDS_OFF_PLACEHOLDER +LIG_MIN_SHIFT_INV = LIG_MIN_SHIFT_INV_PLACEHOLDER +# eval_b claim descriptors. CLAIM_POINT_BUF says which point buffer a pooled +# claim's x-part lives in, CLAIM_COMMITTED_COL maps it to the compact index of the +# committed column it must open (a virtual KECCAK value claim maps to QKECCAK), +# CLAIM_QKECCAK_SLOT_BITS holds the bits of the q_keccak slot a virtual claim reads +# (zero for a non-virtual one), and QKECCAK_COMMITTED_COL is the ring-switch target. +# A KECCAK output lane's claim is POINT_BUF_KECCAK_OUT: never opened, the 25 of them, +# from KECCAK_OUT_POOL on, are the Keccak reduction's claim on the output. +POINT_BUF_ZETA = 0 +POINT_BUF_RHO = 1 +POINT_BUF_PI = 2 +POINT_BUF_QKECCAK_RHO = 3 +POINT_BUF_KECCAK_OUT = 4 +KECCAK_OUT_POOL = KECCAK_OUT_POOL_PLACEHOLDER +CLAIM_POINT_BUF = CLAIM_POINT_BUF_PLACEHOLDER +CLAIM_COMMITTED_COL = CLAIM_COMMITTED_COL_PLACEHOLDER +CLAIM_KAPPA_SRC = CLAIM_KAPPA_SRC_PLACEHOLDER # a claim's low dimension, as a kappa source +CLAIM_KAPPA_ADJ = CLAIM_KAPPA_ADJ_PLACEHOLDER +CLAIM_QKECCAK_SLOT_BITS = CLAIM_QKECCAK_SLOT_BITS_PLACEHOLDER +QKECCAK_COMMITTED_COL = QKECCAK_COMMITTED_COL_PLACEHOLDER +QKECCAK_VARS_CAP = QKECCAK_VARS_CAP_PLACEHOLDER + +# ------------------------------------------------------ statements and deferral +# A node defers one claim on the stacked bytecode. DEFER_SIZE is the region one +# sub-proof's verification exports (the claim's point, then its value); +# DEFER_STMT_* index the batched claim a node's OWN statement carries, laid out the +# same way. The Fiat-Shamir seed rides the public input rather than being baked, +# so one compiled guest verifies proofs of any inner program. +BYTECODE_LOG = BYTECODE_LOG_PLACEHOLDER # log rows of the bytecode blocks +DEFER_SIZE = DEFER_SIZE_PLACEHOLDER +BYTECODE_VARS = BYTECODE_VARS_PLACEHOLDER # = BYTECODE_LOG + LOG2_BYTECODE_COLS +FRESH_BC_VALUE = BYTECODE_VARS +DEFER_STMT_CELLS = BYTECODE_VARS + 1 +DEFER_STMT_BC_VALUE = BYTECODE_VARS +# The aggregation transcript's seeded capacity, and the zeros padding a child's words +# to whole rates (so every child starts with a full rate waiting). +AGG_SEED_0 = AGG_SEED_0_PLACEHOLDER +AGG_SEED_1 = AGG_SEED_1_PLACEHOLDER +AGG_SEED_2 = AGG_SEED_2_PLACEHOLDER +AGG_SEED_3 = AGG_SEED_3_PLACEHOLDER +AGG_SEED_4 = AGG_SEED_4_PLACEHOLDER +AGG_CHILD_PAD = AGG_CHILD_PAD_PLACEHOLDER +AGG_CHILD_WORDS = 2 + DEFER_SIZE + DEFER_STMT_CELLS +# The statement digest's preimage: the STMT_HEADER header values as the 16-byte +# cells they already are (the seed, then the dependency digest of the two claim lists +# (`deps_digest`) and the DA one), then the deferred cells' tower limbs, two to a cell. No +# domain tag: every field has a fixed width, and the seed leads, binding this +# bytecode and the proof system's version. +STMT_HEADER = STMT_HEADER_PLACEHOLDER +STMT_DEFER_OFF = STMT_HEADER +STMT_PAIRS = STMT_PAIRS_PLACEHOLDER +STMT_CELLS = STMT_DEFER_OFF + 3 * STMT_PAIRS + +# ------------------------------------------------------------------ the hash +# Every hash is Keccak-256 (primitives::hash::hash): a block is 136 bytes, eight and a +# half cells, XORed into the previous KECCAK_STATE-cell sponge state before a KECCAK, +# and the last output's first two cells are the digest. A hash of runtime length +# carries that state through a heap run between loop frames, KECCAK_STATE cells a +# step (`claim_list_digest`). +KECCAK_STATE = KECCAK_STATE_PLACEHOLDER +# leanSTARK (0x11) dependencies: the most instructions (log) and claims a list, and the +# leanVM tag's first 8 bytes as a lane and its 9th byte, and the mixed dependency +# digest's 32-byte domain as two cells. +MAX_GENERIC_BYTECODE_LOG = MAX_GENERIC_BYTECODE_LOG_PLACEHOLDER +GEN_KGEN_ARMS = MAX_GENERIC_BYTECODE_LOG + 1 +MAX_GENERIC_DEPENDENCIES = MAX_GENERIC_DEPENDENCIES_PLACEHOLDER +TAG_LANE0 = TAG_LANE0_PLACEHOLDER +TAG_BYTE8 = TAG_BYTE8_PLACEHOLDER +DEPS_DOMAIN_0 = DEPS_DOMAIN_0_PLACEHOLDER +DEPS_DOMAIN_1 = DEPS_DOMAIN_1_PLACEHOLDER +# Claims a declared claim list absorbs a loop frame: 68 cells, four block pairs. The +# list's bulk goes CLAIM_BIG claims a frame, BIG_GROUPS of those groups, so that the +# sponge state's trip through the heap is paid once per CLAIM_BIG claims; the rest +# CLAIM_GROUP a frame, then a `match` on the last group's count. +CLAIM_GROUP = CLAIM_GROUP_PLACEHOLDER +BIG_GROUPS = BIG_GROUPS_PLACEHOLDER +CLAIM_BIG = CLAIM_GROUP * BIG_GROUPS +# A child list's walk record: the sponge state, then a coverage slot and a table entry. +MONO_REC = KECCAK_STATE + 2 + +# -------------------------------------------------- leanSPHINCS (host-supplied) +# The scheme's own letters, prefixed SP_: under each bottom leaf a FORS instance of +# SP_FORS_TREES trees of height SP_FORS_HEIGHT, and above them three layers of trees +# over WOTS+C keys of SP_V chains, of heights SP_HEIGHT_0, SP_HEIGHT_1 and SP_HEIGHT_2 +# from the bottom. An addressed hash is PK.seed, FIPS 205's 32-byte address (two +# cells), then the payload. +# NiceTry SPHINCS- v2 (prototype): standard FORS under a d-layer WOTS+ hypertree, +# every hash keccak over 32-byte words (pp|0, the 32-byte ADRS, value|0). +SP_K = SP_K_PLACEHOLDER # FORS trees +SP_A = SP_A_PLACEHOLDER # FORS tree height +SP_D = SP_D_PLACEHOLDER # hypertree layers, layer 0 at the bottom +SP_HP = SP_HP_PLACEHOLDER # a layer's tree height, h' +SP_H = SP_H_PLACEHOLDER # total height, d h' +SP_W = SP_W_PLACEHOLDER # Winternitz parameter, 16 +SP_LEN1 = SP_LEN1_PLACEHOLDER # message digits +SP_LEN2 = SP_LEN2_PLACEHOLDER # checksum digits +SP_L = SP_LEN1 + SP_LEN2 # chains +SP_MAX_CSUM = SP_MAX_CSUM_PLACEHOLDER # len1 (w - 1): digit sum plus checksum +SP_CHAIN_STEPS = SP_W - 1 +SP_HT_OFF = SP_K * SP_A +SP_BIT_LANES = 3 +SP_BIT_CELLS = SP_BIT_LANES * BASE_FIELD_BITS +# Digest bit b (the digest a big-endian 256-bit integer) as an index into the bit +# run of its last three lanes. +SP_BIT_INDEX = SP_BIT_INDEX_PLACEHOLDER +SP_ONES = 340282366920938463463374607431768211455 # 2^128 - 1 +# ADRS cell bytes: a cell's byte b holds its bits 8b..8b+7. +SP_BYTE3 = 2 ** 24 +SP_BYTE11 = 2 ** 88 +SP_BYTE15 = 2 ** 120 +SP_WOTS_PK = 1 +SP_TREE = 2 +SP_FORS_TREE = 3 +SP_FORS_ROOTS = 4 +# Unused: the shared `walk` names them in its leanSPHINCS arm, which nothing instantiates now. +SP_WORD2_MUL = 0 +SP_WORD3_MUL_0 = 0 +SP_DIGITS_PER_BYTE = 1 + +# --------------------------------------------------------------- node capacities +# The coverage table's slots, both schemes' declared claims and their duplicates, are +# bounded by the memory itself: each count is a g-power address (`assert_gpow`), so its +# log is below the announced memory size, at most 2^30, and sums of a few never wrap. +# MAX_RECURSIONS is the arity of an aggregation tree. +MAX_RECURSIONS = MAX_RECURSIONS_PLACEHOLDER + + +# ---------------------------------- LeanDA ------------------------------------------ +# Blob and cell widths are fixed. The row count is hinted and bounded by DA_MAX_ROWS; +# the tree over the blob roots dispatches on the checked log of its padded value. +DA_LOG_K = DA_LOG_K_PLACEHOLDER +DA_LOG_CELL = DA_LOG_CELL_PLACEHOLDER +DA_MAX_ROWS = DA_MAX_ROWS_PLACEHOLDER +DA_LOG_MAX_ROWS = DA_LOG_MAX_ROWS_PLACEHOLDER +DA_PAD_BLOB_0 = DA_PAD_BLOB_0_PLACEHOLDER +DA_PAD_BLOB_1 = DA_PAD_BLOB_1_PLACEHOLDER +# Vhat_j at the points the membership vector's product tree needs (da_factors): each +# parent node of the tree over a cell index, then of the tree inside a cell, at the bit +# j it splits; and Vhat_j(beta_(DA_LOG_CELL + b)) at DA_LOG_CELL * b + j, for each factor +# j inside a cell and bit b of the cell index. +DA_VHAT = DA_VHAT_PLACEHOLDER +DA_VHAT_COLUMN = DA_VHAT_COLUMN_PLACEHOLDER + +DA_LOG_M = DA_LOG_K + 1 # log of the symbols per encoded row +DA_CELL = 2 ** DA_LOG_CELL # symbols in a cell +DA_BLOCK_BITS = DA_LOG_M - DA_LOG_CELL # log of the cells per row +DA_CELLS = 2 ** DA_BLOCK_BITS # cells per row +DA_PACKED = DA_CELL // 2 # packed 128-bit cells in one cell +DA_TREE_ARMS = DA_LOG_MAX_ROWS + 1 # tree depths the row count can dispatch to +DA_SUM = 4 * DA_CELLS # where a blob's running sum starts, past its tree +DA_REGION = DA_SUM + DA_CELLS + 1 # cells of one blob's tree and running sum + +# =================================== field packing ================================== +# Serializing a word means exposing its three K limbs, and the tower representation +# is unique, so proving that the exposed lanes are in K and weight back to the word +# is the whole check: no equality to make, one less hint to distrust. + + +@inline +def pack64x2(a, b): + assert_in_k(a, b) + return a + Y_TOWER * b + + +def assert_canonical(word): + # Hint the low limb and derive the quotient by Y. Requiring both in K proves + # `word = lo + Y*hi`, hence that its top limb is zero. + lo = StackBuf(1) + hint_f192_limbs(lo, word) + hi = (word + lo[0]) * Y_INV + assert_in_k(lo[0], hi) + return 0 + + +@inline +def canonical_limbs(word): + # assert_canonical, keeping the two limbs it binds. + lo = StackBuf(1) + hint_f192_limbs(lo, word) + hi = (word + lo[0]) * Y_INV + assert_in_k(lo[0], hi) + return lo[0], hi + + +@inline +def pack_pair(out, at: Const, s, t): + # Two field elements as three canonical cells out[at..at+3], (s0,s1) (s2,t0) (t1,t2): + # each top limb derived from the two hinted below it, each pack proving its lanes in K. + s_lo = StackBuf(2) + t_lo = StackBuf(2) + hint_f192_limbs(s_lo, s) + hint_f192_limbs(t_lo, t) + out[at] = pack64x2(s_lo[0], s_lo[1]) + out[at + 1] = pack64x2(((s + s_lo[0]) * Y_INV + s_lo[1]) * Y_INV, t_lo[0]) + out[at + 2] = pack64x2(t_lo[1], ((t + t_lo[0]) * Y_INV + t_lo[1]) * Y_INV) + return + + +@inline +def challenge_from_state(state): + # Both words are hash outputs with zero top limbs. + # Hint d2 and derive d3 = (state[1] + d2)/Y. + # Requiring both in K binds d2 by the tower representation. + # The challenge uses d2; d3 is checked and discarded. + d2 = StackBuf(1) + hint_f192_limbs(d2, state[1]) + d3 = (state[1] + d2[0]) * Y_INV + assert_in_k(d2[0], d3) + return state[0] + Y_TOWER * Y_TOWER * d2[0] + + +# ==================================== Fiat-Shamir =================================== +# A duplex sponge over Keccak-f in overwrite mode (fiat_shamir::FiatShamirState). The +# sponge is the 13-cell output `sp` of its last permutation, whose last FS_CELLS cells, the +# capacity, are all the next permutation keeps: between functions and loop frames those +# cells are all that travels, `fs0 .. fs4`. Scalars absorbed since wait in an eight-cell +# `rate` run, two cells a scalar, at most four. How many wait is a compile-time count at +# every call site, `k`. A fifth scalar, or a challenge, permutes; every challenge is a +# permutation of its own. + + +@inline +def fs_pack(rate, k: Const, x, tag): + # Scalar x as lanes (c0, c1, c2, tag) over rate cells 2k and 2k + 1. KECCAK requires + # both cells' top limbs to be zero, which binds the hinted c2. + limbs = StackBuf(3) + hint_f192_limbs(limbs, x) + assert_in_k(limbs[2], tag) + rate[2 * k] = x + Y_TOWER * Y_TOWER * limbs[2] + rate[2 * k + 1] = limbs[2] + Y_TOWER * tag + return + + +@inline +def fs_perm(sp, rate, k: Const): + # The permutation taking the k waiting scalars: they overwrite the rate, zero past them. + nxt = StackBuf(KECCAK_STATE) + if k == 0: + keccak([0, 0], [0, 0], nxt, cap=sp) + if k == 1: + keccak(rate[0:2], [0, 0], nxt, cap=sp) + if k == 2: + keccak(rate[0:2], rate[2:4], nxt, cap=sp) + if k == 3: + rate[6] = 0 + rate[7] = 0 + keccak(rate[0:2], rate[2:4], nxt, tail=rate[4:8], cap=sp) + if k == 4: + keccak(rate[0:2], rate[2:4], nxt, tail=rate[4:8], cap=sp) + return nxt + + +@inline +def fs_perm4(sp, r0, r1, r2, r3): + # The permutation whose rate is r0..r3 then zero: up to two waiting scalars, held as + # cells rather than in a rate run. + nxt = StackBuf(KECCAK_STATE) + keccak([r0, r1], [r2, r3], nxt, cap=sp) + return nxt + + +@inline +def fs_absorb_nonce(sp, rate, k: Const, nonce): + # A grinding nonce as waiting scalar k (k < 4), with its own tag, then the + # permutation its challenge comes from. + fs_pack(rate, k, nonce, DS_POW_NONCE) + nxt = fs_perm(sp, rate, k + 1) + return nxt + + +@inline +def fs_perm_pending(cap, rec, at: Const): + # The permutation taking a full rate held in the record cells at..at+8. + nxt = StackBuf(KECCAK_STATE) + keccak([rec[GEN ** at], rec[GEN ** (at + 1)]], [rec[GEN ** (at + 2)], rec[GEN ** (at + 3)]], nxt, tail=[rec[GEN ** (at + 4)], rec[GEN ** (at + 5)], rec[GEN ** (at + 6)], rec[GEN ** (at + 7)]], cap=cap) + return nxt + + +@inline +def fs_cap(fs0, fs1, fs2, fs3, fs4): + # A carried capacity as the run a permutation reads. + sp = [fs0, fs1, fs2, fs3, fs4] + return sp + + +@inline +def fs_load(rec, field: Const, stride: Const): + # A record's carried capacity, its cells `stride` fields apart from `field` on. + sp = [rec[GEN ** (field * stride)], rec[GEN ** ((field + 1) * stride)], rec[GEN ** ((field + 2) * stride)], rec[GEN ** ((field + 3) * stride)], rec[GEN ** ((field + 4) * stride)]] + return sp + + +@inline +def fs_store(rec, field: Const, stride: Const, sp): + # Store the capacity of permutation output `sp` as fs_load reads it. + for j in unroll(0, FS_CELLS): + rec[GEN ** ((field + j) * stride)] = sp[KECCAK_STATE - FS_CELLS + j] + return + + +@inline +def fs_next(rate, k: Const, cursor): + # Fetch and absorb in one act: the word under `cursor` becomes waiting scalar k + # (k < 4), and the cursor steps one word on. Reading and absorbing are inseparable, + # so no proof-stream word can enter the computation unbound: the soundness + # invariant the whole guest rests on. + x = cursor[GEN ** 0] + fs_pack(rate, k, x, DS_OBSERVE) + return x, cursor * GEN + + +@inline +def squeeze(sp, rate, k: Const): + # A challenge: the permutation taking the k waiting scalars, read off its first three + # lanes. Returns the new sponge and the challenge. + nxt = fs_perm(sp, rate, k) + challenge = challenge_from_state(nxt) + return nxt, challenge + + +# ============================ bits, logs, and the exponent ========================== + + +def exponent_tables(): + # A read-only lookup table over the exponent domain, indexed at runtime g-powers + # (so it must be heap, not stack): g_squares[g^j] = g^(2^j) turns integer sums of + # powers of two into field products. It spans SIZE_BITS because + # `log2_ceil_in_the_exponent` bounds its result there and indexes it at g^L. + # Past them, the opening selector's nibble shares: g^(u 2^(4j)) at SIZE_BITS + 16j + u. + g_squares = HeapBuf(SIDE_TAB_AT + N_BLOCK_GROUPS) + sq_run = GEN + for j in unroll(0, SIZE_BITS): + g_squares[GEN ** j] = sq_run + sq_run *= sq_run + # Then the rank keys g^(kappa RANK_STRIDE) of the placement orders. + for k in unroll(0, SIZE_BITS): + g_squares[GEN ** (RANK_KEYS_AT + k)] = GEN ** (k * RANK_STRIDE) + # Then each bus block group's side. + for s in unroll(0, N_GKR_SIDES): + for gi in unroll(SIDE_GROUP_START[s], SIDE_GROUP_START[s + 1]): + g_squares[GEN ** (SIDE_TAB_AT + gi)] = s + base = GEN + for j in unroll(0, SEL_TAB_NIBBLES): + share = GEN ** 0 + for u in unroll(0, 16): + g_squares[GEN ** (SIZE_BITS + 16 * j + u)] = share + share *= base + base = share + return g_squares + + +def g_power_of_word(value): + # g^value for a concrete integer `value` < 2^LOG_WORD_BITS: advice-decompose its + # bits, tie them back to the word, and assemble Π g^(bit_j·2^j). + bits = StackBuf(LOG_WORD_BITS) + hint_decompose_bits(bits, value, LOG_WORD_BITS) + word = 0 + g_value = GEN ** 0 + for j in unroll(0, LOG_WORD_BITS): + bit = bits[j] + bits[j] = bit * bit + word += bit * (2 ** j) + g_value *= 1 + bit * (1 + GEN ** (2 ** j)) + assert word == value + return g_value + + +def assert_small_g_power(x, g_squares): + # x = g^e with e < 2^(4 SEL_TAB_NIBBLES): hinted g-power digits, each range-checked + # below 16, whose weighted product is x. A g-power past that, a "negative" + # difference of two small exponents above all, has no such digits. + digits = StackBuf(SEL_TAB_NIBBLES) + hint_decompose_digits_exponent(digits, x, 4 * SEL_TAB_NIBBLES, SEL_TAB_NIBBLES, 4, 0) + shares = g_squares * GEN ** SIZE_BITS + for j in unroll(0, SEL_TAB_NIBBLES): + d = digits[j] + assert log(d) < 16 + if const(j == 0): + rebuilt = shares[d] + else: + rebuilt *= shares[GEN ** (16 * j) * d] + assert rebuilt == x + return + + +def log2_ceil_in_the_exponent(g_N, g_squares, floor: Const, nbits: Const): + # g^L, L = max(log2_ceil(N), floor), given g_N = g^N. L is advice, pinned by + # 2^L - N >= 0 and, above the floor, N - 2^(L-1) - 1 >= 0, each a g-power with a + # small exponent (a negative one wraps around the order of g). The first bounds + # N below 2^nbits, as L < SIZE_BITS = nbits. N must be known nonnegative: at the + # floor only 2^floor - N is checked, which a small negative N passes too. Both + # callers build g_N as a product of certified powers g^(2^kappa). + bits = HeapBuf(GEN ** nbits) + hint_decompose_bits_exponent(bits, g_N, nbits) + g_log = hint_log2_ceil(bits, nbits, floor) + assert log(g_log) < SIZE_BITS + assert log(g_log / GEN ** floor) < SIZE_BITS + assert_small_g_power(g_squares[g_log] / g_N, g_squares) + if g_log != GEN ** floor: + assert_small_g_power(g_N * INV_GEN / g_squares[g_log * INV_GEN], g_squares) + return g_log + + +@inline +def assert_window(w, width: Const): + # log(w) < 2^width, the bound spelled out per width (a range check takes a literal bound). + if const(width == 1): + assert log(w) < 2 + elif const(width == 2): + assert log(w) < 4 + elif const(width == 3): + assert log(w) < 8 + elif const(width == 4): + assert log(w) < 16 + elif const(width == 5): + assert log(w) < 32 + elif const(width == 6): + assert log(w) < 64 + elif const(width == 7): + assert log(w) < 128 + else: + assert 0 == 1 # no window is wider than `win_tabs` covers + return + + +@inline +def query_window(word, wins, tabs, at: Const, start: Const, width: Const): + # Bits start..start+width of `word` into wins[at] as g^n, n the integer they spell, range checked below + # 2^width. Returns n's bit pattern, `tabs[g^n]` (`query_tables`). + hint_windows_exponent(wins, word, start, width, 1, at) + w = wins[at] + assert_window(w, width) + return tabs[w] + + +@inline +def query_window_split(word, wins, tabs, parts, at: Const, start: Const, width: Const, cut: Const): + # A window whose bits cross a limb boundary `cut` bits in: its two runs hinted and range checked apart, + # each read as its bit pattern into parts[0], parts[1], and joined in the exponent into wins[at]. + hint_windows_exponent(parts, word, start, cut, 1, 2) + hint_windows_exponent(parts, word, start + cut, width - cut, 1, 3) + lo = parts[2] + hi = parts[3] + assert_window(lo, cut) + assert_window(hi, width - cut) + hp = hi + for s in unroll(0, cut): + hp = hp * hp + wins[at] = lo * hp + parts[0] = tabs[lo] + parts[1] = tabs[hi] + return + + +def decode_query_windows(squeezed_word, positions_out, win_ptrs_out, tabs, depth: Const, cap_depth: Const): + # The squeezed word's bits are advice HERE, in windows: a window of w bits is hinted as g^n, n the integer + # they spell, range checked below 2^w and read back as n's bit pattern. Each depth-bit group is a query + # position (little-endian), whose windows are its path bits QW at a time from the bottom, then the cap + # child bit, then the cap parent index: `opening_query` dispatches on them, and query j's run of windows + # is at win_ptrs_out[g^j]. The bits above the last position are windows too, and the windows' patterns, + # shifted to their places, must rebuild the word. A position whose bits cross a 64-bit limb splits there, + # the polynomial basis shifting a run as a whole only inside one limb. + per_word = FIELD_BITS // depth + path = depth - cap_depth + n_pw = (path + QW - 1) // QW + n_win = n_pw + 2 + wins = StackBuf(QUERY_WINDOWS_CAP) + wins_ptr = addr(wins) + reconstructed = 0 + for j in unroll(0, per_word): + base = j * depth + at0 = j * n_win + # bits of this position below the next limb; at least `depth` when it does not cross one + cut = 64 - base % 64 + p_lo = 0 + p_hi = 0 + for i in unroll(0, n_win): + # window i: QW path bits, the last path window's remainder, the cap child bit, the cap index + if const(i + 2 == n_win): + off = path + width = 1 + elif const(i + 1 == n_win): + off = path + 1 + width = cap_depth - 1 + elif const(i + 3 == n_win): + off = QW * i + width = path - QW * i + else: + off = QW * i + width = QW + if const((off + width) // (cut + 1) == 0): + p_lo += query_window(squeezed_word, wins, tabs, at0 + i, base + off, width) * COORD_BASIS[off] + elif const(off // cut == 0): + parts = StackBuf(4) + query_window_split(squeezed_word, wins, tabs, parts, at0 + i, base + off, width, cut - off) + p_lo += parts[0] * COORD_BASIS[off] + p_hi += parts[1] + else: + p_hi += query_window(squeezed_word, wins, tabs, at0 + i, base + off, width) * COORD_BASIS[off - cut] + if const(cut // depth == 0): + positions_out[GEN ** j] = p_lo + COORD_BASIS[cut] * p_hi + reconstructed += COORD_BASIS[base] * p_lo + COORD_BASIS[base + cut] * p_hi + else: + positions_out[GEN ** j] = p_lo + reconstructed += COORD_BASIS[base] * p_lo + win_ptrs_out[GEN ** j] = wins_ptr * GEN ** at0 + rest = StackBuf(4) + for i in unroll(0, (FIELD_BITS - per_word * depth + 6) // 7): + start = per_word * depth + 7 * i + if const((start + 7) // (FIELD_BITS + 1) == 0): + width = 7 + else: + width = FIELD_BITS - start + reconstructed += query_window(squeezed_word, rest, tabs, i, start, width) * COORD_BASIS[start] + assert reconstructed == squeezed_word + return + + +def grind_check(word, nbits: Const): + # WHIR query grinding: the challenge drawn right after the nonce is absorbed must + # have its low nbits zero (transcript::pow_bits_ok, nbits < 64). That window lives + # in the challenge's FIRST 64-bit lane, the low limb of the permutation output's + # first cell `word`: only that lane is advice-decomposed and verified. The honest + # prover searches the deterministic u64 subset while verification permits the + # full field: each candidate still costs one permutation and succeeds with + # probability 2^-bits. + lanes = StackBuf(1) + hint_f192_limbs(lanes, word) + high = (word + lanes[0]) * Y_INV + assert_in_k(lanes[0], high) + # The lane is rebuilt from its bits at and above nbits only, which IS the claim + # that its low nbits coordinates are zero; the hinted low bits go unread. + lane_bits = StackBuf(BASE_FIELD_BITS) + hint_decompose_bits(lane_bits, lanes[0], BASE_FIELD_BITS) + # Horner from the top bit, by the one constant X: no constant per bit. + acc = 0 + for i in unroll(nbits, BASE_FIELD_BITS): + b = lane_bits[BASE_FIELD_BITS - 1 + nbits - i] + lane_bits[BASE_FIELD_BITS - 1 + nbits - i] = b * b # booleanity, as a write-once pin + if i == nbits: + acc = b + else: + acc = acc * COORD_BASIS[1] + b + assert acc * COORD_BASIS[nbits] == lanes[0] + return + + +# =============================== multilinear primitives ============================= + + +@inline +def eq_weight(ch, count: Const, idx: Const, msb_span: Const): + # The eq-tensor weight of compile-time index `idx` against the challenge run + # ch[0..count): prod_c eq(bit(idx), ch[c]), where the bit is bit c of idx + # (msb_span == 0) or bit (msb_span - 1 - c) (an MSB-first walk over an + # msb_span-bit index). + w = GEN ** 0 + for c in unroll(0, count): + cv = ch[GEN ** c] + if msb_span == 0: + if (idx // (2 ** c)) % 2 == 1: + w *= cv + else: + w *= (1 + cv) + else: + if (idx // (2 ** (msb_span - 1 - c))) % 2 == 1: + w *= cv + else: + w *= (1 + cv) + return w + + +@inline +def eqtree(point_ptr, out, n_coords: Const): + # The eq tensor of the n_coords challenges at point_ptr[0..n_coords), built by + # doubling into out (size 2^(n_coords+1) - 2); the final 2^n_coords values start + # at offset 2^n_coords - 2. + r0 = point_ptr[GEN ** 0] + out[GEN ** 0] = 1 + r0 + out[GEN ** 1] = r0 + for t in unroll(1, n_coords): + rt = point_ptr[GEN ** t] + one_plus_rt = 1 + rt + for i in unroll(0, 2 ** t): + pw = out[GEN ** (2 ** t - 2 + i)] + out[GEN ** (2 ** (t + 1) - 2 + i)] = pw * one_plus_rt + out[GEN ** (2 ** (t + 1) - 2 + 2 ** t + i)] = pw * rt + return + + +@inline +def eqtree_frame(point_ptr, out, n_coords: Const, count: Const): + # eqtree into frame cells, in the same layout, the last level cut at `count` + # (which must exceed half of it). + r0 = point_ptr[GEN ** 0] + out[0] = 1 + r0 + out[1] = r0 + for t in unroll(1, n_coords): + rt = point_ptr[GEN ** t] + one_plus_rt = 1 + rt + for i in unroll(0, 2 ** t): + pw = out[2 ** t - 2 + i] + out[2 ** (t + 1) - 2 + i] = pw * one_plus_rt + if const(t + 1 == n_coords): + if (2 ** t + i) // count == 0: + out[2 ** (t + 1) - 2 + 2 ** t + i] = pw * rt + else: + out[2 ** (t + 1) - 2 + 2 ** t + i] = pw * rt + return + + +def eq_prefix_chain(chain, seed, a, b1, count_g): + # Prefix products of eq(a_k, b_k) = a_k + b1_k, b1 = 1 + b, from `seed`, so a + # reader picks the partial product up at its own certified length. Entry t is + # written from inputs with index < t only, so a garbage tail past a buffer's + # written extent cannot corrupt any shorter prefix. + assert log(count_g) < EQ_CHAIN_ARMS + done = match(log(count_g), range(0, EQ_CHAIN_ARMS), lambda n: eq_prefix_run(chain, seed, a, b1, n)) # a match needs a target + return + + +def eq_prefix_run(chain, seed, a, b1, n: Const): + chain[GEN ** 0] = seed + v = seed + for k in unroll(0, n): + v = v * (a[GEN ** k] + b1[GEN ** k]) + chain[GEN ** (k + 1)] = v + return 0 + + +def plus_one_run(out, src, n: Const): + for k in unroll(0, n): + out[GEN ** k] = 1 + src[GEN ** k] + return 0 + + +def rs_eq_rows(row, zs, ps, n: Const): + # Coordinates 0..n of zs/ps multiply rows 0..64 of `row` into the next row, row k + # by prod_i (zs_i^(2^k) + ps_i), the Frobenius powers walked inside. + nxt = row * GEN ** BASE_FIELD_BITS + if const(n == 0): + nxt = row + else: + zv = StackBuf(RS_CHUNK * BASE_FIELD_BITS) # zv[64 i + k] = zs_i^(2^k) + a = StackBuf(RS_CHUNK) + for i in unroll(0, n): + zv[BASE_FIELD_BITS * i] = zs[GEN ** i] + a[i] = ps[GEN ** i] + for k in unroll(0, BASE_FIELD_BITS): + acc = row[GEN ** k] + for i in unroll(0, n): + acc *= zv[BASE_FIELD_BITS * i + k] + a[i] + if const(k != BASE_FIELD_BITS - 1): + zv[BASE_FIELD_BITS * i + k + 1] = zv[BASE_FIELD_BITS * i + k] * zv[BASE_FIELD_BITS * i + k] + nxt[GEN ** k] = acc + return nxt + + +def rs_eq_run(chain, z_vals, point1, count_g): + # One run of the telescoped ring-switch product E = sum_k c_k * prod_j + # (z_j^(2^k) + 1 + ris_j): coordinate x multiplies row k by (z^(2^k) + 1 + + # point_x), z evolving by squaring per row. The runtime coordinates walk + # OUTSIDE and the fixed Frobenius powers inside, so nothing stores a z-power + # table. Coordinates go RS_CHUNK an iteration, so a row is read and written once + # per chunk; count = RS_CHUNK q + r is pinned from advice bits, and the r left + # over run after the loop. Returns the final row. + bits = StackBuf(RS_COUNT_BITS) + hint_decompose_bits_exponent(bits, count_g, RS_COUNT_BITS) + r_g = GEN ** 0 + q_g = GEN ** 0 + for k in unroll(0, RS_COUNT_BITS): + b = bits[k] + bits[k] = b * b + if const(k // RS_CHUNK_BITS == 0): + r_g *= 1 + b * (1 + GEN ** (2 ** k)) + else: + q_g *= 1 + b * (1 + GEN ** (2 ** (k - RS_CHUNK_BITS))) + assert q_g ** RS_CHUNK * r_g == count_g + for xq in mul_range(1, q_g): + base = xq ** RS_CHUNK + done = rs_eq_rows(chain * xq ** BASE_FIELD_BITS, z_vals * base, point1 * base, RS_CHUNK) + base = q_g ** RS_CHUNK + row = chain * q_g ** BASE_FIELD_BITS + last = match(log(r_g), range(0, RS_CHUNK), lambda n: rs_eq_rows(row, z_vals * base, point1 * base, n)) + return last + + +def fold_final_msg(msg, point, log_len: Const): + weights = StackBuf(2 * YR_LOG_CAP) + for j in unroll(0, log_len): + weights[2 * j] = 1 + point[GEN ** j] + weights[2 * j + 1] = point[GEN ** j] + # Weighted fold of the final_msg multilinear over 2^log_len values (log_len is + # the candidate's yr_log_n; the frame buffers use the global max size). + l0 = StackBuf(2 ** YR_LOG_CAP) + for t in unroll(0, 2 ** log_len // 2): + l0[t] = weights[0] * msg[GEN ** (2 * t)] + weights[1] * msg[GEN ** (2 * t + 1)] + cursor = l0 + n = 2 ** log_len // 2 + for j in unroll(1, log_len): + nxt = StackBuf(2 ** YR_LOG_CAP) + for t in unroll(0, n // 2): + nxt[t] = weights[2 * j] * cursor[2 * t] + weights[2 * j + 1] * cursor[2 * t + 1] + cursor = nxt + n = n // 2 + return cursor[0] + + +def sumcheck_round4(rd, msg_cursor, claim): + # One PLAIN sumcheck round, from the sponge of round record `rd` into the next + # record's. The prover sends the round polynomial's coefficients bar the one the + # split h(0) + h(1) == claim fixes, so the verifier derives that one and reads h + # at the challenge by Horner. Nothing is reapplied: no eq factor, no separate + # term for the tables still waiting, and the eq point is not read here at all. + sp = fs_load(rd, ROUND_FS, 1) + rate = StackBuf(8) + c0, msg_cursor = fs_next(rate, 0, msg_cursor) + c2, msg_cursor = fs_next(rate, 1, msg_cursor) + c3, msg_cursor = fs_next(rate, 2, msg_cursor) + # The split identity fixes c1 = claim + c2 + c3, so it is neither sent nor bound, + # and h(y) = c0 + y (claim + (1 + y) (c2 + (1 + y) c3)). + sp, y = squeeze(sp, rate, 3) + fs_store(rd * GEN ** ROUND_SLOTS, ROUND_FS, 1, sp) + y1 = 1 + y + return msg_cursor, c0 + y * (claim + y1 * (c2 + y1 * c3)), y + + +def gkr_rounds(rd, pt, n: Const): + # n GKR rounds, from the round record at `rd` (fields GKR_ROUNDS_CAP apart) to the + # one n further. The prover sends every coefficient but c0, which the round's + # pulled-out eq factor leaves fixed: c0 + r (c1 + ... + c4) == claim, r being the + # previous point's coordinate pt[q]; the challenge goes to pt[MU_CAP + 2 + q]. + if const(n != 0): + sp = fs_load(rd, ROUND_FS, GKR_ROUNDS_CAP) + cur = rd[GEN ** (ROUND_CURSOR * GKR_ROUNDS_CAP)] + claim = rd[GEN ** (ROUND_CLAIM * GKR_ROUNDS_CAP)] + for q in unroll(0, n): + rate = StackBuf(8) + c1, cur = fs_next(rate, 0, cur) + c2, cur = fs_next(rate, 1, cur) + c3, cur = fs_next(rate, 2, cur) + c4, cur = fs_next(rate, 3, cur) + sp, rk = squeeze(sp, rate, 4) + c0 = claim + pt[GEN ** q] * (c1 + c2 + c3 + c4) + pt[GEN ** (MU_CAP + 2 + q)] = rk + claim = c0 + rk * (c1 + rk * (c2 + rk * (c3 + rk * c4))) + rd_next = rd * GEN ** n + fs_store(rd_next, ROUND_FS, GKR_ROUNDS_CAP, sp) + rd_next[GEN ** (ROUND_CURSOR * GKR_ROUNDS_CAP)] = cur + rd_next[GEN ** (ROUND_CLAIM * GKR_ROUNDS_CAP)] = claim + return 0 + + +def batch_sumcheck(fs0, fs1, fs2, fs3, fs4, msgs, running, point, n_rounds: Const): + # The rounds of a claim-batching sumcheck: two hinted values per round + # (g(1) and g(inf)), the split fixing the third against the running claim, and + # the challenges collected into `point`. + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + for rd in unroll(0, n_rounds): + rate = StackBuf(8) + msg_g1, c = fs_next(rate, 0, msgs * GEN ** (2 * rd)) + msg_ginf, c = fs_next(rate, 1, c) + sp, rv = squeeze(sp, rate, 2) + point[GEN ** rd] = rv + g_zero = running + msg_g1 + c_one = g_zero + msg_g1 + msg_ginf + running = (msg_ginf * rv + c_one) * rv + g_zero # fold the degree-2 round at rv + return sp[8], sp[9], sp[10], sp[11], sp[12], running + + +# ==================================== Merkle paths ================================== + + +def verify_merkle_cap(cap, flags, depth: Const): + # Unrolled over the cap's nodes, so every address is an immediate. Every node's + # hinted digest (node k at heap cells 2k, 2k+1) is read once into a frame run, + # node k at cells 2(k - 1) and 2k - 1, where an internal node's hash writes its + # digest, landing on (and so asserting) the hinted value. An active (flagged) + # node is hashed; an inactive one pins its children's flags to zero, so every + # active node's parent is active too, up to the root, which is always hashed. + # The queries flag the parents of the cap leaves they reach. The run is sized + # per depth, the cap depths a configuration reaches. + if depth != 0: + n_leaves = 2 ** depth + if const(depth == 4): + runs = StackBuf(4 * 16 - 2) + elif const(depth == 5): + runs = StackBuf(4 * 32 - 2) + elif const(depth == 6): + runs = StackBuf(4 * 64 - 2) + elif const(depth == 7): + runs = StackBuf(4 * 128 - 2) + else: + runs = StackBuf(4 * 256 - 2) + for node in unroll(1, 2 * n_leaves): + at = 2 * (node - 1) + runs[at] = cap[GEN ** (2 * node)] + runs[at + 1] = cap[GEN ** (2 * node + 1)] + for node in unroll(1, n_leaves): + left = 2 * (2 * node - 1) + right = left + 2 + at = 2 * (node - 1) + if const(node == 1): + keccak(runs[left:left + 2], runs[right:right + 2], runs[at:at + 2]) + else: + if flags[GEN ** node] != 0: + keccak(runs[left:left + 2], runs[right:right + 2], runs[at:at + 2]) + else: + if const(2 * node // n_leaves == 0): # node has flagged children + flags[GEN ** (2 * node)] = 0 + flags[GEN ** (2 * node + 1)] = 0 + root_0 = runs[0] + root_1 = runs[1] + else: + root_0 = cap[GEN ** 2] + root_1 = cap[GEN ** 3] + return root_0, root_1 + + +# ============================== the stacked WHIR opening ============================ + + +def opening_fold(fs0, fs1, fs2, fs3, fs4, w0, w1, cursor, c0, c1, c2): + # One fold round: its challenge takes the two scalars waiting in the sponge, w0 and + # w1, and the next round's two coefficients, returned, wait in their place. + cap = fs_cap(fs0, fs1, fs2, fs3, fs4) + rate = StackBuf(8) + fs_pack(rate, 0, w0, DS_OBSERVE) + fs_pack(rate, 1, w1, DS_OBSERVE) + sp, r = squeeze(cap, rate, 2) + claim = (c2 * r + c1) * r + c0 + # The next coefficients are read raw: every consumer of w0, w1 (the next fold, the + # level's root absorb, the final message, the closing round) packs them first. + c0 = cursor[GEN ** 0] + c2 = cursor[GEN ** 1] + return sp[8], sp[9], sp[10], sp[11], sp[12], cursor * GEN ** 2, claim, c0, c2, r + + +def opening_ood_point(fs0, fs1, fs2, fs3, fs4, r0, r1, r2, r3, r4, r5, r6, r7, point, n: Const): + # One specialization per point dimension. The first coordinate's permutation takes + # the rate r0..r7, the scalars waiting in the sponge and zero past them. + cap = fs_cap(fs0, fs1, fs2, fs3, fs4) + sp = StackBuf(KECCAK_STATE) + keccak([r0, r1], [r2, r3], sp, tail=[r4, r5, r6, r7], cap=cap) + point[GEN ** 0] = challenge_from_state(sp) + no_rate = StackBuf(8) + for x in unroll(1, n): + sp, r = squeeze(sp, no_rate, 0) + point[GEN ** x] = r + return sp[8], sp[9], sp[10], sp[11], sp[12] + + +def opening_final_message(fs0, fs1, fs2, fs3, fs4, w0, w1, cursor, out, n: Const): + # The final message's n words, after the two scalars waiting in the sponge, w0 and + # w1, then the grinding nonce and the permutation its challenge comes from. Returns + # that output's first cell, its capacity, the cursor past the nonce, and the nonce. + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + rate = StackBuf(8) + fs_pack(rate, 0, w0, DS_OBSERVE) + fs_pack(rate, 1, w1, DS_OBSERVE) + for pos in unroll(2, 2 + n): + if pos % 4 == 0: + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + value, cursor = fs_next(rate, pos % 4, cursor) + out[GEN ** (pos - 2)] = value + nonce = cursor[GEN ** 0] + if const((2 + n) % 4 == 0): + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + sp = fs_absorb_nonce(sp, rate, (2 + n) % 4, nonce) + return sp[0], sp[8], sp[9], sp[10], sp[11], sp[12], cursor * GEN, nonce + + +def opening_row_weights(point, out, folds: Const, reverse: Const): + # The row's eq weights as their two tensor factors: weight i is + # out[i % 2^lo] * out[2^lo + i // 2^lo], the low `lo` coordinates first. At + # level 0 lane i is interleaving index 2^folds - 1 - i, which reverses both. + if const(folds // 3 == 0): + lo = folds + else: + lo = 3 + hi = folds - lo + for i in unroll(0, 2 ** lo): + if reverse == 1: + slot = 2 ** lo - 1 - i + else: + slot = i + out[GEN ** i] = eq_weight(point, lo, slot, 0) + for i in unroll(0, 2 ** hi): + if reverse == 1: + slot = 2 ** hi - 1 - i + else: + slot = i + out[GEN ** (2 ** lo + i)] = eq_weight(point * GEN ** lo, hi, slot, 0) + return + + +@inline +def walk_window(n0, n1, path, lvl0: Const, width: Const, k: Const): + # Levels lvl0..lvl0+width of a Merkle path, whose order bits are k's: each hashes the running + # node with the sibling on the other side of its hinted pair, which binds the link. + for r in unroll(0, width): + lvl = lvl0 + r + nxt = StackBuf(2) + if const((k // 2 ** r) % 2 == 0): + keccak([n0, n1], path[4 * lvl + 2:4 * lvl + 4], nxt) + else: + keccak(path[4 * lvl:4 * lvl + 2], [n0, n1], nxt) + n0 = nxt[0] + n1 = nxt[1] + return n0, n1 + + +def walk_path(n0, n1, wins, cap, flags, path_depth: Const, cap_depth: Const): + # The path, a window of QW levels a dispatch (`decode_query_windows`): each arm knows its + # levels' order bits, so it hashes the running node against the sibling its hinted pair holds. + path = StackBuf(LIG_PATH_CAP) + hint_witness(path[0:4 * path_depth], "merkle_children") + for i in unroll(0, (path_depth + QW - 1) // QW): + if const(QW * i + QW == QW * ((path_depth + QW - 1) // QW)): + width = path_depth - QW * i + else: + width = QW + w = wins[GEN ** i] + if const(width == 6): + n0, n1 = match(log(w), range(0, 64), lambda k: walk_window(n0, n1, path, QW * i, 6, k)) + elif const(width == 5): + n0, n1 = match(log(w), range(0, 32), lambda k: walk_window(n0, n1, path, QW * i, 5, k)) + elif const(width == 4): + n0, n1 = match(log(w), range(0, 16), lambda k: walk_window(n0, n1, path, QW * i, 4, k)) + elif const(width == 3): + n0, n1 = match(log(w), range(0, 8), lambda k: walk_window(n0, n1, path, QW * i, 3, k)) + elif const(width == 2): + n0, n1 = match(log(w), range(0, 4), lambda k: walk_window(n0, n1, path, QW * i, 2, k)) + else: + n0, n1 = match(log(w), range(0, 2), lambda k: walk_window(n0, n1, path, QW * i, 1, k)) + # The reached cap leaf is node 2*parent + bit at cells 2*node, 2*node + 1, where + # parent = 2^(cap_depth-1) + q: the child bit and q are the last two windows, g^bit + # and g^q, the constant parts folded into the address immediates. Every cap is at + # least two levels deep (`decode_query_windows` gives q cap_depth - 1 bits). + wb = wins[GEN ** ((path_depth + QW - 1) // QW)] + q = wins[GEN ** ((path_depth + QW - 1) // QW + 1)] + flags_q = flags * q + flags_q[GEN ** (2 ** (cap_depth - 1))] = 1 # the cap check propagates this obligation to the root + q2 = q * q + leaf = cap * q2 * q2 * (wb * wb) + leaf[GEN ** (2 ** (cap_depth + 1))] = n0 + leaf[GEN ** (2 ** (cap_depth + 1) + 1)] = n1 + return + + +@inline +def opening_query(cap, flags, row_eq_weights, wins, base: Const, interleave: Const, leaf_cells: Const, depth: Const, cap_depth: Const): + # One query: hash the hinted row up its Merkle path into the shared cap, and + # return its dot product with the row's eq weights. + if base == 1: + row_len = interleave + else: + row_len = 3 * interleave + row = StackBuf(LIG_ROW_CAP) + hint_witness(row[0:row_len], "merkle_leaf_rows") + row_dot = 0 + packed_row = StackBuf(LIG_PACKED_ROW_CAP) + if base == 1: + # Packing proves each hinted lane is in K before hashing or folding it. + for jb in unroll(0, interleave // 2): + packed_row[jb] = pack64x2(row[2 * jb], row[2 * jb + 1]) + else: + # Pack the checked tower limbs into the leaf's contiguous byte image. + for jb in unroll(0, 3 * interleave // 2): + packed_row[jb] = pack64x2(row[2 * jb], row[2 * jb + 1]) + # The weights' tensor factors (opening_row_weights): a run of lo_len row + # entries shares one hi factor. + if const(interleave // 8 == 0): + lo_len = interleave + else: + lo_len = 8 + for h in unroll(0, interleave // lo_len): + part = 0 + for l in unroll(0, lo_len): + jw = lo_len * h + l + if base == 1: + row_word = row[jw] + elif 3 * jw % 2 == 0: + # limbs (3w, 3w+1) are a pack; add Y^2 * limb(3w+2). + row_word = packed_row[3 * jw // 2] + Y_TOWER * Y_TOWER * row[3 * jw + 2] + else: + # limbs (3w+1, 3w+2) are a pack; shift it by Y and add limb(3w). + row_word = row[3 * jw] + Y_TOWER * packed_row[(3 * jw + 1) // 2] + part += row_word * row_eq_weights[GEN ** l] + row_dot += row_eq_weights[GEN ** (lo_len + h)] * part + # Hash the packed row. + leaf_hash_state = StackBuf(2) + keccak_cells(packed_row[0:leaf_cells], leaf_hash_state) + walk_path(leaf_hash_state[0], leaf_hash_state[1], wins, cap, flags, depth - cap_depth, cap_depth) + return row_dot + + +def opening_queries(cap, flags, lam, lam_n, query_win_ptrs, row_eq_weights, n_iters_g, odd: Const, base: Const, interleave: Const, leaf_cells: Const, depth: Const, cap_depth: Const): + # Specialize by row and path shape so opening configurations share query code. + # Base-field rows (level 0) run two queries an iteration, sharing the loop + # overhead and the weight reads; an odd count runs its first query alone, + # ahead of the pairs. Extension rows run one query an iteration. + # Query k is weighted lam^k: the chain runs h <- (h + dot_k) / lam, ending at + # sum_k lam^(k - n) dot_k, and one product by lam_n = lam^n restores it. + mu = 1 / lam + chain = HeapBuf(n_iters_g * GEN) + if odd == 1: + chain[GEN ** 0] = opening_query(cap, flags, row_eq_weights, query_win_ptrs[GEN ** 0], base, interleave, leaf_cells, depth, cap_depth) * mu + else: + chain[GEN ** 0] = 0 + for xi in mul_range(1, n_iters_g): + if base == 1: + bit_ptrs = query_win_ptrs * xi * xi + d0 = opening_query(cap, flags, row_eq_weights, bit_ptrs[GEN ** odd], base, interleave, leaf_cells, depth, cap_depth) + d1 = opening_query(cap, flags, row_eq_weights, bit_ptrs[GEN ** (odd + 1)], base, interleave, leaf_cells, depth, cap_depth) + chain[xi * GEN] = ((chain[xi] + d0) * mu + d1) * mu + else: + d = opening_query(cap, flags, row_eq_weights, query_win_ptrs[xi], base, interleave, leaf_cells, depth, cap_depth) + chain[xi * GEN] = (chain[xi] + d) * mu + return chain[n_iters_g] * lam_n + + +@inline +def residual_query(basis_chain, row_d, n_cols: Const): + # prod_t (d_t + chain_t) over one query's message-column coordinates, chain_0 + # being the query position. + prefix_eq = row_d[GEN ** 0] + basis_chain + for t in unroll(1, n_cols): + # subspace-vanishing recurrence for the novel-basis point, a function of n_cols alone + basis_chain *= basis_chain + VANISH_BY_COLS[n_cols * (n_cols + 1) // 2 + t - 1] + prefix_eq *= row_d[GEN ** t] + basis_chain + return prefix_eq + + +def residual_batch(batch, row_d, mu, h, n_cols: Const): + # RES_BATCH queries of a level's residual chain, sharing the row's reads and constants. + for k in unroll(0, RES_BATCH): + h = (h + residual_query(batch[GEN ** k], row_d, n_cols)) * mu + return h + + +def level_residual(positions, row_d, mu, n_queries: Const, n_cols: Const): + # A level's residual weights at the terminal point: RES_BATCH queries an iteration + # (`residual_batch`), then the rest one at a time. Query q is weighted lam^q: the + # chain runs h <- (h + eq_q) * mu with mu = 1/lam, ending at lam^-n * sum_q lam^q eq_q. + chain = HeapBuf(GEN ** (n_queries // RES_BATCH + 1)) + chain[GEN ** 0] = 0 + for xr in mul_range(1, GEN ** (n_queries // RES_BATCH)): + chain[xr * GEN] = residual_batch(positions * xr ** RES_BATCH, row_d, mu, chain[xr], n_cols) + h = chain[GEN ** (n_queries // RES_BATCH)] + if const(n_queries % RES_BATCH != 0): + rest = HeapBuf(GEN ** (n_queries % RES_BATCH + 1)) + rest[GEN ** 0] = h + tail = positions * (GEN ** (n_queries // RES_BATCH)) ** RES_BATCH + for xq in mul_range(1, GEN ** (n_queries % RES_BATCH)): + rest[xq * GEN] = (rest[xq] + residual_query(tail[xq], row_d, n_cols)) * mu + h = rest[GEN ** (n_queries % RES_BATCH)] + return h + + +def level_positions(fs0, fs1, fs2, fs3, fs4, positions, win_ptrs, win_tabs, squeezes: Const, depth: Const, cap_depth: Const): + # A level's query positions, squeezed from the sponge and decoded, one squeeze per + # FIELD_BITS // depth positions; shared by every stack size with the level's shape. + sqz = HeapBuf((GEN ** (squeezes + 1)) ** FS_CELLS) + sqz[GEN ** 0] = fs0 + sqz[GEN ** 1] = fs1 + sqz[GEN ** 2] = fs2 + sqz[GEN ** 3] = fs3 + sqz[GEN ** 4] = fs4 + for xs in mul_range(1, GEN ** squeezes): + row = sqz * xs ** FS_CELLS + row_cap = fs_load(row, 0, 1) + no_rate = StackBuf(8) + row_sp, packed_word = squeeze(row_cap, no_rate, 0) + fs_store(row * GEN ** FS_CELLS, 0, 1, row_sp) + query_ptr = xs ** (FIELD_BITS // depth) + decode_query_windows(packed_word, positions * query_ptr, win_ptrs * query_ptr, win_tabs, depth, cap_depth) + sp = fs_load(sqz * (GEN ** squeezes) ** FS_CELLS, 0, 1) + return sp[0], sp[1], sp[2], sp[3], sp[4] + + +def level_queries(lam, win_ptrs, row_eq_weights, n_queries: Const, base: Const, interleave: Const, leaf_cells: Const, depth: Const, cap_depth: Const): + # A level's Merkle cap, hinted and checked up to its root, and the level's query batch + # walked into it. Returns the root, the batch's sum, and lam^n_queries. + cap = HeapBuf(GEN ** (4 * 2 ** cap_depth)) + hint_witness(cap[0:4 * 2 ** cap_depth], "merkle_caps") + flags = HeapBuf(GEN ** (2 ** cap_depth)) + hint_witness(flags[0:2 ** cap_depth], "merkle_cap_active") + root_0, root_1 = verify_merkle_cap(cap, flags, cap_depth) + lam_n = lam ** n_queries + if const(base == 1): + n_iters = n_queries // 2 + odd = n_queries % 2 + else: + n_iters = n_queries + odd = 0 + total = opening_queries(cap, flags, lam, lam_n, win_ptrs, row_eq_weights, GEN ** n_iters, odd, base, interleave, leaf_cells, depth, cap_depth) + return root_0, root_1, total, lam_n + + +def open_stacked(m_idx: Const, rate_sel, fs0, fs1, fs2, fs3, fs4, target, commit_root_0, commit_root_1, cursor): + # The stacked WHIR opening, one specialization per committed log-size m_idx, the + # rate g^rate_sel dispatched per level by a `match` into `level_positions`, + # `level_queries` and `level_residual`, which read the tables a rate changes (query + # counts, tree and cap depths, squeezes) at `ml + r * LIG_RATE_STRIDE`. Every other + # LIG_* table is read at the size's first-rate row `ml`, which `opening_placeholders` + # checks does not vary by rate. Only the executed arm pops the opening's hint streams. + # + # The returned point is in witness order. Round-order challenges stay local + # for the induced weights, OOD claims, and residual evaluation. + n_levels = LIG_N_LEVELS[m_idx] + yr_level = LIG_YR_LEVEL[m_idx] + yr_log = LIG_YR_LOG_LEN[m_idx] + yr_len = LIG_YR_LEN[m_idx] + n_folds = LIG_TOTAL_FOLDS[m_idx] + ood_stride = LIG_MAX_OOD_SAMPLES * OOD_SLOTS + + # The opening's scalars (sumcheck messages, level roots, nonces, final message) + # ride the SHARED stream, walked on in protocol order. The K opener binds a + # Merkle root as its two F192 scalars, not as a byte string, and every digest + # uses ONE 128/128 encoding, so those scalars are exactly the root's two cells. + # Between rounds the sponge rides fs0..fs4, its capacity, and w0, w1, the two + # scalars waiting in it: the last round polynomial's sent coefficients. + msg_cursor = cursor + round_quad_c = msg_cursor[GEN ** 0] # the round polynomial in coefficients + round_quad_a = msg_cursor[GEN ** 1] # bar the linear one, which the split fixes + msg_cursor = msg_cursor * GEN ** 2 + w0 = round_quad_c + w1 = round_quad_a + round_quad_b = target + round_quad_a + sumcheck_target = target + + final_msg = HeapBuf(GEN ** yr_len) # filled from the stream at the last level + # Each cap root is checked against its transcript-bound level root. + level_roots = HeapBuf(GEN ** (2 * n_levels)) + level_roots[GEN ** 0] = commit_root_0 + level_roots[GEN ** 1] = commit_root_1 + # ...and guest-filled accumulators (one slot per fold / per level / per query): + fold_challenges = HeapBuf(GEN ** n_folds) + level_betas = HeapBuf(GEN ** n_levels) + level_mus = HeapBuf(GEN ** n_levels) + # A level's query positions, at most LIG_LEVEL_POSITIONS_CAP at any rate, start at + # lvl * LIG_LEVEL_POSITIONS_CAP. + query_positions = HeapBuf(GEN ** (n_levels * LIG_LEVEL_POSITIONS_CAP)) + query_win_ptrs = HeapBuf(GEN ** (n_levels * LIG_LEVEL_POSITIONS_CAP)) + # A query window's bit pattern by its g-power (`decode_query_windows`): win_tabs[g^n] = n. + win_tabs = HeapBuf(GEN ** 128) + for n in unroll(0, 128): + win_tabs[GEN ** n] = const(n) + # Explicit OOD claims bind every recursive Johnson-list commitment. L0 needs + # none: the opening claim itself is its post-commit binding value. An OOD claim + # is read before its level's query positions but batched after them (one + # challenge per level), so its value and intro message wait here. + ood_z = HeapBuf(GEN ** (n_levels * LIG_MAX_OOD_SAMPLES * LIG_LOG_MSG_COLS_CAP)) + ood = HeapBuf(GEN ** (n_levels * ood_stride)) + + for lvl in unroll(0, n_levels): + ml = m_idx * LIG_MAX_LEVELS + lvl + interleave = LIG_INTERLEAVE[ml] + folds_off = LIG_FOLDS_OFF[ml] + pos_off = lvl * LIG_LEVEL_POSITIONS_CAP + for j in unroll(0, LIG_FOLDS[ml]): + fs0, fs1, fs2, fs3, fs4, msg_cursor, sumcheck_target, round_quad_c, round_quad_a, fold_challenge = opening_fold(fs0, fs1, fs2, fs3, fs4, w0, w1, msg_cursor, round_quad_c, round_quad_b, round_quad_a) + w0 = round_quad_c + w1 = round_quad_a + fold_challenges[GEN ** (folds_off + j)] = fold_challenge + round_quad_b = sumcheck_target + round_quad_a + + if lvl == yr_level: + word0, fs0, fs1, fs2, fs3, fs4, msg_cursor, q_nonce = opening_final_message(fs0, fs1, fs2, fs3, fs4, w0, w1, msg_cursor, final_msg, yr_len) + else: + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + rate = StackBuf(8) + fs_pack(rate, 0, w0, DS_OBSERVE) + fs_pack(rate, 1, w1, DS_OBSERVE) + next_root_a, msg_cursor = fs_next(rate, 2, msg_cursor) + next_root_b, msg_cursor = fs_next(rate, 3, msg_cursor) + # A non-canonical half is rejected (`Digest::from_halves` in `crates/verifier`), as + # the commitment root was at its own read. + canon = StackBuf(2) + canon[0] = assert_canonical(next_root_a) + canon[1] = assert_canonical(next_root_b) + level_roots[GEN ** (2 * lvl + 2)] = next_root_a + level_roots[GEN ** (2 * lvl + 3)] = next_root_b + # OOD binding for the newly observed level-(lvl+1) commitment. The + # random point has the just-folded witness dimension, namely this + # level's message-column dimension. + # The point's first permutation takes what waits: the fold's coefficients and + # the root, four scalars, or the previous sample's three. + for os in unroll(0, LIG_OOD_SAMPLES[ml + 1]): + oz = ood_z * GEN ** (((lvl + 1) * LIG_MAX_OOD_SAMPLES + os) * LIG_LOG_MSG_COLS_CAP) + if os == 0: + fs0, fs1, fs2, fs3, fs4 = opening_ood_point(fs0, fs1, fs2, fs3, fs4, rate[0], rate[1], rate[2], rate[3], rate[4], rate[5], rate[6], rate[7], oz, LIG_LOG_MSG_COLS[ml]) + else: + fs0, fs1, fs2, fs3, fs4 = opening_ood_point(fs0, fs1, fs2, fs3, fs4, rate[0], rate[1], rate[2], rate[3], rate[4], rate[5], 0, 0, oz, LIG_LOG_MSG_COLS[ml]) + sample = ood * GEN ** ((lvl + 1) * ood_stride + os * OOD_SLOTS) + rate = StackBuf(8) + ood_y, msg_cursor = fs_next(rate, 0, msg_cursor) + ood_c0, msg_cursor = fs_next(rate, 1, msg_cursor) + ood_c2, msg_cursor = fs_next(rate, 2, msg_cursor) + sample[GEN ** OOD_Y] = ood_y + sample[GEN ** OOD_C0] = ood_c0 + sample[GEN ** OOD_C2] = ood_c2 # the split fixes c1 = y + c2 + q_nonce = msg_cursor[GEN ** 0] + msg_cursor = msg_cursor * GEN + if LIG_OOD_SAMPLES[ml + 1] == 0: + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + sp = fs_absorb_nonce(sp, rate, 0, q_nonce) + else: + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + sp = fs_absorb_nonce(sp, rate, 3, q_nonce) + word0 = sp[0] + fs0 = sp[8] + fs1 = sp[9] + fs2 = sp[10] + fs3 = sp[11] + fs4 = sp[12] + # The nonce, a raw transport word, is bound by its DS_POW_NONCE absorb above. + grind_check(word0, LIG_QUERY_GRIND_BITS[ml]) + + fs0, fs1, fs2, fs3, fs4 = match(log(rate_sel), range(0, LIG_N_RATES), lambda r: level_positions(fs0, fs1, fs2, fs3, fs4, query_positions * GEN ** pos_off, query_win_ptrs * GEN ** pos_off, win_tabs, LIG_SQUEEZES[ml + r * LIG_RATE_STRIDE], LIG_TREE_DEPTH[ml + r * LIG_RATE_STRIDE], LIG_CAP_DEPTH[ml + r * LIG_RATE_STRIDE])) + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + + # One batching challenge for the level, drawn once every claim it batches is + # fixed: its OOD claims above and these query positions. Claim tau of the + # level is weighted lam^tau, the running claim keeping lam^0 (Annex B, + # Protocol 1 step 1): query i is claim n_ood + 1 + i, so its weight splits + # into lam^i here and the level scalar lam^(n_ood+1) below. + no_rate = StackBuf(8) + sp, lam = squeeze(sp, no_rate, 0) + level_mus[GEN ** lvl] = 1 / lam + # At level 0, slot i of a leaf image is interleaving index n-1-i: the image + # reads its lanes from the top down, so the lanes a padding-free commitment + # leaves out are its LEADING words, whose whole blocks the committer hashes + # once for all leaves. The flip is a compile-time index and the guest still + # hashes the full image. Deeper levels commit every lane, ascending. + row_eq_weights = HeapBuf(GEN ** (LIG_MAX_INTERLEAVE[m_idx])) + opening_row_weights(fold_challenges * GEN ** folds_off, row_eq_weights, LIG_FOLDS[ml], 1 // (lvl + 1)) + + root_0, root_1, level_query_sum, lam_pow = match(log(rate_sel), range(0, LIG_N_RATES), lambda r: level_queries(lam, query_win_ptrs * GEN ** pos_off, row_eq_weights, LIG_QUERIES[ml + r * LIG_RATE_STRIDE], 1 // (lvl + 1), interleave, LIG_LEAF_CELLS[ml], LIG_TREE_DEPTH[ml + r * LIG_RATE_STRIDE], LIG_CAP_DEPTH[ml + r * LIG_RATE_STRIDE])) + level_roots[GEN ** (2 * lvl)] = root_0 + level_roots[GEN ** (2 * lvl + 1)] = root_1 + + # Every level, including the last, ties its commitment in through an intro + # message. The level's claims then enter the running one with powers of + # `lam`: the OOD claims held above first, then this query batch. + intro_c0 = msg_cursor[GEN ** 0] + intro_c2 = msg_cursor[GEN ** 1] + msg_cursor = msg_cursor * GEN ** 2 + fs0 = sp[8] + fs1 = sp[9] + fs2 = sp[10] + fs3 = sp[11] + fs4 = sp[12] + w0 = intro_c0 + w1 = intro_c2 + intro_c1 = level_query_sum + intro_c2 # the split fixes the linear coefficient + if lvl == yr_level: + beta_lvl = lam # no OOD claim at the last level: no new oracle + else: + ood_scalar = lam + for os in unroll(0, LIG_OOD_SAMPLES[ml + 1]): + sample = ood * GEN ** ((lvl + 1) * ood_stride + os * OOD_SLOTS) + ood_y = sample[GEN ** OOD_Y] + ood_c2 = sample[GEN ** OOD_C2] + sample[GEN ** OOD_BETA] = ood_scalar + round_quad_c += ood_scalar * sample[GEN ** OOD_C0] + round_quad_b += ood_scalar * (ood_y + ood_c2) + round_quad_a += ood_scalar * ood_c2 + sumcheck_target += ood_scalar * ood_y + ood_scalar = ood_scalar * lam + beta_lvl = ood_scalar + level_betas[GEN ** lvl] = beta_lvl * lam_pow # lam^n restores the residual's mu-chain + round_quad_c += beta_lvl * intro_c0 + round_quad_b += beta_lvl * intro_c1 + round_quad_a += beta_lvl * intro_c2 + sumcheck_target += beta_lvl * level_query_sum + + # ---- finish the sumcheck over the tail coordinates ---- + tail_challenges = HeapBuf(GEN ** YR_LOG_CAP) + for j in unroll(0, yr_log - 1): + fs0, fs1, fs2, fs3, fs4, msg_cursor, sumcheck_target, round_quad_c, round_quad_a, tail_c = opening_fold(fs0, fs1, fs2, fs3, fs4, w0, w1, msg_cursor, round_quad_c, round_quad_b, round_quad_a) + w0 = round_quad_c + w1 = round_quad_a + tail_challenges[GEN ** j] = tail_c + round_quad_b = sumcheck_target + round_quad_a + # The closing round sends no following message. + tail_cap = fs_cap(fs0, fs1, fs2, fs3, fs4) + tail_rate = StackBuf(8) + fs_pack(tail_rate, 0, w0, DS_OBSERVE) + fs_pack(tail_rate, 1, w1, DS_OBSERVE) + tail_sp, tail_last = squeeze(tail_cap, tail_rate, 2) + tail_challenges[GEN ** (yr_log - 1)] = tail_last + sumcheck_target = round_quad_c + tail_last * round_quad_b + tail_last * tail_last * round_quad_a + for j in unroll(yr_log, YR_LOG_CAP): + tail_challenges[GEN ** j] = 0 + + yr_at_tail = fold_final_msg(final_msg, tail_challenges, yr_log) + + # ---- the same point, indexed by committed-witness coordinate ---- + # The folds bind coordinates in ROUND order, and level 0's folds are the lane + # fold, binding the witness's TOP k coordinates: lane l of the commitment is the + # stack block q[l * 2^(mu-k) ...], which is what makes the witness's zero + # padding whole lanes for the committer to leave out of the encode. Every + # transparent weight downstream is written in witness coordinates, so rotate the + # point left by those k rounds here, while the level shape is still + # compile-time. Padding is zero so shared prefix chains may compute unused + # entries above m without reading uninitialized cells. + lane_folds = LIG_FOLDS[m_idx * LIG_MAX_LEVELS] + fold_head = n_folds - lane_folds + # point1 = 1 + point, the form every eq factor reads. + point = HeapBuf(SIZE_BITS + QKECCAK_SLOT_LOG) + point1 = HeapBuf(SIZE_BITS + QKECCAK_SLOT_LOG) + for j in unroll(0, fold_head): + point[GEN ** j] = fold_challenges[GEN ** (lane_folds + j)] + point1[GEN ** j] = 1 + fold_challenges[GEN ** (lane_folds + j)] + for j in unroll(0, yr_log): + point[GEN ** (fold_head + j)] = tail_challenges[GEN ** j] + point1[GEN ** (fold_head + j)] = 1 + tail_challenges[GEN ** j] + for j in unroll(0, lane_folds): + point[GEN ** (fold_head + yr_log + j)] = fold_challenges[GEN ** j] + point1[GEN ** (fold_head + yr_log + j)] = 1 + fold_challenges[GEN ** j] + for j in unroll(n_folds + yr_log, SIZE_BITS + QKECCAK_SLOT_LOG): + point[GEN ** j] = 0 + point1[GEN ** j] = 1 + + # ---- per-level induced bases at the single terminal point ---- + # Every query of a level runs the SAME product shape over its message-column + # coordinates; only the novel-basis chain (the query position's + # subspace-vanishing walk) differs. Since + # 1 + c_t * (1 + chain_t * inv_t) == (1 + c_t) + (c_t * inv_t) * chain_t + # a coordinate's two coefficients depend on the challenge and the baked + # vanishing inverse alone, so they hoist out of the query loop (one row a level, + # fold coords then tail coords). Factoring b_t = c_t * inv_t out of every + # coordinate leaves (a_t / b_t) + chain_t per query, so a query is one add and + # one multiply a coordinate, and the level's product of the b_t scales its sum + # once. A zero challenge makes b_t zero and the division unsatisfiable, which + # rejects rather than accepts. + basis_d = HeapBuf(GEN ** (n_levels * LIG_LOG_MSG_COLS_CAP)) + level_scale = HeapBuf(GEN ** n_levels) + for lvl in unroll(0, n_levels): + ml = m_idx * LIG_MAX_LEVELS + lvl + prefix_len = LIG_RESIDUAL_PREFIX_LEN[ml] + vanish = LIG_LOG_MSG_COLS[ml] * (LIG_LOG_MSG_COLS[ml] + 1) // 2 + scale = level_betas[GEN ** lvl] + for t in unroll(0, prefix_len): + fold_c = fold_challenges[GEN ** (LIG_RESIDUAL_FOLD_OFF[ml] + t)] + coef_b = fold_c * VANISH_INVS_BY_COLS[vanish + t] + basis_d[GEN ** (lvl * LIG_LOG_MSG_COLS_CAP + t)] = (1 + fold_c) / coef_b + scale *= coef_b + for j in unroll(0, yr_log): + tail_c = tail_challenges[GEN ** j] + coef_b = tail_c * VANISH_INVS_BY_COLS[vanish + prefix_len + j] + basis_d[GEN ** (lvl * LIG_LOG_MSG_COLS_CAP + prefix_len + j)] = (1 + tail_c) / coef_b + scale *= coef_b + level_scale[GEN ** lvl] = scale + inner_chain = HeapBuf(GEN ** (n_levels + 1)) + inner_chain[GEN ** 0] = 0 + for lvl in unroll(0, n_levels): + ml = m_idx * LIG_MAX_LEVELS + lvl + # level_betas carries the lam^n that the residual chain's 1/lam steps leave out. + h = match(log(rate_sel), range(0, LIG_N_RATES), lambda r: level_residual(query_positions * GEN ** (lvl * LIG_LEVEL_POSITIONS_CAP), basis_d * GEN ** (lvl * LIG_LOG_MSG_COLS_CAP), level_mus[GEN ** lvl], LIG_QUERIES[ml + r * LIG_RATE_STRIDE], LIG_LOG_MSG_COLS[ml])) + # accumulate beta_lvl * lam^n * prod_t b_t * (per-level residual chain) into the grand residual + inner_chain[GEN ** (lvl + 1)] = inner_chain[GEN ** lvl] + level_scale[GEN ** lvl] * h + + # Explicit OOD eq bases at the same terminal point. + ood_inner = 0 + for ood_lvl in unroll(1, n_levels): + ml = m_idx * LIG_MAX_LEVELS + ood_lvl + z_folded = LIG_LOG_MSG_COLS[ml - 1] - yr_log + ris_start = LIG_FOLDS_OFF[ml] + for os in unroll(0, LIG_OOD_SAMPLES[ml]): + oz = ood_z * GEN ** ((ood_lvl * LIG_MAX_OOD_SAMPLES + os) * LIG_LOG_MSG_COLS_CAP) + scalar = ood[GEN ** (ood_lvl * ood_stride + os * OOD_SLOTS + OOD_BETA)] + for t in unroll(0, z_folded): + scalar *= (1 + oz[GEN ** t] + fold_challenges[GEN ** (ris_start + t)]) + for t in unroll(0, yr_log): + scalar *= (1 + oz[GEN ** (z_folded + t)] + tail_challenges[GEN ** t]) + ood_inner += scalar + return sumcheck_target, point, point1, inner_chain[GEN ** n_levels] + ood_inner, yr_at_tail + + +# ============================== inner-proof verification ============================ +# The phases of `verify_sub`, in the order it runs them. Each takes and returns the +# Fiat-Shamir state pair and the stream cursor, so the sequence is what binds them. + + +def verify_bus_gkr(fs0, fs1, fs2, fs3, fs4, cursor, g_bus_mu, zeta): + # ONE GKR grand product over push, pull and count, RLC-batched. Push and pull + # have equal depth (matched blocks) and the count tree is padded with identity + # leaves up to it (product unchanged), so a single sumcheck serves all three. + # Radix four contracts two binary levels per layer, each batched by a fresh λ + # squeezed at its start. All three trees reduce to the one shared point `zeta`, + # which this fills, returning their three leaf values with the walked + # Fiat-Shamir state and stream cursor. + layers = HeapBuf((g_bus_mu * GEN ** 2) ** LAYER_SLOTS) # mu + 2 layers + rounds = HeapBuf(GKR_ROUNDS_CAP * ROUND_SLOTS) + gkr_pts = HeapBuf(GKR_POINTS_CAP) + assert log(g_bus_mu) < COUNT_BITS + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + rate = StackBuf(8) + root_push, cursor = fs_next(rate, 0, cursor) + root_pull = root_push + root_count, cursor = fs_next(rate, 1, cursor) + assert root_count != 0 # count-tree root nonzero: no read count self-cancels + # The roots wait in the sponge until the first layer's challenge: a layer's first + # permutation takes its LAYER_PEND cells, which are zero for every later layer. + for j in unroll(0, FS_CELLS): + layers[GEN ** (LAYER_FS + j)] = sp[j] + for j in unroll(0, 4): + layers[GEN ** (LAYER_PEND + j)] = rate[j] + layers[GEN ** LAYER_CURSOR] = cursor + layers[GEN ** LAYER_PUSH] = root_push + layers[GEN ** LAYER_PULL] = root_pull + layers[GEN ** LAYER_COUNT] = root_count + layers[GEN ** LAYER_ROW] = gkr_pts + layers[GEN ** LAYER_POS] = GEN ** 0 + layers[GEN ** LAYER_HALF] = GEN ** 0 + layers[GEN ** LAYER_PARITY] = GEN ** 0 + # k % 4 and its carry into k // 4 as layer k becomes k + 1, by g^(k % 4). + k_mod_next = HeapBuf(8) + for r in unroll(0, 4): + k_mod_next[GEN ** r] = GEN ** ((r + 1) % 4) + k_mod_next[GEN ** (4 + r)] = GEN ** ((r + 1) // 4) + + # Contract two binary product levels at a time. pair_bounds[g^d] = g^(d//2) is + # the radix-four layer count, and shift is g exactly when the depth is odd, in + # which case the root-most BINARY layer runs first. + pair_bounds = HeapBuf(COUNT_BITS) + depth_shift = HeapBuf(COUNT_BITS) + for depth in unroll(0, COUNT_BITS): + pair_bounds[GEN ** depth] = GEN ** (depth // 2) + depth_shift[GEN ** depth] = GEN ** (depth % 2) + + if depth_shift[g_bus_mu] != 1: + # The odd layer is layer 0, so its round state would be written and read + # back at the same position: read it straight off the layer instead. + layer_cap = fs_load(layers, LAYER_FS, 1) + tail_sp = fs_perm4(layer_cap, layers[GEN ** LAYER_PEND], layers[GEN ** (LAYER_PEND + 1)], layers[GEN ** (LAYER_PEND + 2)], layers[GEN ** (LAYER_PEND + 3)]) + lam = challenge_from_state(tail_sp) + rate = StackBuf(8) + tcur = layers[GEN ** LAYER_CURSOR] + tclaim = layers[GEN ** LAYER_PUSH] + lam * (layers[GEN ** LAYER_PULL] + lam * layers[GEN ** LAYER_COUNT]) + nextrow = layers[GEN ** LAYER_ROW] * GEN ** MU_CAP + evals = StackBuf(2 * N_GKR_SIDES) # the two children of each side, in side order + for i in unroll(0, 2 * N_GKR_SIDES): + if i % 4 == 0: + if i != 0: + tail_sp = fs_perm(tail_sp, rate, 4) + rate = StackBuf(8) + ev, tcur = fs_next(rate, i % 4, tcur) + evals[i] = ev + combined = 0 + for i in unroll(0, N_GKR_SIDES): + side = N_GKR_SIDES - 1 - i # Horner in lam, so the top side lands last + combined = evals[2 * side] * evals[2 * side + 1] + lam * combined + assert tclaim == combined + tail_sp, c0 = squeeze(tail_sp, rate, (2 * N_GKR_SIDES + 3) % 4 + 1) + nextrow[GEN ** 0] = c0 + nxt = layers * GEN ** LAYER_SLOTS + fs_store(nxt, LAYER_FS, 1, tail_sp) + for j in unroll(0, 4): + nxt[GEN ** (LAYER_PEND + j)] = 0 + nxt[GEN ** LAYER_CURSOR] = tcur + for side in unroll(0, N_GKR_SIDES): + nxt[GEN ** (LAYER_PUSH + side)] = evals[2 * side] + c0 * (evals[2 * side] + evals[2 * side + 1]) + nxt[GEN ** LAYER_ROW] = nextrow + nxt[GEN ** LAYER_POS] = GEN + nxt[GEN ** LAYER_HALF] = GEN ** 0 + nxt[GEN ** LAYER_PARITY] = GEN ** 0 + + for x_pair in mul_range(1, pair_bounds[g_bus_mu]): + sq = x_pair * x_pair + x_layer = sq * depth_shift[g_bus_mu] + layer = layers * x_layer ** LAYER_SLOTS + layer_cap = fs_load(layer, LAYER_FS, 1) + layer_sp = fs_perm4(layer_cap, layer[GEN ** LAYER_PEND], layer[GEN ** (LAYER_PEND + 1)], layer[GEN ** (LAYER_PEND + 2)], layer[GEN ** (LAYER_PEND + 3)]) + lam = challenge_from_state(layer_sp) + point_row = layer[GEN ** LAYER_ROW] + round_pos = layer[GEN ** LAYER_POS] + nextrow = point_row * GEN ** MU_CAP + # Round i's record keeps field k at rounds * g^(k GKR_ROUNDS_CAP + i), so a + # round's record is one product away from the counter. + head = rounds * round_pos + fs_store(head, ROUND_FS, GKR_ROUNDS_CAP, layer_sp) + head[GEN ** (ROUND_CURSOR * GKR_ROUNDS_CAP)] = layer[GEN ** LAYER_CURSOR] + head[GEN ** (ROUND_CLAIM * GKR_ROUNDS_CAP)] = layer[GEN ** LAYER_PUSH] + lam * (layer[GEN ** LAYER_PULL] + lam * layer[GEN ** LAYER_COUNT]) + # 2k rounds, or 2k + 1 when the depth is odd: eight a loop step, the state + # between them staying in the frame, then 2 (k % 4), then the odd one. + half = layer[GEN ** LAYER_HALF] + parity = layer[GEN ** LAYER_PARITY] + for x8 in mul_range(1, half): + step = x8 ** 8 + rd = head * step + pt = point_row * step + round_sp = fs_load(rd, ROUND_FS, GKR_ROUNDS_CAP) + cur = rd[GEN ** (ROUND_CURSOR * GKR_ROUNDS_CAP)] + claim = rd[GEN ** (ROUND_CLAIM * GKR_ROUNDS_CAP)] + for q in unroll(0, 8): + # As in gkr_rounds. + rate = StackBuf(8) + c1, cur = fs_next(rate, 0, cur) + c2, cur = fs_next(rate, 1, cur) + c3, cur = fs_next(rate, 2, cur) + c4, cur = fs_next(rate, 3, cur) + round_sp, rk = squeeze(round_sp, rate, 4) + c0 = claim + pt[GEN ** q] * (c1 + c2 + c3 + c4) + pt[GEN ** (MU_CAP + 2 + q)] = rk # nextrow[round * g^2] + claim = c0 + rk * (c1 + rk * (c2 + rk * (c3 + rk * c4))) + rd_next = rd * GEN ** 8 + fs_store(rd_next, ROUND_FS, GKR_ROUNDS_CAP, round_sp) + rd_next[GEN ** (ROUND_CURSOR * GKR_ROUNDS_CAP)] = cur + rd_next[GEN ** (ROUND_CLAIM * GKR_ROUNDS_CAP)] = claim + octet = half ** 8 + rem_head = head * octet + rem_row = point_row * octet + done = match(log(parity), range(0, 4), lambda r: gkr_rounds(rem_head, rem_row, 2 * r)) + if x_layer != sq: + done = gkr_rounds(head * sq, point_row * sq, 1) + tail = head * x_layer + tail_sp = fs_load(tail, ROUND_FS, GKR_ROUNDS_CAP) + rate = StackBuf(8) + tcur = tail[GEN ** (ROUND_CURSOR * GKR_ROUNDS_CAP)] + tclaim = tail[GEN ** (ROUND_CLAIM * GKR_ROUNDS_CAP)] + evals = StackBuf(4 * N_GKR_SIDES) # the four children of each side, in side order + for i in unroll(0, 4 * N_GKR_SIDES): + if i % 4 == 0: + if i != 0: + tail_sp = fs_perm(tail_sp, rate, 4) + rate = StackBuf(8) + ev, tcur = fs_next(rate, i % 4, tcur) + evals[i] = ev + combined = 0 + for i in unroll(0, N_GKR_SIDES): + side = N_GKR_SIDES - 1 - i # Horner in lam, so the top side lands last + combined = evals[4 * side] * evals[4 * side + 1] * evals[4 * side + 2] * evals[4 * side + 3] + lam * combined + assert tclaim == combined + tail_sp, c0 = squeeze(tail_sp, rate, (4 * N_GKR_SIDES + 3) % 4 + 1) + tail_sp, c1 = squeeze(tail_sp, rate, 0) + nextrow[GEN ** 0] = c0 + nextrow[GEN ** 1] = c1 + nxt = layer * GEN ** (2 * LAYER_SLOTS) + fs_store(nxt, LAYER_FS, 1, tail_sp) + for j in unroll(0, 4): + nxt[GEN ** (LAYER_PEND + j)] = 0 + nxt[GEN ** LAYER_CURSOR] = tcur + for side in unroll(0, N_GKR_SIDES): + lo = evals[4 * side] + c0 * (evals[4 * side] + evals[4 * side + 1]) + hi = evals[4 * side + 2] + c0 * (evals[4 * side + 2] + evals[4 * side + 3]) + nxt[GEN ** (LAYER_PUSH + side)] = lo + c1 * (lo + hi) + nxt[GEN ** LAYER_ROW] = nextrow + nxt[GEN ** LAYER_POS] = round_pos * x_layer * GEN + nxt[GEN ** LAYER_HALF] = half * k_mod_next[GEN ** 4 * parity] + nxt[GEN ** LAYER_PARITY] = k_mod_next[parity] + last = layers * g_bus_mu ** LAYER_SLOTS + cursor = last[GEN ** LAYER_CURSOR] + final_point_row = last[GEN ** LAYER_ROW] + for xt in mul_range(1, g_bus_mu): + zeta[xt] = final_point_row[xt] # the ONE shared point + return last[GEN ** LAYER_FS], last[GEN ** (LAYER_FS + 1)], last[GEN ** (LAYER_FS + 2)], last[GEN ** (LAYER_FS + 3)], last[GEN ** (LAYER_FS + 4)], cursor, last[GEN ** LAYER_PUSH], last[GEN ** LAYER_PULL], last[GEN ** LAYER_COUNT] + + +def index_mle(zeta, n: Const): + # The Index coordinate's MLE over the block's n = κ low coords: + # prod_t (1 + zeta_t * (1 + g^(2^t))). + v = GEN ** 0 + for t in unroll(0, n): + if t == 0: + v = 1 + zeta[GEN ** t] * INDEX_MLE_FACTORS[t] + else: + v *= 1 + zeta[GEN ** t] * INDEX_MLE_FACTORS[t] + return v + + +@inline +def sel_eq_nibble(out, z, z1, width: Const): + # The eq tables over coordinates z[0..t] for t < width, level t at 2^(t+1) - 2, + # coordinate t being index bit t; z1 = 1 + z. + levels = StackBuf(SEL_EQ_STRIDE) + levels[0] = z1[GEN ** 0] + levels[1] = z[GEN ** 0] + for t in unroll(1, width): + r = z[GEN ** t] + r1 = z1[GEN ** t] + for i in unroll(0, 2 ** t): + levels[2 ** (t + 1) - 2 + i] = levels[2 ** t - 2 + i] * r1 + levels[2 ** (t + 1) - 2 + 2 ** t + i] = levels[2 ** t - 2 + i] * r + for i in unroll(0, 2 ** (width + 1) - 2): + out[GEN ** i] = levels[i] + return + + +@inline +def sel_g_nibble(out, gs, width: Const): + # out[u] = prod_t gs[t]^(bit t of u) for u < 2^width. + vals = StackBuf(16) + vals[0] = 1 + vals[1] = gs[GEN ** 0] + for t in unroll(1, width): + g_t = gs[GEN ** t] + for i in unroll(0, 2 ** t): + vals[2 ** t + i] = vals[i] * g_t + for i in unroll(0, 2 ** width): + out[GEN ** i] = vals[i] + return + + +def sel_partial_nibble(eq_out, g_out, z, z1, gs, width: Const): + if const(width != 0): + sel_eq_nibble(eq_out, z, z1, width) + sel_g_nibble(g_out, gs, width) + return 0 + + +def shared_selector_tables(eq_tab, g_tab, zeta_hi, zeta1_hi, gsq_hi, len_g): + # Per nibble j of a selector run of length n = 4q + r (coordinates 4j..4j+3 + # past κ, the last nibble r wide), the eq tables over its low 1..4 + # coordinates and the g-powers of its indices' offset shares. The split of n + # is advice, each bit pinned and the whole tied back to len_g. + nb = StackBuf(6) + hint_decompose_bits_exponent(nb, len_g, 6) + for k in unroll(0, 6): + b = nb[k] + nb[k] = b * b + r_g = (1 + nb[0] * (1 + GEN)) * (1 + nb[1] * (1 + GEN ** 2)) + q_g = GEN ** 0 + for k in unroll(2, 6): + q_g *= 1 + nb[k] * (1 + GEN ** (2 ** (k - 2))) + q4 = q_g ** 4 + assert q4 * r_g == len_g + for xq in mul_range(1, q_g): + x4 = xq ** 4 + x16 = x4 ** 4 + sel_eq_nibble(eq_tab * x16 * x16, zeta_hi * x4, zeta1_hi * x4, 4) + sel_g_nibble(g_tab * x16, gsq_hi * x4, 4) + q16 = q4 ** 4 + done = match(log(r_g), range(0, 4), lambda w: sel_partial_nibble(eq_tab * q16 * q16, g_tab * q16, zeta_hi * q4, zeta1_hi * q4, gsq_hi * q4, w)) # a match needs a target + return + + +@inline +def assert_digit(d, width: Const): + # d = g^u with u < 2^width: three cycles, no XOR. + if const(width == 1): + assert log(d) < 2 + if const(width == 2): + assert log(d) < 4 + if const(width == 3): + assert log(d) < 8 + if const(width == 4): + assert log(d) < 16 + return + + +def block_selector_tab(digits, eq_tab, g_tab, n: Const): + # block_selector off the shared tables: per nibble, a hinted g-power digit g^u, + # range-checked below the nibble's table size, reads its eq factor and its share + # of g^offset. The caller pins the shares' product to the certified offset, which + # makes every digit the offset's own (a base-16 representation is unique). + eq = GEN ** 0 + rebuilt = GEN ** 0 + for j in unroll(0, (n + 3) // 4): + width = 4 + if const(j == n // 4): + width = n - 4 * j + d = digits[GEN ** j] + assert_digit(d, width) + if const(j == 0): + eq = eq_tab[GEN ** (SEL_TAB_EQ_STRIDE * j + 2 ** width - 2) * d] + rebuilt = g_tab[GEN ** (16 * j) * d] + else: + eq *= eq_tab[GEN ** (SEL_TAB_EQ_STRIDE * j + 2 ** width - 2) * d] + rebuilt *= g_tab[GEN ** (16 * j) * d] + return eq, rebuilt + + +def block_selector(sel_bits, zeta1_hi, idxc_hi, n: Const): + # A bus block's eq over its n = mu - κ selector bits, the offset's bits above κ, + # each boolean-pinned, and g^offset rebuilt from those bits alone (weights + # g^(2^(κ+k)) = idxc_hi[k] + 1), which the caller pins to the certified offset. + eq = GEN ** 0 + rebuilt = GEN ** 0 + for k in unroll(0, n): + sbit = sel_bits[GEN ** k] + sel_bits[GEN ** k] = sbit * sbit # booleanity as a write-once pin + if k == 0: + eq = sbit + zeta1_hi[GEN ** k] # eq over GF(2) is 1 + b + z + rebuilt = 1 + sbit * idxc_hi[GEN ** k] + else: + eq *= sbit + zeta1_hi[GEN ** k] + rebuilt *= 1 + sbit * idxc_hi[GEN ** k] + return eq, rebuilt + + +def padding_weight(rows, zeta, tau: Const): + # pad_n(zeta[..tau]) for n = rows <= 2^tau (doc lem:suffix). The advice is the bits + # of n + (2^tau - 1): the complements of n's low bits, as the step reads them, then + # n's bit tau, set only alone. + word = rows + const(2 ** tau - 1) + bits = StackBuf(N_COLUMN_LOGS) + hint_decompose_bits(bits, word, tau + 1) + pad = 1 + for k in unroll(0, tau): + c = bits[k] + bits[k] = c * c + z = zeta[GEN ** k] + pad = c * z + (c + z) * pad + top = bits[tau] + bits[tau] = top * top + # tau is the least height holding n iff lead == 1 and rest != 0: n's highest + # bit is tau - 1 with more below it, or n = 2^tau. + if tau == 0: + low = 0 + lead = 1 + rest = 1 + else: + # The bits below bit tau - 1, by Horner from their high end: one constant, 2. + below = 0 + for k in unroll(1, tau): + below = below * 2 + bits[tau - 1 - k] + low = below + bits[tau - 1] * (2 ** (tau - 1)) + lead = top + 1 + bits[tau - 1] + rest = top + below + const(2 ** (tau - 1) - 1) + assert low + top * (2 ** tau) == word + assert top * (low + const(2 ** tau - 1)) == 0 + return (1 + top) * pad, lead, rest + + +def verify_tables(fs0, fs1, fs2, fs3, fs4, cursor, pi_0, pi_1, zeta, g_bus_mu, dims_g, table_rows, g_squares, fp_w, beta, claim_pool, chi, claim_push, claim_pull, claim_count): + # Settle the bus against the six tables, in four steps: certify each side's + # leaf-cube tiling, decompose the three GKR leaf values over it, run the ONE + # table sumcheck they all reduce to, and bind the public input. Pooled claims + # land in claim_pool and the reduced point in `chi`; the batch's + # round count g^n, the deferred bytecode share and the PI point come back. + # + # Bus-leaf packing offsets, for the selector certification. Each side's blocks + # tile its leaf cube, block b at offset_b, and offsets accumulate as + # g^offset = Π_{earlier} g^(2^κ). The hinted order is certified to be the + # native one, as certify_placement does for the columns: descending κ, then + # ascending block index, the guest's per-side block order being the native + # verifier's (framework blocks, then each table's in table order). The + # decompose below pins each block's selector bits against this offset, forcing + # κ-alignment. Pull's blocks mirror push's and share zeta, so only push and + # count need offsets (pull's slots go unread). + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + rate = StackBuf(8) + gkr_claims = StackBuf(N_GKR_SIDES) + gkr_claims[PUSH_SIDE] = claim_push + gkr_claims[PULL_SIDE] = claim_pull + gkr_claims[COUNT_SIDE] = claim_count + kb = StackBuf(N_TABLES + 3) # kappa_base, in the frame + kb[0] = 1 + for t in unroll(0, N_TABLES + 2): + kb[1 + t] = dims_g[GEN ** t] + final_pc = g_squares[kb[KBC_SRC]] * INV_GEN + # A block group, a run of a side's blocks sharing one kappa source, is consecutive + # in its side's order, so a hinted order of the groups, certified strictly + # descending in its rank key (descending κ, then ascending first index: a side's + # blocks share one base, so the native ascending index), places every block. + group_side_tab = g_squares * GEN ** SIDE_TAB_AT # group -> its side (exponent_tables) + rank_keys = g_squares * GEN ** RANK_KEYS_AT + group_key = HeapBuf(N_BLOCK_GROUPS) + group_size = HeapBuf(N_BLOCK_GROUPS) + group_base = HeapBuf(N_BLOCK_GROUPS) + group_order = HeapBuf(N_BLOCK_GROUPS) + hint_witness(group_order[0:N_BLOCK_GROUPS], "block_group_order") + block_off = StackBuf(N_BLOCKS) # g^offset per push and count block + for cert in unroll(0, 2): + s = COUNT_SIDE * cert # PUSH_SIDE (0), then COUNT_SIDE (2) + for gi in unroll(SIDE_GROUP_START[s], SIDE_GROUP_START[s + 1]): + b0 = BLOCK_GROUP_START[gi] + kappa_g = kb[BLOCK_KAPPA_SRC[b0]] * GEN ** BLOCK_KAPPA_ADJ[b0] + group_key[GEN ** gi] = rank_keys[kappa_g] * INV_GEN ** b0 + group_size[GEN ** gi] = g_squares[kappa_g] ** (BLOCK_GROUP_START[gi + 1] - b0) + g_off = GEN ** 0 + for r in unroll(SIDE_GROUP_START[s], SIDE_GROUP_START[s + 1]): + gi = group_order[GEN ** r] + assert log(gi) < N_BLOCK_GROUPS # a valid group... + group_side_tab[gi] = s # ...of THIS side + key = group_key[gi] + if r != SIDE_GROUP_START[s]: + assert log(prev_key / key * INV_GEN) < RANK_KEY_BOUND + # write-once: a repeat collides (g_off has grown since), so the side's + # ranks name as many distinct groups of the side as it has, omitting none + group_base[gi] = g_off + g_off *= group_size[gi] + prev_key = key + for gi in unroll(SIDE_GROUP_START[s], SIDE_GROUP_START[s + 1]): + b0 = BLOCK_GROUP_START[gi] + step = g_squares[kb[BLOCK_KAPPA_SRC[b0]] * GEN ** BLOCK_KAPPA_ADJ[b0]] + block_off[b0] = group_base[GEN ** gi] + for b in unroll(b0 + 1, BLOCK_GROUP_START[gi + 1]): + block_off[b] = block_off[b - 1] * step + + # ---- 3x leaf decomposition (claims pooled; bytecode Public DEFERRED) ---- + # Reconstruct Ṽ₀(ζ) per side and assert it equals the GKR leaf value. The + # committed-coordinate values ride the stream (observed, pooled); Index + # coordinates use the factored index MLE; and the program's whole share of a + # bytecode leaf is ONE evaluation of the stacked polynomial, since its slots are + # aligned with the tuple and the weights are eq(α⃗, ·), so the share IS that + # polynomial at (ζ_lo, α⃗) (doc sec:e2e-bc): one hinted value, exported as a + # deferred claim, with no per-coordinate values and no selector challenge. + # + # Pull's blocks mirror push's (same kappas, same offsets, generator-asserted + # pairing) and share zeta, so each pull block REUSES its push twin's eq_hi and + # Index-MLE value instead of recomputing them; its column values are mostly + # deduped pool reads (COORD_FRESH). The identity check against pull's own GKR + # claim still binds everything. + bc_share = hint_witness("bytecode_val") + # Each table's padding rows: their weight on a claim at zeta[..tau], dispatched on + # its hinted log height tau, which this certifies: the least holding its rows, or + # its floor. A table is at most the stack, so tau has an arm. + pad_weight = StackBuf(N_TABLES) + for t in unroll(0, N_TABLES): + tau_g = kb[2 + t] + assert log(tau_g) < N_COLUMN_LOGS + rows = table_rows[GEN ** t] + pad_weight[t], lead, rest = match(log(tau_g), range(0, N_COLUMN_LOGS), lambda tau: padding_weight(rows, zeta, tau)) + if FLOORS[t] == 0: + assert lead == 1 + assert rest != 0 + else: + assert log(tau_g / GEN ** FLOORS[t]) < N_COLUMN_LOGS + if tau_g != GEN ** FLOORS[t]: + assert lead == 1 + assert rest != 0 + idxc_tab = HeapBuf(SIZE_BITS) # INDEX_MLE_FACTORS[t] = 1 + g^(2^t) + for t in unroll(0, SIZE_BITS): + idxc_tab[GEN ** t] = INDEX_MLE_FACTORS[t] + zeta1 = HeapBuf(g_bus_mu) # 1 + zeta, so an eq factor 1 + b + z is one XOR + zeta1_done = match(log(g_bus_mu), range(0, SIZE_BITS), lambda n: plus_one_run(zeta1, zeta, n)) # a match needs a target + # A block sized by table t's tau (kappa source 2 + t, unadjusted) has + # block_kappa = kappa_base[2 + t] = dims_g[t + 1] (verify_sub), so all of one + # table's blocks walk the same selector run and read the same tables. + shared_len = StackBuf(N_TABLES) + shared_eq = HeapBuf(N_TABLES * SEL_TAB_NIBBLES * SEL_TAB_EQ_STRIDE) + shared_g = HeapBuf(N_TABLES * SEL_TAB_NIBBLES * 16) + for t in unroll(0, N_TABLES): + tau_g = kb[2 + t] + shared_len[t] = g_bus_mu / tau_g + assert log(shared_len[t]) < SIZE_BITS + shared_selector_tables(shared_eq * GEN ** (t * SEL_TAB_NIBBLES * SEL_TAB_EQ_STRIDE), shared_g * GEN ** (t * SEL_TAB_NIBBLES * 16), zeta * tau_g, zeta1 * tau_g, g_squares * tau_g, shared_len[t]) + bus_table_total = StackBuf(N_GKR_SIDES) # per side, what its tables' blocks owe + block_eq_hi = StackBuf(N_BLOCKS) # every block's eq_hi, reused below + block_index_mle = HeapBuf(N_BLOCKS) # per push block with an Index coord + beta_1 = beta + 1 + for s in unroll(0, N_GKR_SIDES): + acc = 0 + selector_sum = 0 + for b in unroll(SIDE_BLOCK_START[s], SIDE_BLOCK_START[s + 1]): + block_has_public = 0 + kappa_g = kb[BLOCK_KAPPA_SRC[b]] * GEN ** BLOCK_KAPPA_ADJ[b] + # Every certified log is under SIZE_BITS (log_mem below COUNT_BITS, a tau + # below N_COLUMN_LOGS), so only an adjusted one needs bounding. + if const(BLOCK_KAPPA_SRC[b] != 0): + if const(BLOCK_KAPPA_ADJ[b] != 0): + assert log(kappa_g) < SIZE_BITS + if s == PULL_SIDE: + eq_hi = block_eq_hi[b - SIDE_BLOCK_START[PULL_SIDE]] + else: + # eq_hi over the ζ coords above κ against the selector bits, whose + # run is mu_s − κ = g^mu_s / g^κ long. Selector bits = offset >> κ: + # advice-decompose the offset and read it shifted by κ. Rebuilding + # g^offset from those high bits alone (weights g^(2^(κ+k))) and + # asserting it equals block_off_g pins the bits AND the κ-alignment + # in one shot; the low κ bit cells are written but never read. + shared = 0 + if const(BLOCK_KAPPA_SRC[b] // 2 != 0): + if const(BLOCK_KAPPA_SRC[b] != KBC_SRC): + if const(BLOCK_KAPPA_ADJ[b] == 0): + shared = 1 + if const(shared == 1): + t = BLOCK_KAPPA_SRC[b] - 2 + digits = HeapBuf(SEL_TAB_NIBBLES) + hint_decompose_digits_exponent(digits, block_off[b], SIZE_BITS, SEL_TAB_NIBBLES, 4, kb[2 + t]) + len_g = shared_len[t] + tab_eq = shared_eq * GEN ** (t * SEL_TAB_NIBBLES * SEL_TAB_EQ_STRIDE) + tab_g = shared_g * GEN ** (t * SEL_TAB_NIBBLES * 16) + eq_hi, goff = match(log(len_g), range(0, SIZE_BITS), lambda n: block_selector_tab(digits, tab_eq, tab_g, n)) + else: + sel_len_g = g_bus_mu / kappa_g # g^(mu - κ) + assert log(sel_len_g) < SIZE_BITS + offset_bits = HeapBuf(GEN ** SIZE_BITS) + hint_decompose_bits_exponent(offset_bits, block_off[b], SIZE_BITS) + sel_bits = offset_bits * kappa_g + zeta1_hi = zeta1 * kappa_g + idxc_hi = idxc_tab * kappa_g + eq_hi, goff = match(log(sel_len_g), range(0, SIZE_BITS), lambda n: block_selector(sel_bits, zeta1_hi, idxc_hi, n)) + assert goff == block_off[b] # bits == offset >> κ, κ-aligned + selector_sum += eq_hi + block_eq_hi[b] = eq_hi + # A TABLE's block streams no value here: the table sumcheck settles its + # fingerprint from that table's column evaluations. Only the framework + # blocks (boundary, memory, bytecode) still decompose. + if BLOCK_TABLE[b] == NO_TABLE: + # inner fingerprint Σ_i w_i · coord_i(ζ_lo); the count side weighs + # slot 0 alone (α⃗ = 0), γ = 0. + inner_sum = 0 + for i in unroll(0, BLOCK_COORD_COUNT[b]): + ci = BLOCK_COORD_OFF[b] + i # a compile-time index, so `ci` costs nothing + if COORD_TYPE[ci] == COORD_KIND_CONST: + if const(ci == FINAL_PC_COORD): + coord_val = final_pc + else: + coord_val = COORD_CONST[ci] + if COORD_TYPE[ci] == COORD_KIND_COL: + if COORD_FRESH[ci] == 1: + if COORD_FRESH_POS[ci] % 4 == 0: + if COORD_FRESH_POS[ci] != 0: + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + coord_val, cursor = fs_next(rate, COORD_FRESH_POS[ci] % 4, cursor) + claim_pool[GEN ** COORD_CLAIM_SLOT[ci]] = coord_val + else: + coord_val = claim_pool[GEN ** COORD_CLAIM_SLOT[ci]] + if COORD_TYPE[ci] == COORD_KIND_GCOL: + if COORD_FRESH[ci] == 1: + if COORD_FRESH_POS[ci] % 4 == 0: + if COORD_FRESH_POS[ci] != 0: + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + rawv, cursor = fs_next(rate, COORD_FRESH_POS[ci] % 4, cursor) + claim_pool[GEN ** COORD_CLAIM_SLOT[ci]] = rawv + else: + rawv = claim_pool[GEN ** COORD_CLAIM_SLOT[ci]] + coord_val = COORD_CONST[ci] * rawv + if COORD_TYPE[ci] == COORD_KIND_INDEX: + if s == PULL_SIDE: + coord_val = block_index_mle[GEN ** (b - SIDE_BLOCK_START[PULL_SIDE])] + else: + coord_val = match(log(kappa_g), range(0, SIZE_BITS), lambda n: index_mle(zeta, n)) + if s == PUSH_SIDE: + block_index_mle[GEN ** b] = coord_val + if COORD_TYPE[ci] == COORD_KIND_PUBLIC: + # The public slots carry no value of their own here: their + # alpha-weighted sum IS bc_share, added once per block below + # (push and pull share zeta, so both get the same one). + coord_val = 0 + block_has_public = 1 + if s == COUNT_SIDE: + inner_sum += coord_val + else: + inner_sum += fp_w[GEN ** i] * coord_val + inner_sum += block_has_public * bc_share # the bytecode blocks' public slots + if s == COUNT_SIDE: + acc += eq_hi * inner_sum + else: + acc += eq_hi * (beta + inner_sum) + # The table sumcheck sums a table block's form phi over every row, padding + # rows included, whose leaves are 1 instead: the difference, (1 - phi(p_j)) + # times the table's pad_weight, is public and joins the padding mass. A pull + # block has its push twin's eq_hi and padding tuple, so pull owes what push does. + if s == PULL_SIDE: + acc += push_padding + else: + padding_sum = 0 + for t in unroll(0, N_TABLES): + table_sum = 0 + for b in unroll(SIDE_BLOCK_START[s], SIDE_BLOCK_START[s + 1]): + if BLOCK_TABLE[b] == t: + if s == COUNT_SIDE: + coeff = 1 + else: + coeff = beta_1 + for i in unroll(0, BLOCK_COORD_COUNT[b]): + ci = BLOCK_COORD_OFF[b] + i + if const(COORD_PADDING[ci] != 0): + if s == COUNT_SIDE: + coeff += COORD_PADDING[ci] + else: + coeff += fp_w[GEN ** i] * COORD_PADDING[ci] + table_sum += block_eq_hi[b] * coeff + padding_sum += pad_weight[t] * table_sum + acc += padding_sum + if s == PUSH_SIDE: + push_padding = padding_sum + acc += 1 + selector_sum + # What the tables' blocks owe this side: its GKR leaf value less the + # framework decomposition. DERIVED, not read: a transmitted total would be a + # free value in its own check. The table sumcheck's target pins it below. + bus_table_total[s] = acc + gkr_claims[s] + claim_idx = N_BUS_CLAIMS # AIR/PI/pin claims pool after the deduped bus claims + + # ---- ONE table sumcheck for all six tables ---- + # Mirrors leanvm_core::constraints::verify. zc_xi ONCE, each table folding its own + # identities with a DISJOINT range of its powers (ETA_OFFSET[t]); one shared + # point zeta (the bus GKR's); n = max_t tau_t rounds. Rounds bind the HIGHEST + # variable first, so a 2^tau table sits out the first n - tau and joins carrying + # the challenges it sat out, weighing cprod[n - tau] * peq[tau] where peq[tau] = + # eq(zeta[..tau], chi[..tau]); the zc_xi-powers are inside constraint_eval. + # + # g^n is hinted, then pinned exactly: the range-checked division slacks force it + # to dominate every certified tau and the product identity forces it to BE one. + g_zc_n = hint_witness("zc_tau_max") + zc_is_a_tau = 1 + for t in unroll(0, N_TABLES): + tau_g = kb[2 + t] + assert log(g_zc_n / tau_g) < COUNT_BITS + zc_is_a_tau *= g_zc_n + tau_g + assert zc_is_a_tau == 0 + # n <= mu, the `Error::Truncated` of constraints.rs. Every table pushes at + # kappa = tau, so it holds structurally, but zc_peq below reads zeta[..n] and + # zeta only holds mu coords: unwritten heap there is prover-chosen. + assert log(g_bus_mu / g_zc_n) < COUNT_BITS + sp, zc_xi = squeeze(sp, rate, (N_FRESH + 3) % 4 + 1) + zc_xi_pows = StackBuf(N_ETA_POWS) + zc_xi_pows[0] = 1 + for k in unroll(1, N_ETA_POWS): + zc_xi_pows[k] = zc_xi_pows[k - 1] * zc_xi + # The eq point is the bus GKR's zeta, NOT a fresh one, which is what lets the + # batch settle the bus forms alongside the constraints. It is also why no target + # is read: what the three sides' tables owe, each in its own shared power of + # zc_xi, IS the sum the batch must reach, and zc_xi is squeezed after those + # totals are fixed, so hitting one number forces all three side equations. + bus_target = 0 + for sd in unroll(0, N_GKR_SIDES): + bus_target += zc_xi_pows[ETA_FORM_BASE + sd] * bus_table_total[sd] + # n vanilla sumcheck rounds: the round polynomial arrives whole, so a round is + # `h(0) + h(1) == claim` and a fold, with no eq to reapply. The tables still + # waiting ride inside h, so nothing here is indexed by height; the heights enter + # only the per-table weights below. + zc_rounds = HeapBuf((g_zc_n * GEN) ** ROUND_SLOTS) + zc_cprod = HeapBuf(g_zc_n * GEN) # the challenges bound so far, multiplied + fs_store(zc_rounds, ROUND_FS, 1, sp) + zc_rounds[GEN ** ROUND_CURSOR] = cursor + zc_rounds[GEN ** ROUND_CLAIM] = bus_target + zc_cprod[GEN ** 0] = 1 + for xk in mul_range(1, g_zc_n): + rd = zc_rounds * xk ** ROUND_SLOTS + ncur, nclaim, rk = sumcheck_round4(rd, rd[GEN ** ROUND_CURSOR], rd[GEN ** ROUND_CLAIM]) + chi[g_zc_n * INV_GEN / xk] = rk # g^(n-1-j): the variable round j binds + nxt = rd * GEN ** ROUND_SLOTS + nxt[GEN ** ROUND_CURSOR] = ncur + nxt[GEN ** ROUND_CLAIM] = nclaim + # cprod is the weight of a table that joins here; peq below is the rest + zc_cprod[xk * GEN] = zc_cprod[xk] * rk + zc_last = zc_rounds * g_zc_n ** ROUND_SLOTS + sp = fs_load(zc_last, ROUND_FS, 1) + rate = StackBuf(8) + cursor = zc_last[GEN ** ROUND_CURSOR] + claim = zc_last[GEN ** ROUND_CLAIM] + # peq[g^tau] = eq(zeta[..tau], chi[..tau]), as a prefix chain. + zc_peq = HeapBuf(g_zc_n * GEN) + eq_prefix_chain(zc_peq, 1, chi, zeta1, g_zc_n) + # Per table: every committed column's evaluation (pooled), its AIR constraint + # at its own reduced point chi[..tau_t], weighted into the batch's final claim. + air_acc = 0 + # The column evaluations stay where the stream holds them, N_COL_EVALS + # consecutive words that fs_next bound: the pool's slots from N_BUS_CLAIMS on are + # read there rather than copied. + col_eval_words = cursor + for t in unroll(0, N_TABLES): + tau_g = kb[2 + t] + col_evals = StackBuf(TABLE_COLS_CAP) + for pos in unroll(TABLE_COL_BASE[t], TABLE_COL_BASE[t] + N_TABLE_COLS[t]): + if pos % 4 == 0: + if pos != 0: + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + e, cursor = fs_next(rate, pos % 4, cursor) + col_evals[pos - TABLE_COL_BASE[t]] = e + claim_idx += 1 + # The table's AIR constraint at the final point (col_evals is indexed by + # local column index; the formulas mirror tables.rs Table::constraint). Every + # value relation now rides the bus as a degree-2 coordinate, so only JUMP's + # is-nonzero indicator is left with an identity of its own. + constraint_eval = 0 + if t == TABLE_JUMP: + # `b = c*w` and `c*(b+1) = 0`. The condition is K-valued, its memory read + # carrying literal zeros above the low limb, so both identities are + # single-lane (tables.rs JumpTable::constraint). Local columns: v_cond at 5, w at + # 12, the indicator b at 13. + c = col_evals[5] + b = col_evals[13] + constraint_eval = zc_xi_pows[ETA_OFFSET[t] + 0] * (b + c * col_evals[12]) + constraint_eval += zc_xi_pows[ETA_OFFSET[t] + 1] * (c * (b + 1)) + # The table's three bus forms, evaluated at the SAME column evaluations: + # Σ_b eq_hi(b) · (γ + Σ_i α^i · coord_i), the coords read off col_evals at + # their local index. This is what replaces opening those columns at ζ. + for sd in unroll(0, N_GKR_SIDES): + form = 0 + for b in unroll(0, N_BLOCKS): + if BLOCK_SIDE[b] == sd: + if BLOCK_TABLE[b] == t: + if const(sd == PULL_SIDE): + # A pull block shares its push twin's eq_hi, so its form + # is push's plus the coordinates where the two differ. + differs = 0 + for i in unroll(0, BLOCK_COORD_COUNT[b]): + ci = BLOCK_COORD_OFF[b] + i + if const(COORD_DIFF_COUNT[ci] != 0): + cv = 0 + for j in unroll(0, COORD_DIFF_COUNT[ci]): + tj = COORD_DIFF_OFF[ci] + j + if TERM_TYPE[tj] == COORD_KIND_CONST: + cv += TERM_CONST[tj] + if TERM_TYPE[tj] == COORD_KIND_COL: + cv += col_evals[TERM_COL_A[tj]] + if TERM_TYPE[tj] == COORD_KIND_GCOL: + cv += TERM_CONST[tj] * col_evals[TERM_COL_A[tj]] + if TERM_TYPE[tj] == COORD_KIND_PROD: + cv += TERM_CONST[tj] * (col_evals[TERM_COL_A[tj]] * col_evals[TERM_COL_B[tj]]) + dv = fp_w[GEN ** i] * cv + if const(differs == 0): + delta = dv + else: + delta += dv + differs = 1 + if const(differs == 1): + form += block_eq_hi[b] * delta + else: + inner = 0 + for i in unroll(0, BLOCK_COORD_COUNT[b]): + # Each coord is the sum of its terms, over this table's + # column evaluations. A product term (an address, an + # arithmetic result) is degree 2, which the batch's + # round polynomial already allows. + ci = BLOCK_COORD_OFF[b] + i # a compile-time index, so `ci` costs nothing + cv = 0 + for j in unroll(0, COORD_TERM_COUNT[ci]): + tj = COORD_TERM_OFF[ci] + j + if TERM_TYPE[tj] == COORD_KIND_CONST: + cv += TERM_CONST[tj] + if TERM_TYPE[tj] == COORD_KIND_COL: + cv += col_evals[TERM_COL_A[tj]] + if TERM_TYPE[tj] == COORD_KIND_GCOL: + cv += TERM_CONST[tj] * col_evals[TERM_COL_A[tj]] + if TERM_TYPE[tj] == COORD_KIND_PROD: + cv += TERM_CONST[tj] * (col_evals[TERM_COL_A[tj]] * col_evals[TERM_COL_B[tj]]) + if sd == COUNT_SIDE: + inner += cv + else: + inner += fp_w[GEN ** i] * cv + if sd == COUNT_SIDE: + form += block_eq_hi[b] * inner + else: + form += block_eq_hi[b] * (beta + inner) + if const(sd == PUSH_SIDE): + push_form = form + if const(sd == PULL_SIDE): + form = push_form + form + constraint_eval += zc_xi_pows[ETA_FORM_BASE + sd] * form + air_acc += zc_cprod[g_zc_n / tau_g] * zc_peq[tau_g] * constraint_eval # cprod[n - tau] * peq[tau] + assert air_acc == claim + + # ---- public-input binding claim: one per MEM limb, read off no stream ---- + # The VM's bind_pi_claim claims each committed MEM limb at [rm, 0..] equals + # that limb's line through the public words, lo_0 + rm*(lo_0 + lo_1) and so + # on, which the statement alone determines. canonical_limbs binds each word's + # limbs and proves its top limb zero, so the top claim is zero (doc sec:e2e-pi). + sp, rm = squeeze(sp, rate, (N_COL_EVALS + 3) % 4 + 1) + lo_0, hi_0 = canonical_limbs(pi_0) + lo_1, hi_1 = canonical_limbs(pi_1) + claim_pool[GEN ** claim_idx] = lo_0 + rm * (lo_0 + lo_1) + claim_idx += 1 + claim_pool[GEN ** claim_idx] = hi_0 + rm * (hi_0 + hi_1) + claim_idx += 1 + claim_pool[GEN ** claim_idx] = 0 + claim_idx += 1 + return sp[8], sp[9], sp[10], sp[11], sp[12], cursor, g_zc_n, bc_share, rm, col_eval_words + + +@inline +def carry_step(c0, c1, n0, n1, e_t, u_t, v_t, t: Const): + # Extend every t-bit prefix p of r by bit t of r, to p and p + 2^t: c0[p], c1[p] + # weigh the carry 0 and 1 into bit t, and e_t, u_t, v_t are bit t's weights for + # (x, y) equal, (1, 0) and (0, 1). The zero prefix never carries. + for p in unroll(0, 2 ** t): + if p == 0: + n0[0] = c0[0] * e_t + n0[2 ** t] = c0[0] * u_t + n1[2 ** t] = c0[0] * v_t + else: + n0[p] = c0[p] * e_t + c1[p] * u_t + n1[p] = c1[p] * v_t + n0[p + 2 ** t] = c0[p] * u_t + n1[p + 2 ** t] = c0[p] * v_t + c1[p] * e_t + return + + +@inline +def carry_wrap(c0, c1, out, e_t, s_t, t: Const): + # carry_step's last bit, the carry out dropped: u_t + v_t = s_t weighs it either way. + for p in unroll(0, 2 ** t): + if p == 0: + out[0] = c0[0] * e_t + out[2 ** t] = c0[0] * s_t + else: + out[p] = c0[p] * e_t + c1[p] * s_t + out[p + 2 ** t] = c0[p] * s_t + c1[p] * e_t + return + + +@inline +def keccak_shifts_eq(a, b, shifts): + # shifts[r] for every used r: the multilinear extension of "x = y + r mod 64" at + # (a, b), a ripple of the carry over the six bits. Split r = rl + 8 rh: the low + # three bits leave a carry c into the high three, which then add rh + c, so + # shifts[r] = L0[rl] H[rh] + L1[rl] H[rh + 1] over tables of eight. + w_eq = StackBuf(KECCAK_Z_BITS) + w_sum = StackBuf(KECCAK_Z_BITS) + w_up = StackBuf(KECCAK_Z_BITS) + w_down = StackBuf(KECCAK_Z_BITS) + # Bits 0 and 3 start the two tables: their weights are the first level itself. + lo0 = StackBuf(2) + lo1 = StackBuf(2) + hi0 = StackBuf(2) + hi1 = StackBuf(2) + # As products, (1 + a)(1 + b) + ab, a(1 + b) and (1 + a)b, trading XORs for MULs; + # b's 1 + b_t is the local sumcheck's, already in the frame. + for t in unroll(0, KECCAK_Z_BITS): + a_t = a[GEN ** t] + b_t = b[GEN ** t] + a_t1 = 1 + a_t + b_t1 = 1 + b_t + if t == 0: + lo0[0] = a_t1 * b_t1 + a_t * b_t + lo0[1] = a_t * b_t1 + lo1[1] = a_t1 * b_t + elif t == 3: + hi0[0] = a_t1 * b_t1 + a_t * b_t + hi0[1] = a_t * b_t1 + hi1[1] = a_t1 * b_t + elif const(t + 1 == KECCAK_Z_BITS): + w_eq[t] = a_t1 * b_t1 + a_t * b_t + w_sum[t] = a_t + b_t + else: + w_eq[t] = a_t1 * b_t1 + a_t * b_t + w_up[t] = a_t * b_t1 + w_down[t] = a_t1 * b_t + mid0 = StackBuf(4) + mid1 = StackBuf(4) + carry_step(lo0, lo1, mid0, mid1, w_eq[1], w_up[1], w_down[1], 1) + low0 = StackBuf(8) + low1 = StackBuf(8) + carry_step(mid0, mid1, low0, low1, w_eq[2], w_up[2], w_down[2], 2) + hmid0 = StackBuf(4) + hmid1 = StackBuf(4) + carry_step(hi0, hi1, hmid0, hmid1, w_eq[4], w_up[4], w_down[4], 1) + high = StackBuf(8) + carry_wrap(hmid0, hmid1, high, w_eq[5], w_sum[5], 2) + for r in unroll(0, 64): + if KECCAK_SHIFT_USED[r] == 1: + if r % 8 == 0: + shifts[r] = low0[0] * high[r // 8] + else: + shifts[r] = low0[r % 8] * high[r // 8] + low1[r % 8] * high[(r // 8 + 1) % 8] + return + + +@inline +def fold_node(src, dst, a_t, a_t1, p: Const, b: Const, t: Const): + # x0 + a (x0 + x1) as (1 + a) x0 + a x1: a MUL for an XOR, the scarcer row. + n = 2 ** (KECCAK_Z_BITS - t) + for k in unroll(0, n // 2): + x0 = src[p * n + (2 * k + (n - 1) * b) % n] + x1 = src[p * n + (2 * k + 1 + (n - 1) * b) % n] + dst[(p + 2 ** t * b) * (n // 2) + k] = a_t1 * x0 + a_t * x1 + return + + +@inline +def fold_shifts(src, dst, a_t, t: Const): + # One level of sum_w eq(a, w) V[w - r] for every used r at once, LSB first: node + # p of depth t is the vector for the residue p of r mod 2^t, 2^(6 - t) cells at + # p 2^(6 - t). Folding bit t splits it by bit t of r, b: V[w - r] pairs the + # parent's cells 2k - b and 2k + 1 - b. + a_t1 = 1 + a_t + for p in unroll(0, 2 ** t): + for b in unroll(0, 2): + if const(t + 1 == KECCAK_Z_BITS): + if KECCAK_SHIFT_USED[p + 2 ** t * b] == 1: + fold_node(src, dst, a_t, a_t1, p, b, t) + else: + fold_node(src, dst, a_t, a_t1, p, b, t) + return + + +@inline +def keccak_form(lanes, shifts, f_lanes, f_off: Const): + # for theta, rho, pi's transpose L^T, on weights that factor as lane + # times slice: E = lanes (x) eZ and f = f_lanes (x) fZ, the slices entering only + # through shifts[r] = sum_z fZ[z] eZ[(z + r) % 64]. f_lanes are frame cells from + # f_off. + total = 0 + column = StackBuf(5) + column_next = StackBuf(5) + f_column = StackBuf(5) + for x in unroll(0, 5): + acc = 0 + acc_next = 0 + f_acc = 0 + for y in unroll(0, 5): + lane_weight = lanes[GEN ** KECCAK_PI[x + 5 * y]] + moved = lane_weight * shifts[KECCAK_RHO[x + 5 * y]] + f_weight = f_lanes[f_off + x + 5 * y] + total += f_weight * moved + acc += moved + acc_next += lane_weight * shifts[(KECCAK_RHO[x + 5 * y] + 1) % 64] + f_acc += f_weight + column[x] = acc + column_next[x] = acc_next + f_column[x] = f_acc + for x in unroll(0, 5): + total += f_column[x] * (column[(x + 1) % 5] + column_next[(x + 4) % 5]) + return total + + +@inline +def slice_weight(slices, top, top1, z: Const): + # eq(local, z) from the five-coordinate tree `slices` and the top coordinate. + if const(z // 32 == 1): + w = slices[2 ** (KECCAK_Z_BITS - 1) - 2 + z % 32] * top + else: + w = slices[2 ** (KECCAK_Z_BITS - 1) - 2 + z % 32] * top1 + return w + + +def keccak_layer(fs0, fs1, fs2, fs3, fs4, p0, p1, cursor, y_g, step_g, rem_g, claim, out, iota, first: Const): + # One layer (verifier::keccak::verify_layer): the claim on the states after + # a round becomes one on the states before it. A claim's position weights are + # never written out: they are the transpose of the linear layer applied to a + # lane table times the eq table of six slice coordinates, which is all the + # record keeps, and what `keccak_form` evaluates them from. + # The first layer starts with the committed outputs' value waiting in the sponge, its + # two rate cells p0 and p1. + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + rate = StackBuf(8) + if first == 1: + rate[0] = p0 + rate[1] = p1 + # Each product coefficient's reader, a straight line of sums over the instance weights. + readers = StackBuf(KECCAK_PROJ_CELLS) + for i in unroll(0, KECCAK_PACK): + readers[i] = claim[GEN ** (KC_MU + i)] + for t in unroll(0, len(KECCAK_PROJ_A)): + readers[KECCAK_PACK + t] = readers[KECCAK_PROJ_A[t]] + readers[KECCAK_PROJ_B[t]] + # The product polynomial carries the claim. + product = StackBuf(KECCAK_PROD_BITS) + projected = 0 + for pos in unroll(first, first + KECCAK_PROD_BITS): + # fs_next, the word read straight into its cell, k = pos - first. + product[pos - first] = cursor[GEN ** (pos - first)] + coefficient = product[pos - first] + if pos % 4 == 0: + if pos != 0: + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + fs_pack(rate, pos % 4, coefficient, DS_OBSERVE) + projected += readers[KECCAK_PROJ_OUT[pos - first]] * coefficient + cursor = cursor * GEN ** KECCAK_PROD_BITS + assert projected + iota * claim[GEN ** KC_KAPPA] == claim[GEN ** KC_VALUE] + # At a random point a pack is one field element. + sp, s = squeeze(sp, rate, (first + KECCAK_PROD_BITS + 3) % 4 + 1) + # mu[i] = emb_i(s), each embedding a product of small irreducible factors. + s_powers = StackBuf(KECCAK_FAC_DEG) + s_powers[0] = 1 + s_powers[1] = s + for b in unroll(2, KECCAK_FAC_DEG): + s_powers[b] = s_powers[b - 1] * s + factors = StackBuf(KECCAK_EMB_CELLS) + for f in unroll(0, KECCAK_FAC_N): + acc = 0 + for t in unroll(KECCAK_FAC_START[f], KECCAK_FAC_START[f + 1]): + acc += s_powers[KECCAK_FAC_IDX[t]] + factors[f] = acc + for t in unroll(0, len(KECCAK_EMB_A)): + factors[KECCAK_FAC_N + t] = factors[KECCAK_EMB_A[t]] * factors[KECCAK_EMB_B[t]] + kappa = 0 # the sum of the weights + for i in unroll(0, KECCAK_PACK): + out[GEN ** (KC_MU + i)] = factors[KECCAK_EMB_OUT[i]] + kappa += factors[KECCAK_EMB_OUT[i]] + out[GEN ** KC_KAPPA] = kappa + value = product[KECCAK_PROD_BITS - 1] + for k in unroll(1, KECCAK_PROD_BITS): + value = value * s + product[KECCAK_PROD_BITS - 1 - k] + # The pack and the segment, a runtime number of rounds: KR_STEP step_g, KR_STEP a + # loop step, then rem_g, one a loop step (a loop rather than an unrolled arm per remainder, + # whose shared frame would grow every layer's). + prev_point = claim[GEN ** KC_POINT] + point = HeapBuf(y_g) + rounds = HeapBuf(KR_SLOTS * KR_CAP) + fs_store(rounds, KR_FS, KR_CAP, sp) + rounds[GEN ** (KR_CURSOR * KR_CAP)] = cursor + rounds[GEN ** (KR_CLAIM * KR_CAP)] = value + rounds[GEN ** (KR_SCALE * KR_CAP)] = 1 + for xs in mul_range(1, step_g): + rd = rounds * xs + pp = prev_point * xs ** KR_STEP + pt = point * xs ** KR_STEP + round_sp = fs_load(rd, KR_FS, KR_CAP) + cur_i = rd[GEN ** (KR_CURSOR * KR_CAP)] + run = rd[GEN ** (KR_CLAIM * KR_CAP)] + scale = rd[GEN ** (KR_SCALE * KR_CAP)] + for q in unroll(0, KR_STEP): + # q has degree two with the eq factor pulled out, and the prover sends its + # coefficients c1 and c2 (g_1, g_2), c0 being fixed by the claim. With r the + # round's coordinate and c its challenge, q(c) = claim + (r + c) c1 + + # (r + c^2) c2, and the next claim is (1 + r + c) q(c). + r_eq = pp[GEN ** q] + rate = StackBuf(8) + g_1, cur_i = fs_next(rate, 0, cur_i) + g_2, cur_i = fs_next(rate, 1, cur_i) + round_sp, challenge = squeeze(round_sp, rate, 2) + pt[GEN ** q] = challenge + gap = r_eq + challenge + eq_factor = 1 + gap + run = eq_factor * (run + gap * g_1 + (r_eq + challenge * challenge) * g_2) + scale = scale * eq_factor + nxt = rd * GEN + fs_store(nxt, KR_FS, KR_CAP, round_sp) + nxt[GEN ** (KR_CURSOR * KR_CAP)] = cur_i + nxt[GEN ** (KR_CLAIM * KR_CAP)] = run + nxt[GEN ** (KR_SCALE * KR_CAP)] = scale + rem_rounds = rounds * step_g + rem_prev = prev_point * step_g ** KR_STEP + rem_point = point * step_g ** KR_STEP + for x1 in mul_range(1, rem_g): + rd = rem_rounds * x1 + r_eq = rem_prev[x1] + round_sp = fs_load(rd, KR_FS, KR_CAP) + cur_i = rd[GEN ** (KR_CURSOR * KR_CAP)] + rate = StackBuf(8) + g_1, cur_i = fs_next(rate, 0, cur_i) + g_2, cur_i = fs_next(rate, 1, cur_i) + round_sp, challenge = squeeze(round_sp, rate, 2) + rem_point[x1] = challenge + gap = r_eq + challenge + eq_factor = 1 + gap + nxt = rd * GEN + fs_store(nxt, KR_FS, KR_CAP, round_sp) + nxt[GEN ** (KR_CURSOR * KR_CAP)] = cur_i + nxt[GEN ** (KR_CLAIM * KR_CAP)] = eq_factor * (rd[GEN ** (KR_CLAIM * KR_CAP)] + gap * g_1 + (r_eq + challenge * challenge) * g_2) + nxt[GEN ** (KR_SCALE * KR_CAP)] = rd[GEN ** (KR_SCALE * KR_CAP)] * eq_factor + last = rem_rounds * rem_g + sp = fs_load(last, KR_FS, KR_CAP) + cursor = last[GEN ** (KR_CURSOR * KR_CAP)] + value = last[GEN ** (KR_CLAIM * KR_CAP)] + scale = last[GEN ** (KR_SCALE * KR_CAP)] + # The bit position: degree three, the weights being one more multilinear factor. + local = HeapBuf(KECCAK_LOCAL_BITS) + for i in unroll(0, KECCAK_LOCAL_BITS): + rate = StackBuf(8) + c0, cursor = fs_next(rate, 0, cursor) + c2, cursor = fs_next(rate, 1, cursor) + c3, cursor = fs_next(rate, 2, cursor) + sp, challenge = squeeze(sp, rate, 3) + local[GEN ** i] = challenge + # c1 = value + c2 + c3, so h(c) = c0 + c (value + (1 + c) (c2 + (1 + c) c3)). + challenge1 = 1 + challenge + value = c0 + challenge * (value + challenge1 * (c2 + challenge1 * c3)) + rate = StackBuf(8) + t0, cursor = fs_next(rate, 0, cursor) + t1, cursor = fs_next(rate, 1, cursor) + t2, cursor = fs_next(rate, 2, cursor) + # The claim's weights at the local point. + f_lanes = StackBuf(2 ** (KECCAK_LANE_BITS + 1) - 2) + eqtree_frame(local * GEN ** KECCAK_Z_BITS, f_lanes, KECCAK_LANE_BITS, KECCAK_LANES) + prev_lanes = claim[GEN ** KC_LANES] + weight_at_local = StackBuf(1) + if first == 1: + # The first claim weighs a lane table times the bits of a word, x^z. + lanes_eq = 0 + for lane in unroll(0, KECCAK_LANES): + lanes_eq += f_lanes[LANE_OFF + lane] * prev_lanes[GEN ** lane] + for t in unroll(0, KECCAK_Z_BITS): + lanes_eq *= 1 + local[GEN ** t] * (1 + GEN ** (2 ** t)) + weight_at_local[0] = lanes_eq + else: + shifts = StackBuf(64) + keccak_shifts_eq(claim[GEN ** KC_LOCAL], local, shifts) + weight_at_local[0] = keccak_form(prev_lanes, shifts, f_lanes, LANE_OFF) + assert scale * weight_at_local[0] * (kappa * t0 + (kappa + t1) * t2) == value + # t[k] = sum_j eq[j] T[j + k]: one claim on the state before chi. + sp, beta = squeeze(sp, rate, 3) + out[GEN ** KC_VALUE] = t0 + beta * (t1 + beta * t2) + out[GEN ** KC_POINT] = point + out[GEN ** KC_LOCAL] = local + lanes = HeapBuf(KECCAK_LANES) + for y in unroll(0, 5): + for x in unroll(0, 5): + lanes[GEN ** (x + 5 * y)] = f_lanes[LANE_OFF + x + 5 * y] + beta * (f_lanes[LANE_OFF + (x + 4) % 5 + 5 * y] + beta * f_lanes[LANE_OFF + (x + 3) % 5 + 5 * y]) + out[GEN ** KC_LANES] = lanes + # The next layer's iota term: for every i, the new claim's weight on slice 2^i - 1 + # of lane 0, through the linear layer's transpose, from its lane and slice weights. + # The slice weights are an eq tree on the low five coordinates, times the top + # one's factor where a weight is read. + slices = StackBuf(2 ** KECCAK_Z_BITS - 2) + eqtree_frame(local, slices, KECCAK_Z_BITS - 1, 2 ** (KECCAK_Z_BITS - 1)) + top = local[GEN ** (KECCAK_Z_BITS - 1)] + top1 = 1 + top + flips = HeapBuf(KECCAK_IOTA_BITS) + for i in unroll(0, KECCAK_IOTA_BITS): + flipped = lanes[GEN ** KECCAK_PI[0]] * slice_weight(slices, top, top1, (2 ** i - 1 + KECCAK_RHO[0]) % 64) + for y in unroll(0, 5): + flipped += lanes[GEN ** KECCAK_PI[1 + 5 * y]] * slice_weight(slices, top, top1, (2 ** i - 1 + KECCAK_RHO[1 + 5 * y]) % 64) + flipped += lanes[GEN ** KECCAK_PI[4 + 5 * y]] * slice_weight(slices, top, top1, (2 ** i + KECCAK_RHO[4 + 5 * y]) % 64) + flips[GEN ** i] = flipped + out[GEN ** KC_FLIPS] = flips + return sp[8], sp[9], sp[10], sp[11], sp[12], cursor + + +def keccak_plain_rounds(fs0, fs1, fs2, fs3, fs4, p0, p1, cursor, value, point, n: Const, k0: Const): + # n rounds of a degree-2 sumcheck with no equality factor. With k0 = 1 one scalar + # already waits in the sponge, rate cells p0 and p1, and the first round's words + # join it. + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + for i in unroll(0, n): + rate = StackBuf(8) + if i == 0: + if k0 == 1: + rate[0] = p0 + rate[1] = p1 + c0, cursor = fs_next(rate, 1, cursor) + c2, cursor = fs_next(rate, 2, cursor) + sp, challenge = squeeze(sp, rate, 3) + else: + c0, cursor = fs_next(rate, 0, cursor) + c2, cursor = fs_next(rate, 1, cursor) + sp, challenge = squeeze(sp, rate, 2) + else: + c0, cursor = fs_next(rate, 0, cursor) + c2, cursor = fs_next(rate, 1, cursor) + sp, challenge = squeeze(sp, rate, 2) + c1 = value + c2 + point[GEN ** i] = challenge + value = c0 + challenge * (c1 + challenge * c2) + return sp[8], sp[9], sp[10], sp[11], sp[12], cursor, value + + +@inline +def keccak_slot_lanes(slot, segment_point, out, first: Const, count: Const): + # The weight an opening's slot point puts on each lane, into the frame cells + # out[0..25): the states it speaks of start at `first`, one per segment, for the + # first `count` segments. + slot_weights = StackBuf(2 ** (QKECCAK_SLOT_LOG + 1) - 2) + eqtree_frame(slot, slot_weights, QKECCAK_SLOT_LOG, KECCAK_LANES * KECCAK_SEGMENTS) + seg_weights = StackBuf(2 ** (KECCAK_SEG_LOG + 1) - 2) + eqtree_frame(segment_point, seg_weights, KECCAK_SEG_LOG, 2 ** KECCAK_SEG_LOG) + for lane in unroll(0, KECCAK_LANES): + lane_acc = 0 + for m in unroll(0, count): + lane_acc += seg_weights[2 ** KECCAK_SEG_LOG - 2 + m] * slot_weights[2 ** QKECCAK_SLOT_LOG - 2 + KECCAK_LANES * (first + m) + lane] + out[lane] = lane_acc + return + + +def verify_keccak(fs0, fs1, fs2, fs3, fs4, cursor, tau_g, chi, output_lanes, z_vals, slices): + # KECCAK validity (verifier::keccak::verify_keccak): a random claim on every segment's output, + # KECCAK_DEPTH layers down to the committed inputs, every segment at once. The + # last segment's output is the permutation's: the table's claims on its output + # lanes, `output_lanes`, at the table's point chi[..tau], give that part of the + # claim, and the prover sends the committed outputs' part. It leaves two + # openings of q_keccak, at the two points written to `z_vals` (tau + + # QKECCAK_SLOT_LOG coordinates each, QKECCAK_VARS_CAP apart): the committed + # outputs as words at the first, returned, and the 64 `slices` at the second. + # tau >= FLOORS[TABLE_KECCAK] is certified, so the pack count is a real count. It + # also draws the ring-switching map's first challenge, which the last slices wait + # for, and returns it. + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + no_rate = StackBuf(8) + pack_g = tau_g * INV_GEN ** KECCAK_PACK_LOG + y_g = pack_g * GEN ** KECCAK_SEG_LOG + # A layer runs y = log(y_g) rounds KR_STEP at a time: y_g = step_g^KR_STEP rem_g, + # from y's bits, pinned boolean and tied back (y = tau - 3 < 2^5 as tau < COUNT_BITS). + y_bits = StackBuf(5) + hint_decompose_bits_exponent(y_bits, y_g, 5) + for j in unroll(0, 5): + bit = y_bits[j] + y_bits[j] = bit * bit + step_g = GEN ** 0 + for j in unroll(KR_STEP_LOG, 5): + step_g *= 1 + y_bits[j] * (1 + GEN ** (2 ** (j - KR_STEP_LOG))) + rem_g = GEN ** 0 + for j in unroll(0, KR_STEP_LOG): + rem_g *= 1 + y_bits[j] * (1 + GEN ** (2 ** j)) + assert step_g ** KR_STEP * rem_g == y_g + out_point = z_vals + in_point = z_vals * GEN ** QKECCAK_VARS_CAP + for i in unroll(0, KECCAK_PACK_LOG): + out_point[GEN ** i] = chi[GEN ** i] + # The claim's point: chi's pack coordinates, then a fresh segment. + point = HeapBuf(y_g) + chi_pack = chi * GEN ** KECCAK_PACK_LOG + for xt in mul_range(1, pack_g): + point[xt] = chi_pack[xt] + segment = point * pack_g + for i in unroll(0, KECCAK_SEG_LOG): + sp, r = squeeze(sp, no_rate, 0) + segment[GEN ** i] = r + r_lane = HeapBuf(KECCAK_LANE_BITS) + for i in unroll(0, KECCAK_LANE_BITS): + sp, r = squeeze(sp, no_rate, 0) + r_lane[GEN ** i] = r + # The committed outputs' value waits in the sponge until the first layer's words. + value_rate = StackBuf(8) + value, cursor = fs_next(value_rate, 0, cursor) + fs0 = sp[8] + fs1 = sp[9] + fs2 = sp[10] + fs3 = sp[11] + fs4 = sp[12] + + # The first claim combines the output lanes as words: instance weights + # eq(chi_low), position weights eq(r_lane) on the lane times x^z on the bit. + claims = HeapBuf((KECCAK_DEPTH + 1) * KC_SIZE) + claims[GEN ** KC_KAPPA] = 1 + claims[GEN ** KC_POINT] = point + mu_tree = HeapBuf(2 ** (KECCAK_PACK_LOG + 1) - 2) + eqtree(out_point, mu_tree, KECCAK_PACK_LOG) + for i in unroll(0, KECCAK_PACK): + claims[GEN ** (KC_MU + i)] = mu_tree[GEN ** (2 ** KECCAK_PACK_LOG - 2 + i)] + lane_tree = HeapBuf(2 ** (KECCAK_LANE_BITS + 1) - 2) + eqtree(r_lane, lane_tree, KECCAK_LANE_BITS) + first_lanes = lane_tree * GEN ** (2 ** KECCAK_LANE_BITS - 2) + claims[GEN ** KC_LANES] = first_lanes + # `value` is the committed outputs' part; the last segment's is the table's. + segment_tree = HeapBuf(2 ** (KECCAK_SEG_LOG + 1) - 2) + eqtree(segment, segment_tree, KECCAK_SEG_LOG) + output = 0 + for lane in unroll(0, KECCAK_LANES): + output += first_lanes[GEN ** lane] * output_lanes[GEN ** lane] + claims[GEN ** KC_VALUE] = value + segment_tree[GEN ** (2 ** KECCAK_SEG_LOG - 2 + KECCAK_SEGMENTS - 1)] * output + + for step in unroll(0, KECCAK_DEPTH): + claim = claims * GEN ** (step * KC_SIZE) + # iota flips known positions of lane 0, per segment a different round's. + claim_point = claim[GEN ** KC_POINT] + segment_tree = StackBuf(2 ** (KECCAK_SEG_LOG + 1) - 2) + eqtree_frame(claim_point * pack_g, segment_tree, KECCAK_SEG_LOG, 2 ** KECCAK_SEG_LOG) + # The claim's weights where iota can flip, slice 2^i - 1 of lane 0, for the + # i this step's round constants set: the first claim's are a word's, + # x^(2^i - 1) on lane 0; a later claim's come from its lane and slice + # weights through the linear layer's transpose. + flips = StackBuf(KECCAK_IOTA_BITS) + for i in unroll(0, KECCAK_IOTA_BITS): + used = 0 + for t in unroll(KECCAK_IOTA_START[KECCAK_SEGMENTS * step], KECCAK_IOTA_START[KECCAK_SEGMENTS * step + KECCAK_SEGMENTS]): + if const(KECCAK_IOTA_IDX[t] == i): + used = 1 + if const(used == 1): + if step == 0: + flips[i] = first_lanes[GEN ** 0] * GEN ** (2 ** i - 1) + else: + claim_flips = claim[GEN ** KC_FLIPS] + flips[i] = claim_flips[GEN ** i] + iota = 0 + for m in unroll(0, KECCAK_SEGMENTS): + flipped = 0 + for t in unroll(KECCAK_IOTA_START[KECCAK_SEGMENTS * step + m], KECCAK_IOTA_START[KECCAK_SEGMENTS * step + m + 1]): + flipped += flips[KECCAK_IOTA_IDX[t]] + iota += segment_tree[2 ** KECCAK_SEG_LOG - 2 + m] * flipped + if step == 0: + fs0, fs1, fs2, fs3, fs4, cursor = keccak_layer(fs0, fs1, fs2, fs3, fs4, value_rate[0], value_rate[1], cursor, y_g, step_g, rem_g, claim, claim * GEN ** KC_SIZE, iota, 1) + else: + fs0, fs1, fs2, fs3, fs4, cursor = keccak_layer(fs0, fs1, fs2, fs3, fs4, 0, 0, cursor, y_g, step_g, rem_g, claim, claim * GEN ** KC_SIZE, iota, 0) + + # ---- the outputs, whose instance weights are an eq already ---- + # Their position weights are a word's, so the 64 slices are only needed as + # the word they add up to: q_keccak itself at the point. + out_slot = out_point * tau_g + fs0, fs1, fs2, fs3, fs4, cursor, out_value = keccak_plain_rounds(fs0, fs1, fs2, fs3, fs4, 0, 0, cursor, value, out_slot, QKECCAK_SLOT_LOG, 0) + word_rate = StackBuf(8) + word, cursor = fs_next(word_rate, 0, cursor) + out_lanes = StackBuf(KECCAK_LANES) + keccak_slot_lanes(out_slot, point * pack_g, out_lanes, 1, KECCAK_SEGMENTS - 1) + lanes_sum = 0 + for lane in unroll(0, KECCAK_LANES): + lanes_sum += out_lanes[lane] * first_lanes[GEN ** lane] + assert lanes_sum * word == out_value + out_pack = out_point * GEN ** KECCAK_PACK_LOG + for xt in mul_range(1, pack_g): + out_pack[xt] = point[xt] + + # ---- the inputs: the instances of a pack first, then the slot ---- + last = claims * GEN ** (KECCAK_DEPTH * KC_SIZE) + fs0, fs1, fs2, fs3, fs4, cursor, low_value = keccak_plain_rounds(fs0, fs1, fs2, fs3, fs4, word_rate[0], word_rate[1], cursor, last[GEN ** KC_VALUE], in_point, KECCAK_PACK_LOG, 1) + low_tree = HeapBuf(2 ** (KECCAK_PACK_LOG + 1) - 2) + eqtree(in_point, low_tree, KECCAK_PACK_LOG) + mu_at_low = 0 + for i in unroll(0, KECCAK_PACK): + mu_at_low += low_tree[GEN ** (2 ** KECCAK_PACK_LOG - 2 + i)] * last[GEN ** (KC_MU + i)] + in_slot = in_point * tau_g + fs0, fs1, fs2, fs3, fs4, cursor, in_value = keccak_plain_rounds(fs0, fs1, fs2, fs3, fs4, 0, 0, cursor, low_value, in_slot, QKECCAK_SLOT_LOG, 0) + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + rate = StackBuf(8) + for z in unroll(0, BASE_FIELD_BITS): + if z % 4 == 0: + if z != 0: + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + w, cursor = fs_next(rate, z % 4, cursor) + slices[GEN ** z] = w + sp, map_0 = squeeze(sp, rate, 4) + last_point = last[GEN ** KC_POINT] + in_lanes = StackBuf(KECCAK_LANES) + keccak_slot_lanes(in_slot, last_point * pack_g, in_lanes, 0, KECCAK_SEGMENTS) + # shifts[r] = sum_z slices[z] eq(a, z + r), a the last layer's slice point: + # folded a bit at a time for every r at once. + last_local = last[GEN ** KC_LOCAL] + level = StackBuf(64) + for z in unroll(0, BASE_FIELD_BITS): + level[z] = slices[GEN ** z] + for t in unroll(0, KECCAK_Z_BITS): + folded = StackBuf(64) + fold_shifts(level, folded, last_local[GEN ** t], t) + level = folded + shifts = level + last_lanes = last[GEN ** KC_LANES] + assert mu_at_low * keccak_form(last_lanes, shifts, in_lanes, 0) == in_value + in_pack = in_point * GEN ** KECCAK_PACK_LOG + for xt in mul_range(1, pack_g): + in_pack[xt] = last_point[xt] + return sp[8], sp[9], sp[10], sp[11], sp[12], cursor, word, map_0 + + +@inline +def ring_map(lin_eval, map_challenges): + for stage in unroll(0, len(RING_MAP_SHIFTS)): + frobenius = lin_eval + for k in unroll(0, RING_MAP_SHIFTS[stage]): + frobenius *= frobenius + lin_eval += map_challenges[GEN ** stage] * frobenius + return lin_eval + + +def ring_switch_target(slices, map_challenges): + # Evaluate the claim and combine its 64 packing rows, sum_i x^i L(slice_i), + # RS_CHUNK rows an iteration in Horner form: the running x-power and the + # running sum ride one two-slot chain. + rs_chain = HeapBuf((BASE_FIELD_BITS // RS_CHUNK + 1) * PAIR_SLOTS) + rs_chain[GEN ** 0] = GEN ** 0 # x^(RS_CHUNK j) + rs_chain[GEN ** 1] = 0 # the running sum + for x_round in mul_range(1, GEN ** (BASE_FIELD_BITS // RS_CHUNK)): + rows = slices * x_round ** RS_CHUNK + chunk = ring_map(rows[GEN ** (RS_CHUNK - 1)], map_challenges) + for r in unroll(1, RS_CHUNK): + chunk = chunk * 2 + ring_map(rows[GEN ** (RS_CHUNK - 1 - r)], map_challenges) + row = rs_chain * x_round ** PAIR_SLOTS + x_pow = row[GEN ** 0] + row[GEN ** PAIR_SLOTS] = x_pow * const(2 ** RS_CHUNK) + row[GEN ** (PAIR_SLOTS + 1)] = row[GEN ** 1] + x_pow * chunk + rs_end = rs_chain * (GEN ** (BASE_FIELD_BITS // RS_CHUNK)) ** PAIR_SLOTS + return rs_end[GEN ** 1] + + +def ring_switch_weight(z_vals, point1, c_table, qkeccakv_g): + # One ring-switched claim's transparent weight at the opening point. + prod_chains = HeapBuf((qkeccakv_g * GEN) ** BASE_FIELD_BITS) + for k in unroll(0, BASE_FIELD_BITS): + prod_chains[GEN ** k] = 1 + prod_final = rs_eq_run(prod_chains, z_vals, point1, qkeccakv_g) + rs_weight = 0 + for k in unroll(0, BASE_FIELD_BITS): + rs_weight += c_table[GEN ** k] * prod_final[GEN ** k] + return rs_weight + + +def certify_placement(kappa_base, g_squares): + # Certify the native order (descending kappa, then ascending column index) group + # by group: a column group, a run of columns sharing one kappa source, is + # consecutive in it, so a hinted order of the groups, certified strictly + # descending in its rank key, places every column. Returns g^(sum 2^kappa) and + # each column's g^offset. + kb = StackBuf(N_TABLES + 3) + for i in unroll(0, N_TABLES + 3): + kb[i] = kappa_base[GEN ** i] + rank_keys = g_squares * GEN ** RANK_KEYS_AT + group_key = HeapBuf(N_COL_GROUPS) + group_size = HeapBuf(N_COL_GROUPS) + for gi in unroll(0, N_COL_GROUPS): + c0 = COL_GROUP_START[gi] + kappa_g = kb[COL_KAPPA_SRC[c0]] * GEN ** COL_KAPPA_ADJ[c0] + # g_squares spans SIZE_BITS, and every kappa is under it: a certified log + # <= 30 (log_mem or a tau), the baked bytecode log, or q_keccak's tau_5 + QKECCAK_SLOT_LOG. + assert log(kappa_g) < SIZE_BITS + # Descending kappa, then the group's first (compact, native-order) index ascending. + group_key[GEN ** gi] = rank_keys[kappa_g] * INV_GEN ** c0 + group_size[GEN ** gi] = g_squares[kappa_g] ** (COL_GROUP_START[gi + 1] - c0) + group_order = HeapBuf(N_COL_GROUPS) + hint_witness(group_order[0:N_COL_GROUPS], "col_group_order") + group_base = HeapBuf(N_COL_GROUPS) + g_total = GEN ** 0 + for rank in unroll(0, N_COL_GROUPS): + gi = group_order[GEN ** rank] + assert log(gi) < N_COL_GROUPS + key = group_key[gi] + # A negative step wraps around the order of GEN and cannot pass this check. + if rank != 0: + assert log(prev_key / key * INV_GEN) < RANK_KEY_BOUND + group_base[gi] = g_total # write-once: a repeated group collides + g_total *= group_size[gi] + prev_key = key + col_off_g = HeapBuf(N_COMMITTED_COLS) + for gi in unroll(0, N_COL_GROUPS): + c0 = COL_GROUP_START[gi] + step = g_squares[kb[COL_KAPPA_SRC[c0]] * GEN ** COL_KAPPA_ADJ[c0]] + off = group_base[GEN ** gi] + for c in unroll(c0, COL_GROUP_START[gi + 1]): + col_off_g[GEN ** c] = off + if const(c + 1 != COL_GROUP_START[gi + 1]): + off *= step + return g_total, col_off_g + + +def column_selector(offset, sel_eq, sel_g, kappa: Const): + # The column's selector at the complete opening point, from hinted g-power digits + # of its offset's nibbles at and above kappa // 4, each range-checked below 16. + # Pinning their weighted product to the certified offset makes them its own + # nibbles (a base-16 representation is unique), and that offset is 2^kappa-aligned + # (certify_placement orders kappa descending), so the lowest digit, kappa's own + # nibble, is a multiple of 2^(4 - width): the strided level-width table holds it. + # Both offset and point are zero above m, so extending to 4 * SEL_NIBBLES adds + # only factors eq(0, 0) = 1. + digits = StackBuf(SEL_NIBBLES) + hint_decompose_digits_exponent(digits, offset, 4 * SEL_NIBBLES, SEL_NIBBLES - kappa // 4, 4, 4 * (kappa // 4)) + if const(kappa // 4 == SEL_NIBBLES): + selector = GEN ** 0 + rebuilt = GEN ** 0 + for j in unroll(kappa // 4, SEL_NIBBLES): + width = 4 + if const(j == kappa // 4): + width = 4 * j + 4 - kappa + d = digits[j - kappa // 4] + assert log(d) < 16 + if const(j == kappa // 4): + selector = sel_eq[GEN ** (SEL_EQ_STRIDE * j + 16 * (width - 1)) * d] + rebuilt = sel_g[GEN ** (16 * j) * d] + else: + selector *= sel_eq[GEN ** (SEL_EQ_STRIDE * j + 16 * (width - 1)) * d] + rebuilt *= sel_g[GEN ** (16 * j) * d] + assert rebuilt == offset + return selector + + +def selector_tables(point, point1): + # Per nibble j of the selector bits, the eq tables over its top 1..4 coordinates, + # the level-w one at 16 (w - 1), strided: entry u, a multiple of 2^(4 - w), holds + # the eq of bit t of u against coordinate 4j + t for t >= 4 - w. + sel_eq = HeapBuf(SEL_NIBBLES * SEL_EQ_STRIDE) + for j in unroll(0, SEL_NIBBLES): + levels = StackBuf(64) + levels[0] = point1[GEN ** (4 * j + 3)] + levels[8] = point[GEN ** (4 * j + 3)] + for w in unroll(1, 4): + r = point[GEN ** (4 * j + 3 - w)] + r1 = point1[GEN ** (4 * j + 3 - w)] + for i in unroll(0, 2 ** w): + e = i * 2 ** (4 - w) + levels[16 * w + e] = levels[16 * (w - 1) + e] * r1 + levels[16 * w + e + 2 ** (3 - w)] = levels[16 * (w - 1) + e] * r + for w in unroll(0, 4): + for i in unroll(0, 2 ** (w + 1)): + e = i * 2 ** (3 - w) + sel_eq[GEN ** (SEL_EQ_STRIDE * j + 16 * w + e)] = levels[16 * w + e] + return sel_eq + + +def check_opening_terminal(g_squares, zeta, chi, rm, g_bus_mu, g_zc_n, g_log_mem, tau_keccak_g, kappa_base, lam_cl, col_offsets, z_vals, c_table, point, point1, inner_total, yr_at_tail, sumcheck_target): + # Evaluate each transparent weight at the complete point in witness order. + # A claim is its low point followed by the certified column's selector bits; + # a q_keccak lane puts its slot's bits between the two. + # point1 = 1 + point holds each coordinate's eq factor against a zero bit. + zeta_eq_chain = HeapBuf(SIZE_BITS + 1) + eq_prefix_chain(zeta_eq_chain, 1, zeta, point1, g_bus_mu) + chi_eq_chain = HeapBuf(SIZE_BITS + 1) + eq_prefix_chain(chi_eq_chain, 1, chi, point1, g_zc_n) + slot_point = point * tau_keccak_g + slot_point1 = point1 * tau_keccak_g + pi_chain = HeapBuf(SIZE_BITS + 1) + pi_chain[GEN ** 0] = 1 + pi_chain[GEN ** 1] = rm + point1[GEN ** 0] + for xk in mul_range(GEN, g_log_mem): + pi_chain[xk * GEN] = pi_chain[xk] * point1[xk] + pi_eq = pi_chain[g_log_mem] + + # Every low dimension here is a certified log at a baked kappa source, as + # certify_placement derives the columns'. + kb = StackBuf(N_TABLES + 3) + for i in unroll(0, N_TABLES + 3): + kb[i] = kappa_base[GEN ** i] + sel_eq = selector_tables(point, point1) + sel_g = g_squares * GEN ** SIZE_BITS # exponent_tables' constant nibble shares + selectors = StackBuf(N_COMMITTED_COLS) + for c in unroll(0, N_COMMITTED_COLS): + offset = col_offsets[GEN ** c] + kappa_g = kb[COL_KAPPA_SRC[c]] * GEN ** COL_KAPPA_ADJ[c] + selectors[c] = match(log(kappa_g), range(0, N_COLUMN_LOGS), lambda kappa: column_selector(offset, sel_eq, sel_g, kappa)) + + inner_sum = inner_total + lam_pow = lam_cl # the pool's powers, as verify_sub batched it + for j in unroll(0, N_CLAIMS): + if CLAIM_POINT_BUF[j] != POINT_BUF_KECCAK_OUT: + col = CLAIM_COMMITTED_COL[j] + cplen_g = kb[CLAIM_KAPPA_SRC[j]] * GEN ** CLAIM_KAPPA_ADJ[j] + nlow_adj = CLAIM_KAPPA_ADJ[j] + if CLAIM_POINT_BUF[j] == POINT_BUF_PI: + low_eq = pi_eq + if CLAIM_POINT_BUF[j] == POINT_BUF_ZETA: + low_eq = zeta_eq_chain[cplen_g] + if CLAIM_POINT_BUF[j] == POINT_BUF_RHO: + low_eq = chi_eq_chain[cplen_g] + if CLAIM_POINT_BUF[j] == POINT_BUF_QKECCAK_RHO: + # Top bit first, so claims sharing high slot bits share partial products. + slot_eq = GEN ** 0 + for kk in unroll(0, QKECCAK_SLOT_LOG): + k = QKECCAK_SLOT_LOG - 1 - kk + if CLAIM_QKECCAK_SLOT_BITS[QKECCAK_SLOT_LOG * j + k] == 1: + slot_factor = slot_point[GEN ** k] + else: + slot_factor = slot_point1[GEN ** k] + if kk == 0: + slot_eq = slot_factor + else: + slot_eq *= slot_factor + low_eq = slot_eq * chi_eq_chain[cplen_g] + nlow_adj = CLAIM_KAPPA_ADJ[j] + QKECCAK_SLOT_LOG + # The claim's low dimension is its column's kappa: by construction when + # the two share a kappa source, else checked. + if const(CLAIM_KAPPA_SRC[j] != COL_KAPPA_SRC[col]): + assert kb[CLAIM_KAPPA_SRC[j]] * GEN ** nlow_adj == kb[COL_KAPPA_SRC[col]] * GEN ** COL_KAPPA_ADJ[col] + else: + if const(nlow_adj != COL_KAPPA_ADJ[col]): + assert kb[CLAIM_KAPPA_SRC[j]] * GEN ** nlow_adj == kb[COL_KAPPA_SRC[col]] * GEN ** COL_KAPPA_ADJ[col] + inner_sum += lam_pow * low_eq * selectors[col] + lam_pow *= lam_cl + lam_out = lam_pow + + # The Keccak reduction's two claims on q_keccak, each over the whole column: + # the ring-switched one leads the batch at lam_cl^0, the plain one closes it. + # A lane claim's low point is tau_keccak long, the kappa check above holding it + # to this column. + qkeccakv_g = tau_keccak_g * GEN ** QKECCAK_SLOT_LOG + if const(COL_KAPPA_SRC[QKECCAK_COMMITTED_COL] != TABLE_KECCAK + 2): + assert qkeccakv_g == kb[COL_KAPPA_SRC[QKECCAK_COMMITTED_COL]] * GEN ** COL_KAPPA_ADJ[QKECCAK_COMMITTED_COL] + else: + if const(COL_KAPPA_ADJ[QKECCAK_COMMITTED_COL] != QKECCAK_SLOT_LOG): + assert qkeccakv_g == kb[COL_KAPPA_SRC[QKECCAK_COMMITTED_COL]] * GEN ** COL_KAPPA_ADJ[QKECCAK_COMMITTED_COL] + out_eq_chain = HeapBuf(SIZE_BITS + QKECCAK_SLOT_LOG + 1) + eq_prefix_chain(out_eq_chain, 1, z_vals, point1, qkeccakv_g) + rs_weight = ring_switch_weight(z_vals * GEN ** QKECCAK_VARS_CAP, point1, c_table, qkeccakv_g) + inner_sum += (rs_weight + lam_out * out_eq_chain[qkeccakv_g]) * selectors[QKECCAK_COMMITTED_COL] + assert inner_sum * yr_at_tail == sumcheck_target + + +def verify_sub(pi_0, pi_1, seed_0, seed_1, g_kbc, g_squares): + # In-circuit verification of ONE inner proof for the statement (pi_0, pi_1) about + # the program with Fiat-Shamir seed (seed_0, seed_1) and g^kbc instructions, + # mirroring `crates/verifier` step for step; the `# ---- ... ----` headers below run + # in that order. All proof data is hinted HERE, so each call pops the next + # sub-proof's entry of every witness stream and the body lowers once. The + # exponent tables are shared read-only. The caller must bound g_kbc. Returns + # the bytecode claim left open: its point (zeta's first kbc coordinates, then + # the bus alphas) and its value. + # + # The pool holds every committed-coordinate claim's value in decompose order + # (the points are the GKR zetas, resolvable from the baked block structure) and + # its certified low dimension, which the terminal pins its lengths against. + claim_pool = HeapBuf(N_CLAIMS) + + # ---- seed (statement pre-bound: hinted sub pi + baked program digest) ---- + # The sponge's first permutation takes the program digest and the statement as + # its rate, from the zero state. + sp = StackBuf(KECCAK_STATE) + keccak_cells([seed_0, seed_1, pi_0, pi_1], sp, final=0) + stream = HeapBuf(STREAM_CAP) + hint_witness(stream[0:STREAM_CAP], "stream") + cursor = stream # the proof stream, replayed word by word (advance = * g) + + # ---- announced layout and PCS rate (observed, then certified) ---- + # The stream announces the memory's log size and each table's row count as + # integer WORDS; the shape-generic phases need logs as G-POWERS (loop bounds, + # match scrutinees). log_mem is reassembled from its advice-decomposed bits, with + # no hint and no g^j -> j lookup. A table's log height is advice, which + # verify_tables certifies to be the least holding its rows, above its floor. + table_rows = HeapBuf(N_TABLES) + rate = StackBuf(8) + for i in unroll(0, N_TABLES + 1): + if i % 4 == 0: + if i != 0: + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + x, cursor = fs_next(rate, i % 4, cursor) + if i == 0: + log_mem_word = x + else: + table_rows[GEN ** (i - 1)] = x + if const((N_TABLES + 1) % 4 == 0): + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + log_inv_rate, cursor = fs_next(rate, (N_TABLES + 1) % 4, cursor) + rate_sel = g_power_of_word(log_inv_rate) / GEN + assert log(rate_sel) < LIG_N_RATES + dims_g = HeapBuf(N_TABLES + 2) # [g^log_mem, g^tau_0 .. g^tau_{N_TABLES-1}, g^kbc] + g_log_mem = g_power_of_word(log_mem_word) + assert log(g_log_mem) < MAX_LOG_MEM + 1 # native log_mem <= MAX_LOG_MEM + assert log(g_log_mem / GEN ** MIN_LOG_MEM) < COUNT_BITS # native MIN_LOG_MEM <= log_mem + dims_g[GEN ** 0] = g_log_mem + hint_witness(dims_g[1:1 + N_TABLES], "table_logs") # certified in verify_tables + dims_g[GEN ** (N_TABLES + 1)] = g_kbc + # kappa_base maps a kappa source index to its certified log (source 0 + # = const via the baked adj). Each block's kappa then DERIVES from its + # structural source as a compile-time offset off a certified log: no hint, and + # nothing left free. + kappa_base = HeapBuf(N_TABLES + 3) + kb = StackBuf(N_TABLES + 3) # the same, in the frame + kb[0] = 1 + kb[1] = g_log_mem + for t in unroll(0, N_TABLES): + kb[2 + t] = dims_g[GEN ** (t + 1)] + kb[KBC_SRC] = g_kbc + for i in unroll(0, N_TABLES + 3): + kappa_base[GEN ** i] = kb[i] + # The ONE bus depth, COMPUTED (not hinted): mu = log2_ceil(Σ_b 2^κ_b) over + # PUSH's blocks; pull matches by pairing, the count tree is padded to it. + push_total = GEN ** 0 + for b in unroll(SIDE_BLOCK_START[PUSH_SIDE], SIDE_BLOCK_START[PUSH_SIDE + 1]): + push_total *= g_squares[kb[BLOCK_KAPPA_SRC[b]] * GEN ** BLOCK_KAPPA_ADJ[b]] # g^(sum of 2^kappa) + g_bus_mu = log2_ceil_in_the_exponent(push_total, g_squares, 0, SIZE_BITS) + zeta = HeapBuf(g_bus_mu) # the ONE shared GKR point: exactly mu coords + + # ---- commitment root (2 words), kept for the opening phase ---- + # The announcement's N_TABLES + 2 words, then the root's two, wait as one run. + if const((N_TABLES + 2) % 4 == 0): + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + commit_root_0, cursor = fs_next(rate, (N_TABLES + 2) % 4, cursor) + if const((N_TABLES + 3) % 4 == 0): + sp = fs_perm(sp, rate, 4) + rate = StackBuf(8) + commit_root_1, cursor = fs_next(rate, (N_TABLES + 3) % 4, cursor) + # A non-canonical half is rejected here (`Digest::from_halves` in `crates/verifier`); the level + # roots get the same treatment at their own read. + root_cells = StackBuf(2) + root_cells[0] = assert_canonical(commit_root_0) + root_cells[1] = assert_canonical(commit_root_1) + + # ---- bus challenges (F192 provides the soundness margin without grinding) ---- + # A tuple is fingerprinted multilinearly: slot x weighs eq(alphas, x), so a leaf + # factor has total degree N_TUPLE_BITS in the challenges and the aligned bytecode + # polynomial is read off at the challenge vector itself (doc sec:gp, sec:e2e-bc). + bus_alpha = HeapBuf(N_TUPLE_BITS) + for t in unroll(0, N_TUPLE_BITS): + if t == 0: + sp, av = squeeze(sp, rate, (N_TABLES + 3) % 4 + 1) + else: + sp, av = squeeze(sp, rate, 0) + bus_alpha[GEN ** t] = av + fp_w = HeapBuf(N_TUPLE_SLOTS) + for x in unroll(0, N_TUPLE_SLOTS): + fp_w[GEN ** x] = eq_weight(bus_alpha, N_TUPLE_BITS, x, 0) + sp, beta = squeeze(sp, rate, 0) + + # ---- ONE GKR grand product: push, pull, and count RLC-batched ---- + fs0, fs1, fs2, fs3, fs4, cursor, claim_push, claim_pull, claim_count = verify_bus_gkr(sp[8], sp[9], sp[10], sp[11], sp[12], cursor, g_bus_mu, zeta) + + # ---- the bus leaves, the table sumcheck, and the public-input claim ---- + chi = HeapBuf(SIZE_BITS) # chi[i] = the challenge that bound variable i + fs0, fs1, fs2, fs3, fs4, cursor, g_zc_n, bc_share, rm, col_eval_words = verify_tables(fs0, fs1, fs2, fs3, fs4, cursor, pi_0, pi_1, zeta, g_bus_mu, dims_g, table_rows, g_squares, fp_w, beta, claim_pool, chi, claim_push, claim_pull, claim_count) + + # ---- KECCAK validity, by GKR over the rounds ---- + tau_keccak_g = dims_g[GEN ** (TABLE_KECCAK + 1)] # the KECCAK table's certified tau + z_vals = HeapBuf(2 * QKECCAK_VARS_CAP) + slices = HeapBuf(BASE_FIELD_BITS) + fs0, fs1, fs2, fs3, fs4, cursor, keccak_word, map_0 = verify_keccak(fs0, fs1, fs2, fs3, fs4, cursor, tau_keccak_g, chi, col_eval_words * GEN ** (KECCAK_OUT_POOL - N_BUS_CLAIMS), z_vals, slices) + sp = fs_cap(fs0, fs1, fs2, fs3, fs4) + + # ---- stacked mixed opening: ring-switch front + claim combination ---- + # The input opening's slices were read and bound above; this block only binds + # them to the commitment. Compose six two-term F2-linear maps with shifts + # 32,16,8,4,2,1: their expansion has all 64 Frobenius terms soundness needs, + # while direct application costs 63 squarings and only six general + # multiplications. + map_challenges = HeapBuf(6) # len(RING_MAP_SHIFTS) + c_table = HeapBuf(BASE_FIELD_BITS) + # verify_keccak drew the first, which the last slices waited for. + map_challenges[GEN ** 0] = map_0 + for stage in unroll(1, len(RING_MAP_SHIFTS)): + sp, map_challenge = squeeze(sp, rate, 0) + map_challenges[GEN ** stage] = map_challenge + # Expand the same composition once for the later transparent-weight evaluation. + # Before shift d, the populated coefficients are exactly at multiples of 2d; the + # new branch fills the adjacent d-offset entries. + c_table[GEN ** 0] = 1 + for stage in unroll(0, len(RING_MAP_SHIFTS)): + shift = RING_MAP_SHIFTS[stage] + map_challenge = map_challenges[GEN ** stage] + for slot in unroll(0, BASE_FIELD_BITS // (2 * shift)): + coefficient = c_table[GEN ** (slot * 2 * shift)] + for k in unroll(0, shift): + coefficient *= coefficient + c_table[GEN ** (slot * 2 * shift + shift)] = map_challenge * coefficient + target_in = ring_switch_target(slices, map_challenges) + # ONE batching challenge for the whole pool: N_CLAIMS - 1 fewer Fiat-Shamir + # hashes than a challenge per claim, and none for the values themselves, + # `fs_next` having bound every one it read and the public-input ones deriving + # from the seeded statement and rm, so `lam_cl` already depends on all of + # them. Disjoint power ranges, as for the zc_xi-powers above: + # the ring-switch claim takes lam_cl^0, the pool lam_cl^1 onward (bar the output + # lanes' claims, which the Keccak reduction consumed), and the Keccak reduction's + # claim on its committed outputs the power after the pool's last. + sp, lam_cl = squeeze(sp, rate, 0) + target = target_in + lam_pow = lam_cl + for j in unroll(0, N_CLAIMS): + if CLAIM_POINT_BUF[j] != POINT_BUF_KECCAK_OUT: + if const(j // N_BUS_CLAIMS == 0): + target += lam_pow * claim_pool[GEN ** j] + else: + if const((j - N_BUS_CLAIMS) // N_COL_EVALS == 0): + target += lam_pow * col_eval_words[GEN ** (j - N_BUS_CLAIMS)] + else: + target += lam_pow * claim_pool[GEN ** j] + lam_pow *= lam_cl + target += lam_pow * keccak_word + + g_total, col_offsets = certify_placement(kappa_base, g_squares) + + # ---- certify g^m: m = max(log2_ceil(sum_cols 2^kappa), PCS_MIN_MU) ---- + # g_total is g^(sum 2^kappa) from the certified placement walk above. + gmv = log2_ceil_in_the_exponent(g_total, g_squares, PCS_MIN_MU, SIZE_BITS) # g^m + size_sel = gmv * LIG_MIN_SHIFT_INV # g^(m - MIN) + assert log(size_sel) < LIG_N_LOG_SIZES + # One arm per size, the rate passed on. Both are transcript-bound and range-checked + # above, so a single compiled guest dispatches independently for every inner proof + # in a mixed-rate batch. + sumcheck_target, point, point1, inner_total, yr_at_tail = match(log(size_sel), range(0, LIG_N_LOG_SIZES), lambda m_idx: open_stacked(m_idx, rate_sel, sp[8], sp[9], sp[10], sp[11], sp[12], target, commit_root_0, commit_root_1, cursor)) + # `stream` is a fixed-capacity witness transport. The shape fixes the exact + # consumed prefix, whose every word is transcript-bound; the unused suffix + # is outside the recursively verified proof and intentionally unconstrained. + + # ---- generalized eval_b terminal (runtime claim shapes) ---- + check_opening_terminal(g_squares, zeta, chi, rm, g_bus_mu, g_zc_n, g_log_mem, tau_keccak_g, kappa_base, lam_cl, col_offsets, z_vals, c_table, point, point1, inner_total, yr_at_tail, sumcheck_target) + + return zeta, bus_alpha, bc_share + + +# ========================= leanSTARK (0x11) dependency verification ======================== +# A claim is data_hash | vk, four cells: the program whose stacked bytecode table hashes to +# vk accepts data_hash as its public input. vk is upstream's own program digest, +# keccak256(TAG | table) with TAG the 9-byte "leanvm-v1" and the table 16 * 2^kgen words, +# little endian: the very Fiat-Shamir IV the dependency's proof was made under, so the +# guest seeds verify_sub with the claim's vk cells and nothing about the program is +# hinted unbound. The table is hinted as bytes, which this rehashes into vk and folds +# into the bytecode claim's value: settled here, never deferred. +# +# Why the bytes hashed are the words evaluated: each byte is a cell of the 256-entry +# table t8 read at a hinted g^b with log(g^b) < 256 checked, so it is the polynomial of +# an integer below 256, of degree below 8. A word is DEFINED as sum_b x^(8b) byte_b and +# a hash lane as its bytes' sum at the lane's own shifts, every product of degree below +# 64, so neither reduces modulo the field polynomial: a word's 64 coefficients are its +# bytes' bits, and so are the lanes', byte for byte. No split of a word is hinted. +# With the tag 9 bytes long, lane i + 1 (i >= 0) is word i - 1's top byte (the tag's +# last byte for i = 0) under word i's low 7 bytes, and the last lane word N - 1's top byte. + + +def gen_table(t8, zeta, alpha, k: Const): + # The table of a 2^k-instruction program, hinted a 16-word row (128 bytes in the + # exponent) a frame: keccak256(TAG | table), and its multilinear at the bytecode + # claim's point (zeta's first k coordinates, then the 4 alphas, LSB first). The + # first fold rides the row loop; the rest halve a heap run per coordinate. + n_cells = 8 * 2 ** k + 1 + pre = HeapBuf(GEN ** n_cells) + pt = StackBuf(MAX_GENERIC_BYTECODE_LOG + 4) + for j in unroll(0, k): + pt[j] = zeta[GEN ** j] + for j in unroll(0, 4): + pt[k + j] = alpha[GEN ** j] + r0 = pt[0] + v = HeapBuf(GEN ** (8 * 2 ** k)) + # Per row boundary: the last word's top byte and the last lane, carried to the next row. + carry = HeapBuf(GEN ** (2 * 2 ** k + 2)) + carry[GEN ** 0] = TAG_BYTE8 + carry[GEN ** 1] = TAG_LANE0 + for x in mul_range(1, GEN ** (2 ** k)): + rb = StackBuf(128) + hint_witness(rb, "gen_bytes") + cin = carry * x ** 2 + hi_prev = cin[GEN ** 0] + lanes = StackBuf(17) + lanes[0] = cin[GEN ** 1] + words = StackBuf(16) + for j in unroll(0, 16): + lo = 0 + for b in unroll(0, 7): + d = rb[8 * j + b] + assert log(d) < 256 + lo += (2 ** (8 * b)) * t8[d] + d7 = rb[8 * j + 7] + assert log(d7) < 256 + h = t8[d7] + words[j] = lo + (2 ** 56) * h + lanes[j + 1] = hi_prev + 256 * lo + hi_prev = h + c = pre * x ** 8 + f = v * x ** 8 + for q in unroll(0, 8): + c[GEN ** q] = lanes[2 * q] + Y_TOWER * lanes[2 * q + 1] + a = words[2 * q] + bw = words[2 * q + 1] + f[GEN ** q] = a + r0 * (a + bw) + cout = cin * GEN ** 2 + cout[GEN ** 0] = hi_prev + cout[GEN ** 1] = lanes[16] + last = carry * GEN ** (2 * 2 ** k) + pre[GEN ** (n_cells - 1)] = last[GEN ** 1] + Y_TOWER * last[GEN ** 0] + for rd in unroll(1, k + 4): + r = pt[rd] + half = 2 ** (k + 3 - rd) + nxt = HeapBuf(GEN ** half) + for i in mul_range(1, GEN ** half): + a = v[i * i] + b = v[i * i * GEN] + nxt[i] = a + r * (a + b) + v = nxt + # TAG | table, 17 cells (two KECCAKs) a frame, then the last 1..17 cells padded. + pairs = (n_cells - 1) // 17 + chain = HeapBuf(GEN ** ((pairs + 1) * KECCAK_STATE)) + zero_state(chain) + for xq in mul_range(1, GEN ** pairs): + st = chain * xq ** KECCAK_STATE + nxt_st = st * GEN ** KECCAK_STATE + blk = pre * xq ** 17 + keccak_cells(blk[0:17], nxt_st[0:KECCAK_STATE], state=st[0:KECCAK_STATE], final=0) + state = chain * GEN ** (pairs * KECCAK_STATE) + digest = StackBuf(2) + rest = n_cells - 17 * pairs + tail = pre * GEN ** (17 * pairs) + keccak_cells(tail[0:rest], digest, state=state[0:KECCAK_STATE], len=16 * rest - 7) + return digest[0], digest[1], v[1] + + +def verify_generic(slot, g_squares): + # One 0x11 claim, slot = data_hash | vk (four cells); vk seeds the sub-proof's transcript. + g_kgen = hint_witness("gen_kbc") + assert log(g_kgen) < MAX_GENERIC_BYTECODE_LOG + 1 + zeta, alpha, bc_share = verify_sub(slot[GEN ** 0], slot[GEN ** 1], slot[GEN ** 2], slot[GEN ** 3], g_kgen, g_squares) + t8 = HeapBuf(256) + for b in unroll(0, 256): + t8[GEN ** b] = b + d0, d1, value = match(log(g_kgen), range(0, GEN_KGEN_ARMS), lambda k: gen_table(t8, zeta, alpha, k)) + assert d0 == slot[GEN ** 2] + assert d1 == slot[GEN ** 3] + assert value == bc_share + return + + +# ================= NiceTry SPHINCS- v2 signature verification (prototype) ================ + + +@inline +def sp_bind_lane(bits, off: Const, value): + # One 64-bit lane's advice bits, each pinned boolean (a second write of its cell, + # one MUL), tied back to the lane and copied into the shared run at `off`. + lane_bits = StackBuf(BASE_FIELD_BITS) + hint_decompose_bits(lane_bits, value, BASE_FIELD_BITS) + acc = 0 + for i in unroll(0, BASE_FIELD_BITS): + b = lane_bits[i] + lane_bits[i] = b * b + bits[off + i] = b + acc += b * COORD_BASIS[i] + assert acc == value + return + + +@inline +def sp_field(bits, off: Const, n: Const, lsb_byte: Const): + # The integer digest bits [off, off+n) spell, as the big-endian ADRS field + # whose least significant byte is cell byte lsb_byte: value bit j lands on + # cell bit 8 (lsb_byte - j/8) + j mod 8. One multiply-add a bit. + acc = 0 + for j in unroll(0, n): + acc += bits[SP_BIT_INDEX[off + j]] * COORD_BASIS[8 * (lsb_byte - j // 8) + j % 8] + return acc + + +@inline +def sp_index(v: Const): + # A compile-time word3 (tree index or hash address) below 2^16, big-endian at + # cell bytes 14 and 15. + return const((v % 256) * SP_BYTE15 + (v // 256) * 2 ** 112) + + +def sp_walk(value, adrs_a, adrs_b, pp, k: Const): + # Chain steps k..w-2 from position k: value' = F(pp, ADRS(hash address s), value), + # keccak(pp | 0 | ADRS | value | 0), 96 bytes. + word = value + for s in unroll(k, SP_CHAIN_STEPS): + out = StackBuf(1) + keccak([pp, 0], [adrs_a, adrs_b + const(s * SP_BYTE15)], out, tail=[word, 0, 0, 0], len=96) + word = out[0] + return word, k + + +@inline +def sp_tl_word(pp, a0, a1, run, j: Const): + # Word j of a T_l input: pp | 0 | ADRS | value_0 | 0 | value_1 | 0 | ... + w = 0 + if const(j == 0): + w = pp + if const(j == 2): + w = a0 + if const(j == 3): + w = a1 + if const(j // 4 != 0): + if const(j % 2 == 0): + w = run[(j - 4) // 2] + return w + + +def sp_wots_pk(adrs_a, kp, pp, msg): + # One layer's WOTS+ verification: `msg`'s len1 base-16 digits and the len2 digits + # of their checksum, the chains walked from the revealed values, and the key they + # compress to. + tips = StackBuf(SP_L) + exponent = 1 + acc = 0 + for i in unroll(0, SP_LEN1): + digit = hint_witness("sp_digits") + assert log(digit) < SP_W + chain_start = hint_witness("sp_chain_starts") + adrs_b = kp + const(i * SP_BYTE11) + tips[i], e = match(log(digit), range(0, SP_W), lambda k: sp_walk(chain_start, adrs_a, adrs_b, pp, k)) + exponent = exponent * digit + acc += e * COORD_BASIS[8 * (i // 2) + 4 * (1 - i % 2)] + assert acc == msg + for j in unroll(0, SP_LEN2): + digit = hint_witness("sp_digits") + assert log(digit) < SP_W + chain_start = hint_witness("sp_chain_starts") + adrs_b = kp + const((SP_LEN1 + j) * SP_BYTE11) + tips[SP_LEN1 + j], e = match(log(digit), range(0, SP_W), lambda k: sp_walk(chain_start, adrs_a, adrs_b, pp, k)) + exponent = exponent * digit ** (SP_W ** (SP_LEN2 - 1 - j)) + assert exponent == GEN ** SP_MAX_CSUM + + # The WOTS key: keccak(pp | ADRS(WOTS_PK) | 35 tips), 1184 bytes. + key = StackBuf(1) + pk_b = kp + const(SP_WOTS_PK * SP_BYTE3) + keccak_cells([sp_tl_word(pp, adrs_a, pk_b, tips, j) for j in unroll(0, 4 + 2 * SP_L)], key) + return key[0] + + +def verify_sig_lean_sphincs(signer): + # `signer` is one 4-cell entry of the coverage table, an EIP-8288 dependency entry: + # the 32-byte data_hash (the message), then pkRoot, pkSeed. + pp = signer[GEN ** 3] + root = signer[GEN ** 2] + + # H_msg = keccak(0xFF..FF | R | pkSeed | pkRoot | M), 112 bytes in one block. + r = hint_witness("sp_rand") + digest = StackBuf(2) + keccak([SP_ONES, SP_ONES], [r, pp], digest, tail=[root, signer[1], signer[GEN], 0], len=112) + + # Every index below is a bit field of that digest, so its last 24 bytes are + # advice-decomposed here and bound lane by lane. + bits = StackBuf(SP_BIT_CELLS) + lo = StackBuf(1) + hint_f192_limbs(lo, digest[0]) + hi = (digest[0] + lo[0]) * Y_INV + assert_in_k(lo[0], hi) + tail = StackBuf(1) + hint_f192_limbs(tail, digest[1]) + tail_hi = (digest[1] + tail[0]) * Y_INV + assert_in_k(tail[0], tail_hi) + lanes = [hi, tail[0], tail_hi] + for lane in unroll(0, SP_BIT_LANES): + sp_bind_lane(bits, lane * BASE_FIELD_BITS, lanes[lane]) + + # ---- FORS: one opened leaf per tree ---- + fors_a = sp_field(bits, SP_HT_OFF + SP_HP, SP_H - SP_HP, 15) + fors_kp = sp_field(bits, SP_HT_OFF, SP_HP, 7) + fors_b = fors_kp + const(SP_FORS_TREE * SP_BYTE3) + roots = StackBuf(SP_K) + for i in unroll(0, SP_K): + secret = hint_witness("sp_fors_secrets") + placed = sp_field(bits, i * SP_A, SP_A, 15) + leaf = StackBuf(1) + keccak([pp, 0], [fors_a, fors_b + placed + sp_index(i * 2 ** SP_A)], leaf, tail=[secret, 0, 0, 0], len=96) + node = leaf[0] + b0 = bits[SP_BIT_INDEX[i * SP_A]] + b8 = bits[SP_BIT_INDEX[i * SP_A + SP_A - 1]] + shifted = (placed + b0 * COORD_BASIS[120] + b8 * COORD_BASIS[112]) / GEN + b8 * COORD_BASIS[127] + for z in unroll(1, SP_A + 1): + sibling = hint_witness("sp_fors_paths") + m = bits[SP_BIT_INDEX[i * SP_A + z - 1]] * (node + sibling) + adrs_b = fors_b + shifted + const(z * SP_BYTE11 + (i * 2 ** (SP_A - z)) % 256 * SP_BYTE15 + (i * 2 ** (SP_A - z)) // 256 * 2 ** 112) + parent = StackBuf(1) + keccak([pp, 0], [fors_a, adrs_b], parent, tail=[node + m, 0, sibling + m, 0]) + node = parent[0] + if const(z != SP_A): + shifted = (shifted + bits[SP_BIT_INDEX[i * SP_A + z]] * COORD_BASIS[120]) / GEN + roots[i] = node + # The FORS key: keccak(pp | ADRS(FORS_ROOTS) | 19 roots), 672 bytes. + fors_key = StackBuf(1) + roots_b = fors_kp + const(SP_FORS_ROOTS * SP_BYTE3) + keccak_cells([sp_tl_word(pp, fors_a, roots_b, roots, j) for j in unroll(0, 4 + 2 * SP_K)], fors_key) + signed = fors_key[0] + + # ---- the hypertree, bottom layer first ---- + for layer in unroll(0, SP_D): + leaf_off = SP_HT_OFF + layer * SP_HP + adrs_a = const(layer * SP_BYTE3) + sp_field(bits, leaf_off + SP_HP, SP_H - (layer + 1) * SP_HP, 15) + kp = sp_field(bits, leaf_off, SP_HP, 7) + node = sp_wots_pk(adrs_a, kp, pp, signed) + for z in unroll(1, SP_HP + 1): + sibling = hint_witness("sp_siblings") + m = bits[SP_BIT_INDEX[leaf_off + z - 1]] * (node + sibling) + adrs_b = const(SP_TREE * SP_BYTE3 + z * SP_BYTE11) + sp_field(bits, leaf_off + z, SP_HP - z, 15) + parent = StackBuf(1) + keccak([pp, 0], [adrs_a, adrs_b], parent, tail=[node + m, 0, sibling + m, 0]) + node = parent[0] + signed = node + assert signed == root + return + + +# ========================== statements and the claim lists ========================== + + +def deps_digest(gd_0, gd_1, sd_0, sd_1, n_gen_g): + # The EIP-8288 dependency digest of the two lists: the leanSPHINCS list's digest + # alone without leanSTARK claims (the published get_deps_hash), else + # keccak256(S | G | DEPS_DOMAIN), a 96-byte preimage no list of 64-byte entries has. + out = StackBuf(2) + if n_gen_g == 1: + out[0] = sd_0 + out[1] = sd_1 + else: + keccak([sd_0, sd_1], [gd_0, gd_1], out, tail=[DEPS_DOMAIN_0, DEPS_DOMAIN_1, 0, 0], len=96) + return out[0], out[1] + + +def statement_digest(seed_0, seed_1, dd_0, dd_1, da_0, da_1, defer): + # A node's statement, hashed to the two words the VM publishes, over the + # proving environment's Fiat-Shamir seed (the proof system's version and this + # bytecode), the dependency digest of the two claim lists, its DA digest (zeros + # without a root), and the DEFER_STMT_CELLS deferred-claim cells. A + # parent rebuilds a child's statement with this same call, over list digests it + # re-absorbed itself, which forces the child to be a proof of THIS bytecode over + # claims read out of the parent's own tables. + # + # The preimage is fixed-length, so a plain hash beats the Fiat-Shamir chain. + # A header value is a canonical cell and needs no check, the KECCAK table + # reading only cells whose top limb is zero. A deferred cell is a full field + # element, so two fill three cells (`pack_pair`). + cells = StackBuf(STMT_CELLS) + cells[0] = seed_0 # the STMT_HEADER header cells + cells[1] = seed_1 + cells[2] = dd_0 + cells[3] = dd_1 + cells[4] = da_0 + cells[5] = da_1 + for p in unroll(0, STMT_PAIRS): + s = defer[GEN ** (2 * p)] + if const(2 * p + 1 == DEFER_STMT_CELLS): + t = 0 # an odd cell count pairs the last one with a zero partner + else: + t = defer[GEN ** (2 * p + 1)] + pack_pair(cells, STMT_DEFER_OFF + 3 * p, s, t) + st = StackBuf(2) + keccak_cells(cells, st) + return st[0], st[1] + + +# A declared claim list is hashed CLAIM_GROUP claims a loop frame: a claim is 64 bytes, +# four cells, so a group is 68 cells, four block pairs, and the sponge state is carried +# between frames through a heap chain of KECCAK_STATE cells a step, from the zero state. +# Only the claims after the last whole group depend on the length: a `match` on their +# count. + + +@inline +def zero_state(chain): + for k in unroll(0, KECCAK_STATE): + chain[GEN ** k] = 0 + return + + +def list_split(n_g): + # A list of n claims as (g^bigs, g^groups, g^rest), n = CLAIM_BIG * bigs + + # CLAIM_GROUP * groups + rest with groups < BIG_GROUPS. The range checks and the + # product identity leave that split the only solution. + split = StackBuf(3) + hint_witness(split, "signers_split") + bigs = split[0] + groups = split[1] + rest = split[2] + assert log(rest) < CLAIM_GROUP + assert log(groups) < BIG_GROUPS + assert_gpow(bigs) + # Every exponent is small and nonnegative, so this is an identity over the + # integers: CLAIM_BIG * bigs + CLAIM_GROUP * groups + rest == n. + assert bigs ** CLAIM_BIG * groups ** CLAIM_GROUP * rest == n_g + return bigs, groups, rest + + +def claim_list_digest(entries_ptr, n_g): + # The hash of one scheme's declared claims: 64 bytes a claim, the claimed pubkey + # then the message it signed. An empty list hashes the empty string, and the + # padding binds the length. + digest = StackBuf(2) + if n_g == 1: + keccak([0, 0], [0, 0], digest, len=0) + else: + bigs, groups, rest = list_split(n_g) + chain = HeapBuf((bigs * groups * GEN) ** KECCAK_STATE) + zero_state(chain) + for xq in mul_range(1, bigs): + st = chain * xq ** KECCAK_STATE + nxt = st * GEN ** KECCAK_STATE + blk = entries_ptr * xq ** (4 * CLAIM_BIG) + for j in unroll(0, CLAIM_BIG): + hint_witness(blk[4 * j:4 * j + 4], "claims") + keccak_cells(blk[0:4 * CLAIM_BIG], nxt[0:KECCAK_STATE], state=st[0:KECCAK_STATE], final=0) + mid = chain * bigs ** KECCAK_STATE + mid_entries = entries_ptr * bigs ** (4 * CLAIM_BIG) + for xq in mul_range(1, groups): + st = mid * xq ** KECCAK_STATE + nxt = st * GEN ** KECCAK_STATE + blk = mid_entries * xq ** (4 * CLAIM_GROUP) + for j in unroll(0, CLAIM_GROUP): + hint_witness(blk[4 * j:4 * j + 4], "claims") + keccak_cells(blk[0:4 * CLAIM_GROUP], nxt[0:KECCAK_STATE], state=st[0:KECCAK_STATE], final=0) + last = mid * groups ** KECCAK_STATE + d0, d1 = match(log(rest), range(0, CLAIM_GROUP), lambda k: claim_list_last(last, mid_entries * groups ** (4 * CLAIM_GROUP), k)) + digest[0] = d0 + digest[1] = d1 + return digest[0], digest[1] + + +def claim_list_last(state, blk, k: Const): + digest = StackBuf(2) + if const(k == 0): + keccak_cells([0], digest, state=state[0:KECCAK_STATE], len=0) + else: + for j in unroll(0, k): + hint_witness(blk[4 * j:4 * j + 4], "claims") + keccak_cells(blk[0:4 * k], digest, state=state[0:KECCAK_STATE]) + return digest[0], digest[1] + + +@inline +def child_claims(entries_ptr, region, marks, last_g, blk, n: Const): + # n of a child's claims, each read at a hinted index into THIS node's table and + # marked in the coverage table. The index is an offset into the scheme's region + # (`region`, the coverage table from the scheme's origin) and bounded by that + # region's size, last_g * GEN, so a child's claim can only ever land on a slot of + # its own scheme. The bound so written is its own range-check target, last_g. + for j in unroll(0, n): + off_hint = hint_witness("child_claim_index") + assert log(off_hint) < log(last_g * GEN) # precondition as in the raw loops + region[off_hint] = marks * (GEN ** j) + entry = entries_ptr * (off_hint ** 4) + for c in unroll(0, 4): + blk[4 * j + c] = entry[GEN ** c] + return + + +def child_claim_list_digest(entries_ptr, region, base, limit_g, n_g): + # The hash of one of a child's declared claim lists, over the same 64n bytes the + # child hashed (`claim_list_digest`), so the digest it rebuilds is the one the + # child's statement carries. The coverage marks lie in [base, base + n): the claims' + # running count, or `base` alone along a strictly increasing gap walk. + # A list whose slots strictly increase (no duplicate slot among them) takes the + # cheaper walk; either walk is sound on its own, so the choice is the prover's. + digest = StackBuf(2) + if n_g == 1: + keccak([0, 0], [0, 0], digest, len=0) + else: + bigs, groups, rest = list_split(n_g) + mono = hint_witness("child_mono") + if mono == GEN: + d0, d1 = child_claim_list_mono(entries_ptr, region, base, limit_g, bigs, groups, rest) + digest[0] = d0 + digest[1] = d1 + else: + d0, d1 = child_claim_list_any(entries_ptr, region, base, limit_g, bigs, groups, rest) + digest[0] = d0 + digest[1] = d1 + return digest[0], digest[1] + + +def child_claim_list_any(entries_ptr, region, base, limit_g, bigs, groups, rest): + last_g = limit_g / GEN + chain = HeapBuf((bigs * groups * GEN) ** KECCAK_STATE) + zero_state(chain) + for xq in mul_range(1, bigs): + st = chain * xq ** KECCAK_STATE + nxt = st * GEN ** KECCAK_STATE + blk = StackBuf(4 * CLAIM_BIG) + child_claims(entries_ptr, region, base * xq ** CLAIM_BIG, last_g, blk, CLAIM_BIG) + keccak_cells(blk, nxt[0:KECCAK_STATE], state=st[0:KECCAK_STATE], final=0) + mid = chain * bigs ** KECCAK_STATE + mid_base = base * bigs ** CLAIM_BIG + for xq in mul_range(1, groups): + st = mid * xq ** KECCAK_STATE + nxt = st * GEN ** KECCAK_STATE + blk = StackBuf(4 * CLAIM_GROUP) + child_claims(entries_ptr, region, mid_base * xq ** CLAIM_GROUP, last_g, blk, CLAIM_GROUP) + keccak_cells(blk, nxt[0:KECCAK_STATE], state=st[0:KECCAK_STATE], final=0) + last = mid * groups ** KECCAK_STATE + d0, d1 = match(log(rest), range(0, CLAIM_GROUP), lambda k: child_claim_list_last(last, entries_ptr, region, mid_base * groups ** CLAIM_GROUP, last_g, k)) + return d0, d1 + + +@inline +def child_claims_mono(cov, ent, mark, blk, out, n: Const): + # n of a child's claims at strictly increasing offsets into THIS node's table: each + # hinted gap g^e, e >= 0 by the DEREF through it, moves the offset on by e + 1. + # cov and ent are the coverage slot and the table entry the next claim takes at + # gap zero; out receives them for the claim after the last. Every claim of the list + # writes the same mark, its first count: the walk cannot revisit a slot, and any + # other writer's value differs, so a slot written twice still conflicts. + for j in unroll(0, n): + gap = hint_witness("child_claim_gap") + assert_gpow(gap) + cov = cov * gap + gap2 = gap * gap + ent = ent * (gap2 * gap2) + cov[GEN ** j] = mark + for c in unroll(0, 4): + blk[4 * j + c] = ent[GEN ** (4 * j + c)] + out[0] = cov * GEN ** n + out[1] = ent * GEN ** (4 * n) + return + + +def child_claim_list_mono(entries_ptr, region, base, limit_g, bigs, groups, rest): + # The walk of a list at strictly increasing offsets o_j = e_0 + ... + e_j + j, every + # e_j in [0, 2^h): its first offset is at least zero, and the count of claims, a + # memory address (`assert_gpow`, below 2^30), keeps the sum below 2^60, short + # of wrapping, so bounding its last offset below the region's size + # (child_claim_list_mono_last) bounds them all. A record carries the sponge state, + # then the walk's coverage slot and table entry. + chain = HeapBuf((bigs * groups * GEN) ** MONO_REC) + zero_state(chain) + chain[GEN ** KECCAK_STATE] = region + chain[GEN ** (KECCAK_STATE + 1)] = entries_ptr + for xq in mul_range(1, bigs): + rec = chain * xq ** MONO_REC + nxt = rec * GEN ** MONO_REC + blk = StackBuf(4 * CLAIM_BIG) + walk = StackBuf(2) + child_claims_mono(rec[GEN ** KECCAK_STATE], rec[GEN ** (KECCAK_STATE + 1)], base, blk, walk, CLAIM_BIG) + nxt[GEN ** KECCAK_STATE] = walk[0] + nxt[GEN ** (KECCAK_STATE + 1)] = walk[1] + keccak_cells(blk, nxt[0:KECCAK_STATE], state=rec[0:KECCAK_STATE], final=0) + mid = chain * bigs ** MONO_REC + for xq in mul_range(1, groups): + rec = mid * xq ** MONO_REC + nxt = rec * GEN ** MONO_REC + blk = StackBuf(4 * CLAIM_GROUP) + walk = StackBuf(2) + child_claims_mono(rec[GEN ** KECCAK_STATE], rec[GEN ** (KECCAK_STATE + 1)], base, blk, walk, CLAIM_GROUP) + nxt[GEN ** KECCAK_STATE] = walk[0] + nxt[GEN ** (KECCAK_STATE + 1)] = walk[1] + keccak_cells(blk, nxt[0:KECCAK_STATE], state=rec[0:KECCAK_STATE], final=0) + last = mid * groups ** MONO_REC + d0, d1 = match(log(rest), range(0, CLAIM_GROUP), lambda k: child_claim_list_mono_last(last, region, base, limit_g, k)) + return d0, d1 + + +def child_claim_list_mono_last(rec, region, mark, limit_g, k: Const): + digest = StackBuf(2) + if const(k == 0): + keccak_cells([0], digest, state=rec[0:KECCAK_STATE], len=0) + cov_end = rec[GEN ** KECCAK_STATE] + else: + blk = StackBuf(4 * CLAIM_GROUP) + walk = StackBuf(2) + child_claims_mono(rec[GEN ** KECCAK_STATE], rec[GEN ** (KECCAK_STATE + 1)], mark, blk, walk, k) + keccak_cells(blk[0:4 * k], digest, state=rec[0:KECCAK_STATE]) + cov_end = walk[0] + # The offset after the last claim is at most the region's size. + assert log(cov_end / region) < log(limit_g * GEN) + return digest[0], digest[1] + + +def child_claim_list_last(state, entries_ptr, region, marks, last_g, k: Const): + digest = StackBuf(2) + if const(k == 0): + keccak_cells([0], digest, state=state[0:KECCAK_STATE], len=0) + else: + blk = StackBuf(4 * CLAIM_GROUP) + child_claims(entries_ptr, region, marks, last_g, blk, k) + keccak_cells(blk[0:4 * k], digest, state=state[0:KECCAK_STATE]) + return digest[0], digest[1] + + + +# ================================ LeanDA =============================== +# Hash the encoded rows and build the membership vector from the challenge, then check +# their inner products. The external verifier derives the challenge from the root; +# recursion preserves both. + + +def da_verify(g_squares): + # Returns the commitment root and the membership challenge, two canonical cells each. + # Only the tree over the blob roots walks the padded count; a padding blob's root + # is a constant, which the gap loop writes without hashing anything. + shape = StackBuf(2) + hint_witness(shape, "da_shape") # n_blob, then log2 of the padded count + n_blob_g = shape[0] + g_log_pad = shape[1] + n_pad_g, gap_g = da_row_shape(n_blob_g, g_log_pad, g_squares) + + # The challenge as its 32 bytes, two cells whose top limbs the KECCAK hashing them checks. + z = hint_witness("da_challenge") + z_cells = StackBuf(2) + fs_pack(z_cells, 0, z, 0) + zs, top_leaves, cell_factors, offsets = da_vector(z) + + # A blob's region: its tree, then its running sum, a fresh write-once cell per column. + blobs = HeapBuf(n_blob_g ** DA_REGION) + rowbase = HeapBuf(n_blob_g) + for xi in mul_range(1, n_blob_g): + base = blobs * (xi ** DA_REGION) + rowbase[xi] = base + base[GEN ** DA_SUM] = 0 + + for xb in mul_range(1, GEN ** DA_CELLS): + da_verify_column(xb, n_blob_g, rowbase, zs, top_leaves, cell_factors, offsets) + + roots = HeapBuf(n_pad_g ** 4) + for xi in mul_range(1, n_blob_g): + base = rowbase[xi] + assert base[GEN ** (DA_SUM + DA_CELLS)] == 0 + b0, b1 = da_levels(base, DA_CELLS, DA_BLOCK_BITS) + slot = roots * (xi ** 2) + slot[1] = b0 + slot[GEN] = b1 + for xd in mul_range(1, gap_g): + pad = roots * (n_blob_g ** 2) * (xd ** 2) + pad[1] = DA_PAD_BLOB_0 + pad[GEN] = DA_PAD_BLOB_1 + r0, r1 = match(log(g_log_pad), range(0, DA_TREE_ARMS), lambda k: da_levels(roots, 2 ** k, k)) + return r0, r1, z_cells[0], z_cells[1] + + +def da_row_shape(n_blob_g, g_log_pad, g_squares): + assert n_blob_g != 1 + assert log(n_blob_g) < DA_MAX_ROWS + 1 + assert log(g_log_pad) < DA_LOG_MAX_ROWS + 1 + n_pad_g = g_squares[g_log_pad] + # n_blob <= n_pad < 2*n_blob: both differences must be nonnegative. + gap_g = n_pad_g / n_blob_g + assert log(gap_g) < DA_MAX_ROWS + 1 + assert log(n_blob_g * n_blob_g / (n_pad_g * GEN)) < DA_MAX_ROWS + return n_pad_g, gap_g + + +def da_verify_column(xb, n_blob_g, rowbase, zs, top_leaves, cell_factors, offsets): + # Column log(xb) of every blob: its weights expand the tree inside a cell from the + # column's leaf of the tree above; a cell's digest is leaf log(xb) of its blob's tree, + # and its inner product with the weights joins its blob's running sum. + lvals = da_expand(top_leaves[xb], cell_factors, zs, offsets * xb ** DA_LOG_CELL, DA_LOG_CELL - 1, DA_LOG_CELL) + xb2 = xb * xb + for xi in mul_range(1, n_blob_g): + d0, d1, s = da_verify_cell(lvals) + base = rowbase[xi] + chain = base * xb + chain[GEN ** (DA_SUM + 1)] = chain[GEN ** DA_SUM] + s + leaf = base * xb2 + leaf[1] = d0 + leaf[GEN] = d1 + return + + +def da_verify_cell(lvals): + # Packing checks that each symbol used by both the hash and dot product is in K. + sym = StackBuf(DA_CELL) + hint_witness(sym, "da_symbols") + packed = StackBuf(DA_PACKED) + s = 0 + for e in unroll(0, DA_PACKED): + packed[e] = pack64x2(sym[2 * e], sym[2 * e + 1]) + s = s + lvals[GEN ** (2 * e)] * sym[2 * e] + s = s + lvals[GEN ** (2 * e + 1)] * sym[2 * e + 1] + st = StackBuf(2) + keccak_cells(packed, st) + return st[0], st[1], s + + +def da_levels(tree, n: Const, log_n: Const): + # The internal levels of a Merkle tree whose leaves already sit at level 0 of + # `tree` (2 cells a node, level lvl at 4n - 4n//2**lvl). Returns the root pair. + for lvl in unroll(0, log_n): + for xp in mul_range(1, GEN ** (n // 2 ** (lvl + 1))): + a = tree * (GEN ** (4 * n - 4 * n // 2 ** lvl)) * (xp ** 4) + b = tree * (GEN ** (4 * n - 4 * n // 2 ** (lvl + 1))) * (xp * xp) + keccak(a[0:2], a[2:4], b[0:2]) + return tree[GEN ** (4 * n - 4)], tree[GEN ** (4 * n - 3)] + + +def da_vector(z): + # L(x) = prod_(j < DA_LOG_K) (1 + z_j Vhat_j(x)), z_j = z^(2^j), as a binary tree over + # x's bits from the top down: factor j depends only on the bits from j up, so a node + # is its parent times one factor. The tree over the cell index runs once; each column + # expands the tree inside a cell from its leaf, its factors shifted by the cell + # index's part of each Vhat_j (da_column_offsets). + zs = HeapBuf(DA_LOG_M) + zj = z + for j in unroll(0, DA_LOG_K): + zs[GEN ** j] = zj + zj = zj * zj + zs[GEN ** DA_LOG_K] = 0 # the top bit splits without a factor: the degree bound + above_top = HeapBuf(DA_LOG_M) # nothing sits above the tree over the cell index + for j in unroll(DA_LOG_CELL, DA_LOG_M): + above_top[GEN ** j] = 0 + top_factors = da_factors(zs, DA_LOG_K, DA_BLOCK_BITS, 0) + top_leaves = da_expand(1, top_factors, zs, above_top, DA_LOG_K, DA_BLOCK_BITS) + cell_factors = da_factors(zs, DA_LOG_CELL - 1, DA_LOG_CELL, DA_CELLS) + offsets = da_column_offsets(zs) + return zs, top_leaves, cell_factors, offsets + + +def da_factors(zs, top: Const, depth: Const, at: Const): + # 1 + z_j Vhat_j(x) for each parent node n of a product tree over bits top, top - 1, ..., + # in heap order: j the bit n splits and x the point its path's bits make, DA_VHAT[at + n]. + factors = HeapBuf(DA_CELL) # the deepest tree's parents, the one inside a cell + for d in unroll(0, depth): + z = zs[GEN ** (top - d)] + for y in unroll(0, 2 ** d): + factors[GEN ** (2 ** d + y)] = z * DA_VHAT[at + 2 ** d + y] + 1 + return factors + + +def da_expand(root, factors, zs, shifts, top: Const, depth: Const): + # The product tree below `root` over bits top, top - 1, ..., in heap order: node n's + # children 2n and 2n + 1 are n times its factor at the two values of the bit j it + # splits, factors[n] + shifts[j] and that plus z_j. Returns a pointer to its leaves. + nodes = StackBuf(2 * DA_CELL) # the deepest tree's, the one inside a cell + nodes[1] = root + for d in unroll(0, depth): + z = zs[GEN ** (top - d)] + shift = shifts[GEN ** (top - d)] + for y in unroll(0, 2 ** d): + nodes[2 ** (d + 1) + 2 * y] = nodes[2 ** d + y] * (factors[GEN ** (2 ** d + y)] + shift) + nodes[2 ** (d + 1) + 2 * y + 1] = nodes[2 ** (d + 1) + 2 * y] + nodes[2 ** d + y] * z + leaves = addr(nodes) + return leaves * GEN ** (2 ** depth) + + +def da_column_offsets(zs): + # offsets[DA_LOG_CELL * h + j] = z_j Vhat_j at the cell index h's bits, for each factor + # j inside a cell: zero at h = 0, then one doubling per bit of h. + offsets = HeapBuf(DA_LOG_CELL * DA_CELLS) + for j in unroll(0, DA_LOG_CELL): + offsets[GEN ** j] = 0 + for b in unroll(0, DA_BLOCK_BITS): + steps = HeapBuf(DA_LOG_CELL) + for j in unroll(0, DA_LOG_CELL): + steps[GEN ** j] = zs[GEN ** j] * DA_VHAT_COLUMN[DA_LOG_CELL * b + j] + for xh in mul_range(1, GEN ** (2 ** b)): + src = offsets * xh ** DA_LOG_CELL + dst = src * GEN ** (DA_LOG_CELL * 2 ** b) + for j in unroll(0, DA_LOG_CELL): + dst[GEN ** j] = src[GEN ** j] + steps[GEN ** j] + return offsets + + +# ================================ the aggregation node ============================== + + +def main(): + # One node of an aggregation tree: n_raw_generic leanSTARK proofs, n_raw_lean_sphincs + # leanSPHINCS signatures and n_children sub-proofs OF THIS SAME BYTECODE. A claim is + # data_hash | vk (leanSTARK) or message | pubkey (leanSPHINCS), and each raw proof or + # signature is verified against the claim in its own coverage slot. It publishes at most one DA digest: that of the blob matrix it + # checks, or one of its children's, or none. + # + # The declared lists are what the node publishes; the duplicate slots absorb claims a + # child covers that the set does not declare. The coverage table is one region + # per scheme, each its declared claims then its own duplicates: + # + # [leanSTARK: declared | dup][leanSPHINCS: declared | dup] + # + # so one range check per write keeps each writer inside its own region: that is + # what makes the statement's split mean which scheme verified which claim. A + # slot is four cells, a pubkey and the message that pubkey signed. + meta = StackBuf(8) + hint_witness(meta, "meta") # every count in the exponent + n_lx_g = meta[0] + n_xdup_g = meta[1] + n_raw_x_g = meta[2] + n_lean_sphincs_g = meta[3] + n_sdup_g = meta[4] + n_raw_s_g = meta[5] + n_children_g = meta[6] + n_direct_da_g = meta[7] + # The first list is the leanSTARK (0x11) one, at most MAX_GENERIC_DEPENDENCIES + # declared claims and as many proofs verified here. + assert log(n_lx_g) < MAX_GENERIC_DEPENDENCIES + 1 + assert_gpow(n_xdup_g) + assert log(n_raw_x_g) < MAX_GENERIC_DEPENDENCIES + 1 + assert_gpow(n_lean_sphincs_g) + assert_gpow(n_sdup_g) + assert_gpow(n_raw_s_g) + assert log(n_children_g) < MAX_RECURSIONS + 1 + assert log(n_direct_da_g) < 2 + lx_slots_g = n_lx_g * n_xdup_g + lean_sphincs_slots_g = n_lean_sphincs_g * n_sdup_g + # The sum of both regions bounds the coverage indices, so it is what has to be a + # memory address: its log is then below 2^30 and the runtime bounds below mean + # what they say. An honest node holds its coverage table in memory, so it fits. + n_total_g = lx_slots_g * lean_sphincs_slots_g + assert_gpow(n_total_g) + + # The proving environment (the proof system's version and this bytecode) as one + # digest. It rides the statement rather than the bytecode, so nothing here has to + # know its own hash; the outer verifier pins it, and every child statement rebuilt + # below copies it, which is what keeps a whole tree on one bytecode. + fs_seed = StackBuf(2) + hint_witness(fs_seed, "fs_seed") + seed_0 = fs_seed[0] + seed_1 = fs_seed[1] + + # ---- the claim lists ---- + g_squares = exponent_tables() + + # One table per scheme: its declared list (strictly sorted, checked by the outer + # verifier, which holds it) followed by its own duplicate slots. The coverage + # indices below run over one space: leanSTARK, then leanSPHINCS. + # + # Each list's digest is a plain hash of its claims, 64 bytes a claim, its padding + # binding its length; the statement carries the two digests side by side. + lx_table = HeapBuf(lx_slots_g ** 4) + lean_sphincs_table = HeapBuf(lean_sphincs_slots_g ** 4) + xd_0, xd_1 = claim_list_digest(lx_table, n_lx_g) + sd_0, sd_1 = claim_list_digest(lean_sphincs_table, n_lean_sphincs_g) + # The duplicate slots ride the same tables but outside the hashed prefixes. + for xd in mul_range(1, n_xdup_g): + dup = lx_table * ((n_lx_g * xd) ** 4) + hint_witness(dup[0:4], "dup_claims") + for xd in mul_range(1, n_sdup_g): + dup = lean_sphincs_table * ((n_lean_sphincs_g * xd) ** 4) + hint_witness(dup[0:4], "dup_claims") + + # ---- coverage ---- + # Every one of the n_total slots is written exactly once: write-once memory + # rejects a second write (each writer's marks lie in its own range of counts, and a + # gap-walked child list never revisits a slot, so two writes to one slot disagree), + # and the count below rejects a missed one. So every + # declared claim is covered by a direct check of its own kind or by a verified + # child. Each writer is confined to its own region. + cover = HeapBuf(n_total_g) + sp_cover = cover * lx_slots_g # the leanSPHINCS region + for xi in mul_range(1, n_raw_x_g): + idx = hint_witness("raw_index") + # The runtime bound is small by `assert_gpow(n_total_g)` above (its log below + # the memory size). Without it this degenerates to what DEREF alone gives and an + # index could reach past `cover`, which is the whole bijection. The bound is the + # leanSTARK region, so a raw leanSTARK proof covers no leanSPHINCS claim. + assert log(idx) < log(lx_slots_g) + cover[idx] = xi + verify_generic(lx_table * (idx ** 4), g_squares) + for xj in mul_range(1, n_raw_s_g): + off_hint = hint_witness("sp_raw_index") + assert log(off_hint) < log(lean_sphincs_slots_g) + sp_cover[off_hint] = n_raw_x_g * xj + verify_sig_lean_sphincs(lean_sphincs_table * (off_hint ** 4)) + + # ---- data availability ---- + # One DA digest per child, read off its statement below, then the node's own: + # H(root, challenge) of its blob matrix, or zeros without blobs. + da_digests = HeapBuf((n_children_g * GEN) ** 2) + own_da = da_digests * n_children_g ** 2 + if n_direct_da_g == GEN: + root_0, root_1, z_0, z_1 = da_verify(g_squares) + keccak([root_0, root_1], [z_0, z_1], own_da[0:2]) + else: + own_da[1] = 0 + own_da[GEN] = 0 + + # ---- children ---- + child_pi = HeapBuf(n_children_g * n_children_g) + child_fresh = HeapBuf(n_children_g ** DEFER_SIZE) + child_carried = HeapBuf(n_children_g ** DEFER_STMT_CELLS) + written = HeapBuf(n_children_g * GEN) # loop-carried write count, one per child + written[GEN ** 0] = n_raw_x_g * n_raw_s_g + for xc in mul_range(1, n_children_g): + base = written[xc] + # The child's two list lengths, then its two lists, rebuilt from THIS node's + # tables through hinted indices: everything hinted there is pinned by the + # list digests, which the child's statement carries, so a lie about any of it + # changes the public input its proof has to satisfy. + child_meta = StackBuf(2) + hint_witness(child_meta, "child_meta") # n_generic, n_lean_sphincs + nsub_x_g = child_meta[0] + nsub_s_g = child_meta[1] + assert log(nsub_x_g) < MAX_GENERIC_DEPENDENCIES + 1 + assert_gpow(nsub_s_g) + nsub_g = nsub_x_g * nsub_s_g + cx_0, cx_1 = child_claim_list_digest(lx_table, cover, base, lx_slots_g, nsub_x_g) + cs_0, cs_1 = child_claim_list_digest(lean_sphincs_table, sp_cover, base * nsub_x_g, lean_sphincs_slots_g, nsub_s_g) + carried = child_carried * xc ** DEFER_STMT_CELLS + hint_witness(carried[0:DEFER_STMT_CELLS], "child_defer") + child_da = da_digests * xc ** 2 + hint_witness(child_da[0:2], "child_da") + cd_0, cd_1 = deps_digest(cx_0, cx_1, cs_0, cs_1, nsub_x_g) + pi_0, pi_1 = statement_digest(seed_0, seed_1, cd_0, cd_1, child_da[1], child_da[GEN], carried) + pi = xc * xc + child_pi[pi] = pi_0 + child_pi[pi * GEN] = pi_1 + zeta, bus_alpha, bc_share = verify_sub(pi_0, pi_1, seed_0, seed_1, GEN ** BYTECODE_LOG, g_squares) + # A child is a proof of this bytecode: its bytecode claim joins this node's deferred one. + defer_out = child_fresh * xc ** DEFER_SIZE + for k in unroll(0, BYTECODE_LOG): + defer_out[GEN ** k] = zeta[GEN ** k] + for k in unroll(0, LOG2_BYTECODE_COLS): + defer_out[GEN ** (BYTECODE_LOG + k)] = bus_alpha[GEN ** k] + defer_out[GEN ** FRESH_BC_VALUE] = bc_share + written[xc * GEN] = base * nsub_g + assert written[n_children_g] == n_total_g + + # The published DA digest: a child's or the node's own, its own if it has blobs. + da_pick = hint_witness("da_pick") + assert log(da_pick) < log(n_children_g * GEN) + assert (da_pick + n_children_g) * (n_direct_da_g + 1) == 0 + da = da_digests * da_pick ** 2 + + # ---- this node's own deferred claim ---- + defer_stmt = HeapBuf(DEFER_STMT_CELLS) + if n_children_g == 1: + # A leaf has nothing to batch, so it defers the bytecode at the all-zeros + # point. The value rides a hint and is checked nowhere here: the outer + # verifier recomputes it and rebuilds the statement, so a lie changes the + # public input rather than the claim. + leaf_value = hint_witness("leaf_defer") + for k in unroll(0, BYTECODE_VARS): + defer_stmt[GEN ** k] = 0 + defer_stmt[GEN ** DEFER_STMT_BC_VALUE] = leaf_value + else: + aggregate_claims(n_children_g, child_pi, child_fresh, child_carried, defer_stmt) + + dd_0, dd_1 = deps_digest(xd_0, xd_1, sd_0, sd_1, n_lx_g) + own_0, own_1 = statement_digest(seed_0, seed_1, dd_0, dd_1, da[1], da[GEN], defer_stmt) + pub_ptr = GEN ** 0 + assert pub_ptr[1] == own_0 + assert pub_ptr[GEN] == own_1 + return + + +def aggregate_claims(n_children_g, child_pi, child_fresh, child_carried, defer_stmt): + # Every child contributes two claims on the stacked bytecode: the one IT + # deferred (carried in its statement) and the fresh one raised by verifying its + # proof. A fresh transcript binds all of them, samples the batching + # coefficient, and one sumcheck reduces the lot to one claim, which is what the + # node then defers in its own statement. + bc_msgs = HeapBuf(2 * BYTECODE_VARS) + hint_witness(bc_msgs[0:2 * BYTECODE_VARS], "bc_sumcheck_msgs") + bytecode_star = hint_witness("bc_star_hint") + + # ---- one transcript over every child's statement and both its claim sets ---- + # The count, padded to a full rate, then each child's words, padded likewise: a + # child starts by permuting the full rate the previous one left waiting, which + # every record carries (PEND_SLOTS) beside its capacity. + fresh_row = HeapBuf(n_children_g) + carried_row = HeapBuf(n_children_g) + seed = [AGG_SEED_0, AGG_SEED_1, AGG_SEED_2, AGG_SEED_3, AGG_SEED_4] + rate = StackBuf(8) + fs_pack(rate, 0, n_children_g, DS_OBSERVE) + for j in unroll(1, 4): + fs_pack(rate, j, 0, DS_OBSERVE) + absorb = HeapBuf((n_children_g * GEN) ** PEND_SLOTS) + for j in unroll(0, FS_CELLS): + absorb[GEN ** j] = seed[j] + for j in unroll(0, 8): + absorb[GEN ** (FS_CELLS + j)] = rate[j] + for xc in mul_range(1, n_children_g): + row = absorb * xc ** PEND_SLOTS + row_cap = fs_load(row, 0, 1) + st = fs_perm_pending(row_cap, row, FS_CELLS) + rate = StackBuf(8) + pi = xc * xc + fresh = child_fresh * xc ** DEFER_SIZE + fresh_row[xc] = fresh + carried = child_carried * xc ** DEFER_STMT_CELLS + carried_row[xc] = carried + for pos in unroll(0, AGG_CHILD_WORDS + AGG_CHILD_PAD): + if pos % 4 == 0: + if pos != 0: + st = fs_perm(st, rate, 4) + rate = StackBuf(8) + if const(pos // 2 == 0): + fs_pack(rate, pos % 4, child_pi[pi * GEN ** pos], DS_OBSERVE) + if const(pos // 2 != 0): + if const((pos - 2) // DEFER_SIZE == 0): + fs_pack(rate, pos % 4, fresh[GEN ** (pos - 2)], DS_OBSERVE) + if const((pos - 2) // DEFER_SIZE != 0): + if const((pos - 2 - DEFER_SIZE) // DEFER_STMT_CELLS == 0): + fs_pack(rate, pos % 4, carried[GEN ** (pos - 2 - DEFER_SIZE)], DS_OBSERVE) + if const((pos - 2 - DEFER_SIZE) // DEFER_STMT_CELLS != 0): + fs_pack(rate, pos % 4, 0, DS_OBSERVE) + nxt = row * GEN ** PEND_SLOTS + fs_store(nxt, 0, 1, st) + for j in unroll(0, 8): + nxt[GEN ** (FS_CELLS + j)] = rate[j] + absorbed = absorb * n_children_g ** PEND_SLOTS + + # ---- the batching sumcheck (BYTECODE_VARS variables, 2 claims per child) ---- + # One challenge lam, its permutation taking the last full rate: child i's fresh + # claim takes lam^(2i), its carried one lam^(2i+1). Fresh and carried share one + # layout (point, then value), so the two differ only in which buffer they come + # from. Each record holds the child's power of lam^2, then the running value. + absorbed_cap = fs_load(absorbed, 0, 1) + st = fs_perm_pending(absorbed_cap, absorbed, FS_CELLS) + lam = challenge_from_state(st) + lam_sq = lam * lam + bc_chain = HeapBuf((n_children_g * GEN) ** PAIR_SLOTS) + bc_chain[GEN ** 0] = 1 + bc_chain[GEN ** 1] = 0 + for xc in mul_range(1, n_children_g): + row = bc_chain * xc ** PAIR_SLOTS + lam_pow = row[GEN ** 0] + fresh = fresh_row[xc] + carried = carried_row[xc] + nxt = row * GEN ** PAIR_SLOTS + nxt[GEN ** 0] = lam_pow * lam_sq + nxt[GEN ** 1] = row[GEN ** 1] + lam_pow * (fresh[GEN ** FRESH_BC_VALUE] + lam * carried[GEN ** DEFER_STMT_BC_VALUE]) + bc_end = bc_chain * n_children_g ** PAIR_SLOTS + bc_running = bc_end[GEN ** 1] + bc_point = HeapBuf(BYTECODE_VARS) + fs0, fs1, fs2, fs3, fs4, bc_running = batch_sumcheck(st[8], st[9], st[10], st[11], st[12], bc_msgs, bc_running, bc_point, BYTECODE_VARS) + bc_point1 = HeapBuf(BYTECODE_VARS) # 1 + bc_point, so an eq factor is one XOR + for k in unroll(0, BYTECODE_VARS): + bc_point1[GEN ** k] = 1 + bc_point[GEN ** k] + bc_wsum = HeapBuf(n_children_g * GEN) + bc_wsum[GEN ** 0] = 0 + for xc in mul_range(1, n_children_g): + fresh = fresh_row[xc] + carried = carried_row[xc] + for k in unroll(0, BYTECODE_VARS): + rk1 = bc_point1[GEN ** k] + if const(k == 0): + eq_fresh = fresh[GEN ** k] + rk1 + eq_carried = carried[GEN ** k] + rk1 + else: + eq_fresh *= fresh[GEN ** k] + rk1 + eq_carried *= carried[GEN ** k] + rk1 + row = bc_chain * xc ** PAIR_SLOTS + bc_wsum[xc * GEN] = bc_wsum[xc] + row[GEN ** 0] * (eq_fresh + lam * eq_carried) + assert bc_running == bytecode_star * bc_wsum[n_children_g] + + for k in unroll(0, BYTECODE_VARS): + defer_stmt[GEN ** k] = bc_point[GEN ** k] + defer_stmt[GEN ** DEFER_STMT_BC_VALUE] = bytecode_star + return diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/cells.rs b/src/ffi/crates/rec_aggregation/src/aggregation/cells.rs new file mode 100644 index 0000000..50ce0fc --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/cells.rs @@ -0,0 +1,38 @@ +//! Field-element encodings shared by the host and the guest. + +use primitives::field::{F192, g_pow}; + +/// A count as the guest carries it: in the exponent, `g^n`. +pub(super) fn count(n: usize) -> F192 { + F192::from(g_pow(n)) +} + +/// Bytes, each in the exponent: how the guest bounds a value it rebuilds +/// (`sp_from_bytes`). +pub(super) fn exponent_bytes(bytes: &[u8]) -> Vec { + bytes.iter().map(|&b| count(b.into())).collect() +} + +/// A field element as the decimal `u128` literal the zkDSL parser accepts. +pub(super) fn dsl_u128(value: F192) -> u128 { + assert_eq!(value.c2, 0, "u128 DSL literal cannot encode the top F192 limb"); + (value.c0 as u128) | ((value.c1 as u128) << 64) +} + +/// A 16-byte native value as one canonical 128-bit cell. +pub(super) fn pack_16_bytes(bytes: &[u8]) -> F192 { + let word_at = |offset: usize| u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap()); + F192::new(word_at(0), word_at(8), 0) +} + +/// A run of canonical 128-bit cells as the byte string the hash takes: each cell +/// is its two low limbs, little-endian, which is the order the VM's hash +/// instruction reads a memory cell in. +pub(super) fn cell_bytes(cells: impl IntoIterator) -> Vec { + let mut bytes = Vec::new(); + for cell in cells { + bytes.extend_from_slice(&cell.c0.to_le_bytes()); + bytes.extend_from_slice(&cell.c1.to_le_bytes()); + } + bytes +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/child.rs b/src/ffi/crates/rec_aggregation/src/aggregation/child.rs new file mode 100644 index 0000000..eba6134 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/child.rs @@ -0,0 +1,288 @@ +//! The guest's witness for one child proof, and the opening shapes it shares with the guest's constants. + +use super::cells::count; +use super::deferred::DeferredSubproof; +use super::error::AggregationError; +use super::hints::SubHints; +use fiat_shamir::merkle::hash_to_scalars; +use leanvm_core::cpu::{Program, from_scalar}; +use leanvm_core::leaf::Block; +use primitives::field::F192; + +/// The verifier-side WHIR config for one committed size and rate, plus the +/// query packing derived from it. The hint builder needs it for the real +/// opening and the placeholder map for every candidate size, so it lives here: +/// a candidate whose shape differed from the real one would compile a guest +/// that cannot open the proof it is handed. +pub(super) struct WhirShape { + pub(super) config: pcs::whir::ProverConfig, + pub(super) levels: pcs::whir::LevelShapes, + /// Merkle tree depth per level. + pub(super) depth: Vec, + /// Query positions carried by one squeezed F192 word, per level. + pub(super) per_squeeze: Vec, +} + +pub(super) fn whir_shape(mu: usize, log_inv_rate: usize) -> WhirShape { + let config = pcs::whir::config_for_rate(mu, log_inv_rate).expect("stacked whir config"); + let levels = config.level_shapes(mu); + let depth: Vec = levels.block_len.iter().map(|b| b.trailing_zeros() as usize).collect(); + let per_squeeze = depth.iter().map(|&d| 192 / d).collect(); + WhirShape { + config, + levels, + depth, + per_squeeze, + } +} + +/// The path bits a query's Merkle walk dispatches on at a time, the guest's `QW` (`walk_path`). +pub(super) const QUERY_WINDOW_BITS: usize = 6; +/// The cells `decode_query_windows` holds a squeezed word's windows in, the guest's `QUERY_WINDOWS_CAP`. +pub(super) const QUERY_WINDOWS_CAP: usize = 64; + +/// The depth of the cap a level's paths are walked into, which leaves the walked part whole windows of +/// [`QUERY_WINDOW_BITS`] bits: at least two levels deep, and from `log2(queries) - 5` to five levels below that. +pub(super) fn merkle_cap_depth(queries: usize, depth: usize) -> usize { + let near = (queries.next_power_of_two().ilog2() as usize) + .saturating_sub(QUERY_WINDOW_BITS - 1) + .max(2) + .min(depth); + depth - (depth - near) / QUERY_WINDOW_BITS * QUERY_WINDOW_BITS +} + +/// The first column of each column group: a maximal run of committed columns, in compact +/// order, sharing one kappa source. The stacked order (descending kappa, then index) keeps +/// a group's columns consecutive, since any other column of equal kappa lies outside it. +pub(super) fn block_groups(sources: &[(usize, usize)], side_starts: &[usize]) -> Vec { + // As `col_groups`, per side: a side's blocks tile its own cube. + (0..sources.len()) + .filter(|&b| side_starts.contains(&b) || sources[b] != sources[b - 1]) + .collect() +} + +/// The kappa source of the inner program's bytecode log: after `log_mem` (1) and the table +/// logs (`2 + t`), so one guest verifies proofs of programs of any bytecode size. +pub(super) const KBC_SOURCE: usize = leanvm_core::tables::N_TABLES + 2; + +/// A bytecode log no program has: upstream's `(0, PROBE)` entries are the bytecode-sized ones. +const KBC_PROBE: usize = 1 << 20; + +fn bytecode_source(source: (usize, usize)) -> (usize, usize) { + assert!(source.1 < KBC_PROBE || source == (0, KBC_PROBE), "a bytecode-sized kappa off its constant source"); + if source == (0, KBC_PROBE) { (KBC_SOURCE, 0) } else { source } +} + +/// [`leanvm_core::cpu::block_kappa_sources`] with the bytecode log as the runtime source [`KBC_SOURCE`]. +pub(super) fn block_sources() -> Vec<(usize, usize)> { + leanvm_core::cpu::block_kappa_sources(KBC_PROBE).into_iter().map(bytecode_source).collect() +} + +/// [`leanvm_core::cpu::col_kappa_sources`] with the bytecode log as the runtime source [`KBC_SOURCE`]. +pub(super) fn col_sources() -> Vec> { + leanvm_core::cpu::col_kappa_sources(KBC_PROBE).into_iter().map(|s| s.map(bytecode_source)).collect() +} + +pub(super) fn col_groups(sources: &[(usize, usize)]) -> Vec { + (0..sources.len()) + .filter(|&c| c == 0 || sources[c] != sources[c - 1]) + .collect() +} + +/// Config + hints for the recursion guest (`guests/lean_ethereum.py`), built +/// from the REAL `cpu::layout` of the inner program, the `summary` the +/// `verifier` crate read out of the child's `proof`, and that proof's Merkle +/// paths, whose positions and sizes its replay pinned (zero hand-mirroring +/// drift). Nothing here says the child verifies: the guest's run does. +pub(super) fn gen_verify( + program: &Program, + public_input: [F192; 2], + proof: &leanvm_core::cpu::Proof, + summary: &verifier::Summary, + bytecode_value: F192, +) -> Result<(SubHints, DeferredSubproof), AggregationError> { + let layout = program.layout( + summary.log_memory, + std::array::from_fn(|t| summary.table_rows[t] as usize), + public_input, + ); + let sides: [&[Block]; 3] = [&layout.push, &layout.pull, &layout.count]; + let side_layouts = sides.map(leanvm_core::leaf::layout); + // The guest holds one opening arm per committed size an execution can have, so + // a child the native verifier accepts always has one. Fixed capacities: every + // buffer/stride placeholder is a global cap so the placeholder map is + // SHAPE-INDEPENDENT (the definition of generic). A child that verifies fits them. + let fits = side_layouts.iter().all(|side| side.mu <= MU_CAP) && proof.stream.len() <= STREAM_CAP; + if !fits { + return Err(AggregationError::ChildOutOfRange { + log_committed: layout.shape.mu, + }); + } + + // ---- typed extraction: the proof's stream + the verifier's summary ---- + let scalars = |values: &[verifier::E]| -> Vec { values.iter().copied().map(from_scalar).collect() }; + // Push and pull share the bytecode point. + let bytecode_point = scalars(&summary.bytecode_point); + + let taus = layout.taus; + // ---- the stacked opening: config + the opening summary ---- + let stack = whir_shape(layout.shape.mu, summary.log_inv_rate); + + // ---- hints ---- + // The program's whole share of a bytecode leaf: ONE value, the stacked + // polynomial at (ζ_lo, α⃗), the slot coordinates of the claim's own point being + // the fingerprint challenges (§sec:e2e-bc). The summary carries the point + // only; the caller evaluates it. + let bcv = vec![bytecode_value]; + + // ---- per-sub HINT data (the placeholder map is built once, elsewhere) ---- + // Per side, the packing order read straight off `leaf::layout`'s offsets: + // sort_order[side_base + rank] = g^{global index of the rank-r block}. The + // guest certifies it is the native verifier's order (descending kappa, then + // ascending index) before deriving offsets from it. + let mut sort_order: Vec = Vec::new(); + let mut gbase = 0usize; + let block_sources = block_sources(); + let side_starts: Vec = sides + .iter() + .scan(0, |at, blocks| { + let start = *at; + *at += blocks.len(); + Some(start) + }) + .chain([block_sources.len()]) + .collect(); + let block_groups = block_groups(&block_sources, &side_starts); + for (s, blocks) in sides.iter().enumerate() { + let mut order: Vec = (0..blocks.len()).collect(); + order.sort_by_key(|&i| side_layouts[s].offsets[i]); + let mut last_group = usize::MAX; + for &i in &order { + // g^{global group index}, once per group: its blocks are consecutive. + let group = block_groups.partition_point(|&start| start <= gbase + i) - 1; + if group != last_group { + sort_order.push(count(group)); + last_group = group; + } + } + gbase += blocks.len(); + } + assert_eq!( + sort_order.len(), + block_groups.len(), + "a block group's blocks are consecutive" + ); + // The stacked commitment uses witness::placements_of: committed columns + // sorted by descending kappa, then by their native column index. Transport + // compact committed-column indices; the guest certifies the permutation, + // ordering, and accumulated offsets before using them as claim selectors. + let col_sources = col_sources(); + let committed_globals: Vec = col_sources + .iter() + .enumerate() + .filter_map(|(i, source)| source.map(|_| i)) + .collect(); + let mut compact_col = vec![usize::MAX; col_sources.len()]; + for (compact, &global) in committed_globals.iter().enumerate() { + compact_col[global] = compact; + } + let mut col_order = committed_globals; + col_order.sort_by_key(|&global| layout.placements[global].offset); + // The order by column group (`col_groups`): its columns are consecutive in it. + let groups = col_groups(&col_sources.iter().flatten().copied().collect::>()); + let group_of = |compact: usize| groups.partition_point(|&start| start <= compact) - 1; + let mut col_group_order: Vec = Vec::new(); + let mut last_group = usize::MAX; + for &global in &col_order { + let group = group_of(compact_col[global]); + if group != last_group { + col_group_order.push(count(group)); + last_group = group; + } + } + assert_eq!( + col_group_order.len(), + groups.len(), + "a column group's columns are consecutive" + ); + + // ---- Hints for the stacked WHIR opening ---- + // Share the upper Merkle tree across a level's queries, one cap entry a level. Unknown, + // unused subtrees stay opaque; the guest authenticates every cap leaf a query reaches. + let mut query_hints = Vec::new(); + let levels = stack.config.queries.iter().zip(&stack.depth); + for ((&queries, &depth), phase) in levels.zip(&proof.merkle) { + let cap_depth = merkle_cap_depth(queries, depth); + let n = 1 << cap_depth; + let path_depth = depth - cap_depth; + let mut nodes = vec![[0u8; 32]; 2 * n]; + let mut active = vec![F192::ZERO; n]; + for opening in phase { + query_hints.push(( + "merkle_leaf_rows", + opening.leaf.iter().map(|&x| F192::from(x)).collect(), + )); + let mut path_children = Vec::with_capacity(4 * path_depth); + let mut node = fiat_shamir::merkle::hash_words(&opening.leaf); + let mut index = (1 << depth) + opening.leaf_index as usize; + for (height, sibling) in opening.siblings.iter().enumerate() { + if height >= path_depth { + nodes[index] = node; + nodes[index ^ 1] = *sibling; + active[index >> 1] = F192::ONE; + } + let (left, right) = if index & 1 == 0 { + (&node, sibling) + } else { + (sibling, &node) + }; + if height < path_depth { + path_children.extend(hash_to_scalars(left)); + path_children.extend(hash_to_scalars(right)); + } + node = pcs::merkle::hash_pair(left, right); + index >>= 1; + } + nodes[1] = node; + query_hints.push(("merkle_children", path_children)); + } + query_hints.push(("merkle_caps", nodes.iter().flat_map(hash_to_scalars).collect())); + query_hints.push(("merkle_cap_active", active)); + } + let deferred = DeferredSubproof { + public_input, + bytecode_point, + bytecode_value, + }; + + let mut hints = vec![ + ("stream", { + // The guest replays the WHIR opening off the same stream the native + // verifier reads: every transmitted scalar (sumcheck messages, level + // roots, OOD claims, grind nonces, `yr`) is already there in protocol + // order, so there is nothing to reassemble. The guest's `open_stacked` + // picks it up at `msg_cursor = cursor`, which sits where the Keccak + // reduction stopped; the ring-switch messages are struct-observed and + // still do not advance that cursor. + let mut stream = proof.stream.clone(); + stream.resize(STREAM_CAP, F192::ZERO); + stream + }), + ("bytecode_val", bcv), + // the table sumcheck's round count: max_t tau_t, certified in-guest as a + // maximum (one of the taus, and dominating them all). + ("zc_tau_max", vec![count(*taus.iter().max().unwrap())]), + // Every table's log height, which the guest certifies against its row count. + ("table_logs", taus.iter().map(|&t| count(t)).collect()), + ("col_group_order", col_group_order), + ("block_group_order", sort_order), + ]; + hints.extend(query_hints); + Ok((hints, deferred)) +} + +/// The guest's baked buffer caps, which `placeholder_map` compiles in and +/// `gen_verify` admits against: one definition, so a hinted shape can never +/// outgrow the buffer the guest was compiled with. +pub(super) const MU_CAP: usize = 40; +pub(super) const STREAM_CAP: usize = 8192; diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/claim_pool.rs b/src/ffi/crates/rec_aggregation/src/aggregation/claim_pool.rs new file mode 100644 index 0000000..668a18b --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/claim_pool.rs @@ -0,0 +1,165 @@ +//! The guest's claim pool and its coordinate terms. + +use super::cells::{count, dsl_u128}; +use leanvm_core::cpu::keccak_value; +use leanvm_core::leaf::{Block, Coord}; +use primitives::field::F192; +use std::collections::BTreeMap; + +/// Why the guest reads every `q_keccak` slot claim's instance point off `chi`: a +/// virtual value column is referenced only by its own table's bus blocks, which +/// the table sumcheck settles, so no framework block can raise one at `zeta`. +const VALCOL_FRAMEWORK: &str = "a framework block must not reference a virtual value column"; + +/// One entry of the guest's claim pool. +pub(super) enum ClaimSite { + /// A committed column read by a framework bus block of kappa source `source`. + Framework { column: usize, source: (usize, usize) }, + /// A table column, a KECCAK value lane's ([`keccak_value`]) included. + TableColumn { column: usize, table: usize }, + /// One of the three PI memory limbs (MEM_LO, MEM_HI, MEM_TOP). + MemoryLimb { column: usize }, +} + +/// The guest's `COORD_KIND_*` code for a coordinate (`guests/lean_ethereum.py`), +/// shared by its `COORD_TYPE` and `TERM_TYPE` arrays. +pub(super) fn coord_kind(c: &Coord) -> usize { + match c { + Coord::Const(_) => 0, + Coord::Col(_) => 1, + Coord::GCol(..) => 2, + Coord::Index => 3, + Coord::Public(_) => 4, + Coord::Prod(..) => 5, + Coord::Sum(..) => 6, + } +} + +/// The `K` scalar a coordinate carries beside its columns: the constant itself, +/// or the `g^k` a `GCol`/`Prod` scales by. Zero for every other kind. +pub(super) fn coord_scale(c: &Coord) -> F192 { + match c { + Coord::Const(v) => F192::from(*v), + Coord::GCol(_, k) | Coord::Prod(_, _, k) => count(*k as usize), + _ => F192::ZERO, + } +} + +/// Flatten one table-block coordinate into the guest's term arrays, in local +/// column indices. A [`Coord::Sum`]'s children are its terms; every other kind is +/// one term. `Index`/`Public` never reach a table block. +pub(super) fn push_coord_terms(c: &Coord, base: usize, terms: &mut Vec) { + let (column_a, column_b) = match c { + Coord::Const(_) => (0, 0), + Coord::Col(i) | Coord::GCol(i, _) => (*i - base, 0), + Coord::Prod(i, j, _) => (*i - base, *j - base), + Coord::Sum(cs) => { + for c in cs { + push_coord_terms(c, base, terms); + } + return; + } + Coord::Index | Coord::Public(_) => unreachable!("a table's bus block carries no virtual coordinate"), + }; + terms.push(Term { + kind: coord_kind(c), + constant: dsl_u128(coord_scale(c)), + column_a, + column_b, + }); +} + +/// The terms of `a + b` (characteristic 2), merged: constants summed, the scales of +/// one column or one column product summed, and whatever sums to zero dropped. +pub(super) fn coord_difference(a: &[Term], b: &[Term]) -> Vec { + let (konst, col, gcol) = (0, 1, 2); // coord_kind of Const, Col, GCol + let one = dsl_u128(F192::ONE); + let mut constant = 0u128; + let mut scaled: BTreeMap<(usize, usize, usize), u128> = BTreeMap::new(); + for t in a.iter().chain(b) { + match t.kind { + k if k == konst => constant ^= t.constant, + k if k == col => *scaled.entry((gcol, t.column_a, 0)).or_default() ^= one, + k if k == gcol => *scaled.entry((gcol, t.column_a, 0)).or_default() ^= t.constant, + k => { + let (x, y) = (t.column_a.min(t.column_b), t.column_a.max(t.column_b)); + *scaled.entry((k, x, y)).or_default() ^= t.constant; + } + } + } + let mut out = Vec::new(); + if constant != 0 { + out.push(Term { + kind: konst, + constant, + column_a: 0, + column_b: 0, + }); + } + for ((kind, column_a, column_b), constant) in scaled { + if constant != 0 { + let kind = if kind == gcol && constant == one { col } else { kind }; + out.push(Term { + kind, + constant, + column_a, + column_b, + }); + } + } + out +} + +/// Visit the claim pool in the exact order the guest indexes it: the framework +/// bus claims (deduped by `(column, kappa)`, as `leaf.rs` pools them), then every +/// table's committed columns, then the PI memory triple. The placeholder map's +/// claim descriptors follow this order. +pub(super) fn walk_claims(layout: &leanvm_core::cpu::Layout, mut visit: impl FnMut(ClaimSite)) { + let sides: [&[Block]; 3] = [&layout.push, &layout.pull, &layout.count]; + // Only the framework blocks raise claims: a table's coords are settled inside + // the table sumcheck. + let sources = super::child::block_sources(); + let mut seen: std::collections::HashSet<(usize, usize)> = Default::default(); + let mut bi = 0usize; + for blocks in sides.iter() { + for blk in blocks.iter() { + let source = sources[bi]; + bi += 1; + if (2..2 + leanvm_core::tables::N_TABLES).contains(&source.0) { + continue; + } + for c in &blk.coords { + if let Coord::Col(i) | Coord::GCol(i, _) = c { + if !seen.insert((*i, blk.kappa)) { + continue; // deduped: pooled once at its first occurrence + } + assert!(keccak_value(*i).is_none(), "{VALCOL_FRAMEWORK}"); + visit(ClaimSite::Framework { column: *i, source }); + } + } + } + } + let sch = leanvm_core::cpu::schema(); + for (t, table) in leanvm_core::tables::tables().iter().enumerate() { + for c in 0..table.n_committed_columns() { + visit(ClaimSite::TableColumn { + column: sch.base[t] + c, + table: t, + }); + } + } + for &column in &[ + leanvm_core::cpu::MEM_LO, + leanvm_core::cpu::MEM_HI, + leanvm_core::cpu::MEM_TOP, + ] { + visit(ClaimSite::MemoryLimb { column }); + } +} + +pub(super) struct Term { + pub(super) kind: usize, + pub(super) constant: u128, + pub(super) column_a: usize, + pub(super) column_b: usize, +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/coverage.rs b/src/ffi/crates/rec_aggregation/src/aggregation/coverage.rs new file mode 100644 index 0000000..3df7c9a --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/coverage.rs @@ -0,0 +1,127 @@ +//! The coverage table: which slot each raw signature and child claim writes. + +use super::error::AggregationError; +use super::proof::{EthereumClaims, EthereumProof}; +use super::{GenericClaim, LeanSphincsClaim}; +use std::collections::BTreeSet; + +/// The coverage slot each write in the guest's coverage walk targets, in walk +/// order: the raw signatures first, then each child's claim lists. +/// +/// The table is one contiguous region per scheme, each its declared claims followed +/// by its own duplicate slots: +/// +/// | slots | holds | +/// | --- | --- | +/// | `[0, n_generic)` | the declared leanSTARK claims | +/// | `[n_generic, X)` | leanSTARK duplicate slots | +/// | `[X, X + n_lean_sphincs)` | the declared leanSPHINCS claims | +/// | `[X + n_lean_sphincs, n_total)` | leanSPHINCS duplicate slots | +/// +/// A claim the declared set holds takes its slot there, one it does not takes a +/// fresh duplicate slot in the same region, so the walk hits every one of the +/// `n_total` slots exactly once. That bijection, enforced in-circuit by write-once +/// memory plus the final count, is what makes every declared claim covered by a real +/// signature or a verified child. +/// +/// Keeping each region contiguous is what binds the scheme: the guest addresses +/// every writer as an offset into one region, bounded by that region's size, one +/// range check per write, so no signature can reach a claim of the other scheme. +pub(super) struct Coverage { + pub(super) generic: Region, + pub(super) lean_sphincs: Region, +} + +/// One scheme's region of the coverage table. +pub(super) struct Region { + /// The declared claims, strictly sorted. + pub(super) signers: Vec, + pub(super) dups: Vec, + /// The slot of each raw signature, in the order given. + pub(super) raw: Vec, + /// Per child, the slot of each of its claims. + pub(super) children: Vec>, +} + +impl Region { + /// Declares the union of the raw claims and the children's, or exactly + /// `declare`, which they must cover. + fn plan<'a>( + raw: &[K], + children: impl Iterator + Clone, + declare: Option<&[K]>, + ) -> Result + where + K: 'a, + { + let mut signers = raw.to_vec(); + for child in children.clone() { + signers.extend_from_slice(child); + } + signers.sort(); + signers.dedup(); + if let Some(declare) = declare { + let mut wanted: BTreeSet<&K> = declare.iter().collect(); + signers.retain(|claim| wanted.remove(claim)); + if !wanted.is_empty() { + return Err(AggregationError::NotCovered); + } + } + let mut claimed = vec![false; signers.len()]; + let mut dups = Vec::new(); + let raw = raw + .iter() + .map(|claim| take_slot(&signers, &mut claimed, &mut dups, claim)) + .collect(); + let children = children + .map(|child| { + child + .iter() + .map(|claim| take_slot(&signers, &mut claimed, &mut dups, claim)) + .collect() + }) + .collect(); + Ok(Self { + signers, + dups, + raw, + children, + }) + } +} + +/// A claim takes its declared slot once; subsequent or omitted occurrences take +/// fresh slots outside the hashed prefix. +fn take_slot(claims: &[K], claimed: &mut [bool], duplicates: &mut Vec, claim: &K) -> usize { + match claims.binary_search(claim) { + Ok(pos) if !claimed[pos] => { + claimed[pos] = true; + pos + } + _ => { + duplicates.push(claim.clone()); + claims.len() + duplicates.len() - 1 + } + } +} + +pub(super) fn plan_coverage( + raw_generic: &[GenericClaim], + raw_lean_sphincs: &[LeanSphincsClaim], + children: &[EthereumProof], + declare: Option<&EthereumClaims>, +) -> Result { + let cover = Coverage { + generic: Region::plan( + raw_generic, + children.iter().map(|child| child.claims.generic.as_slice()), + declare.map(|claims| claims.generic.as_slice()), + )?, + lean_sphincs: Region::plan( + raw_lean_sphincs, + children.iter().map(|child| child.claims.lean_sphincs.as_slice()), + declare.map(|claims| claims.lean_sphincs.as_slice()), + )?, + }; + Ok(cover) +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/deferred.rs b/src/ffi/crates/rec_aggregation/src/aggregation/deferred.rs new file mode 100644 index 0000000..2a82777 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/deferred.rs @@ -0,0 +1,354 @@ +//! The deferred claim on the stacked bytecode and the sumcheck that batches it. + +use super::cells::count; +use super::error::AggregateVerifyError; +use super::guest::unified_guest; +use super::hints::SubHints; +use leanvm_core::cpu::{from_scalar, to_scalar}; +use leanvm_core::transcript::{FiatShamirState, RATE_SCALARS}; +use primitives::field::{F64, F64x8, F192, F192x8, Prod8, dot_base}; +use primitives::multilinear::{eq_table, interp, mle_eval_par, packed_eq}; +use std::ops::Range; + +pub(super) const RECURSION_AGG_LABEL: &[u8] = b"leanvm/recursion-aggregation/v1"; + +// The guest bakes a bytecode claim's width from `N_TUPLE_BITS` while `bytecode_vars` +// reads it off the stacked table, which is `N_BYTECODE_SELECTORS` wide. Two constants +// that happen to agree: were they to drift, a leaf's claim point would be one length in +// the guest and another in the statement, and nothing else would notice. +const _: () = assert!(leanvm_core::leaf::N_TUPLE_BITS == leanvm_core::leaf::N_BYTECODE_SELECTORS); + +/// The claim on the stacked bytecode that a node defers rather than evaluating +/// in-circuit: one point and value (`doc/leanvm/main.tex` §Deferred evaluation +/// claims). +/// +/// Only the point is transmitted; the value is derived from it on receipt, so a +/// prover that lies about a value changes the statement its proof has to satisfy +/// rather than the claim anyone checks. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct DeferredClaim { + pub(super) bytecode_point: Vec, + pub(super) bytecode_value: F192, +} + +impl DeferredClaim { + /// What a leaf defers: the all-zeros point, where the value is just the + /// table's first entry. + pub(super) fn leaf() -> Self { + Self::recompute(vec![F192::ZERO; bytecode_vars()]).expect("the all-zeros point has the right shape") + } + + /// Evaluate the stacked bytecode at `bytecode_point`, on the prover's field kernels and thread pool. + pub(super) fn recompute(bytecode_point: Vec) -> Result { + Self::evaluate(bytecode_point, |point| { + let _span = tracing::info_span!("bytecode mle").entered(); + mle_eval_par(stacked_bytecode(), point) + }) + } + + /// [`Self::recompute`] in the `verifier` crate: scalar and single threaded, what a received aggregate is checked with. + pub(super) fn recompute_scalar(bytecode_point: Vec) -> Result { + Self::evaluate(bytecode_point, |point| { + let _span = tracing::info_span!("bytecode mle").entered(); + let scalars: Vec<_> = point.iter().map(to_scalar).collect(); + from_scalar( + scalar_bytecode() + .evaluate(&scalars) + .expect("the point's length was checked"), + ) + }) + } + + fn evaluate( + bytecode_point: Vec, + bytecode_at: impl FnOnce(&[F192]) -> F192, + ) -> Result { + if bytecode_point.len() != bytecode_vars() { + return Err(AggregateVerifyError::MalformedClaim); + } + // Every leaf defers the all-zeros point, where the bytecode polynomial is + // just its table's first entry. Worth special-casing: the general path is + // a pass over 2^23 entries, and a leaf is the aggregate people verify most. + let bytecode_value = if bytecode_point.iter().all(|x| *x == F192::ZERO) { + F192::from(stacked_bytecode()[0]) + } else { + bytecode_at(&bytecode_point) + }; + Ok(Self { + bytecode_point, + bytecode_value, + }) + } + + /// The cells the statement digest absorbs, in the guest's `defer_stmt` order. + pub(super) fn cells(&self) -> Vec { + let mut cells = self.bytecode_point.clone(); + cells.push(self.bytecode_value); + cells + } +} + +/// The deferred-claim data the guest binds to the outer public input: the outer +/// verifier checks each claim natively (`doc/leanvm/main.tex` §Deferred evaluation claims). +/// A child's fresh claim is batched with its carried one at every child count, one included. +pub(super) struct DeferredSubproof { + pub(super) public_input: [F192; 2], + pub(super) bytecode_point: Vec, + pub(super) bytecode_value: F192, +} + +/// [`stacked_bytecode`] as the `verifier` crate takes it, hashed once: what [`EthereumProof::verify`](super::EthereumProof::verify) checks proofs against, and [`aggregate`](super::aggregate) reads its children with. +pub(super) fn scalar_bytecode() -> &'static verifier::Bytecode { + static BYTECODE: std::sync::OnceLock = std::sync::OnceLock::new(); + BYTECODE.get_or_init(|| { + let words: Vec = stacked_bytecode().iter().map(|word| word.0).collect(); + verifier::Bytecode::new(&words) + }) +} + +/// The stacked bytecode polynomial of the aggregation guest: the one fixed +/// table every node's bytecode claims are about. Cached, because verification +/// evaluates it and building it walks the whole program. +pub(super) fn stacked_bytecode() -> &'static [F64] { + static TABLE: std::sync::OnceLock> = std::sync::OnceLock::new(); + TABLE.get_or_init(|| leanvm_core::cpu::layout::bytecode_table(unified_guest().prog())) +} + +/// The rows of [`BLOCK`] entries of the stacked bytecode that hold its populated slots: every other row is +/// structurally zero. +fn bytecode_rows() -> Range { + static ROWS: std::sync::OnceLock> = std::sync::OnceLock::new(); + ROWS.get_or_init(|| { + let rows = stacked_bytecode().as_chunks::().0; + let per_slot = rows.len() >> leanvm_core::leaf::N_BYTECODE_SELECTORS; + assert!(per_slot >= GROUP, "a slot is whole groups of rows"); + let live = |s: &usize| { + rows[s * per_slot..(s + 1) * per_slot] + .iter() + .flatten() + .any(|v| *v != F64::ZERO) + }; + let slots = || 0..1 << leanvm_core::leaf::N_BYTECODE_SELECTORS; + let start = slots().find(live).expect("the bytecode is not all zero"); + let end = slots().rfind(live).expect("the bytecode is not all zero") + 1; + start * per_slot..end * per_slot + }) + .clone() +} + +/// Low row variables of the stacked bytecode, which the batch binds first. +const BLOCK_VARS: usize = 10; +const BLOCK: usize = 1 << BLOCK_VARS; +/// Rows [`bytecode_sums`] adds into its accumulators between two reads of them. +const GROUP: usize = 8; + +/// Per claim point, the stacked bytecode with the point's weight on all but the low `BLOCK_VARS` variables summed +/// out: `G(low) = Σ_r eq(point[BLOCK_VARS..], r) · row_r[low]`, over rows `r` of `BLOCK` entries. The weight +/// `eq(point, ·)` is a product of a factor on the low variables and one on the rest, so the batch's low rounds run +/// on these, and a point's value is read off its own ([`bytecode_value`]). +pub(super) fn bytecode_sums(points: &[Vec]) -> Vec> { + let _span = tracing::info_span!("Bytecode sums").entered(); + let (rows, live) = (stacked_bytecode().as_chunks::().0, bytecode_rows()); + points + .iter() + .map(|point| { + // A leaf's claim weighs the first row alone. + if point.iter().all(|&x| x == F192::ZERO) { + return rows[0].iter().map(|&w| F192::from(w)).collect(); + } + let eq = eq_table(&point[BLOCK_VARS..]); + let (groups, weights) = ( + rows[live.clone()].as_chunks::().0, + eq[live.clone()].as_chunks::().0, + ); + let partial = parallel::fold_reduce( + groups.len(), + || vec![Prod8::ZERO; BLOCK / 8], + |acc: &mut Vec, g| { + let splats = weights[g].map(F192x8::splat); + for (x, acc) in acc.iter_mut().enumerate() { + for (splat, row) in splats.iter().zip(&groups[g]) { + *acc ^= splat.mul_base_unreduced(F64x8::from_array(&row.as_chunks::<8>().0[x])); + } + } + }, + |mut a, b| { + a.iter_mut().zip(b).for_each(|(a, b)| *a ^= b); + a + }, + ); + partial.into_iter().flat_map(|p| p.reduce_lanes().to_array()).collect() + }) + .collect() +} + +/// The stacked bytecode at `point`, from its [`bytecode_sums`]: what [`mle_eval_par`] gives over the whole table. +pub(super) fn bytecode_value(sums: &[F192], point: &[F192]) -> F192 { + eq_table(&point[..BLOCK_VARS]) + .iter() + .zip(sums) + .map(|(&eq, &sum)| eq * sum) + .sum() +} + +/// Variables of a bytecode claim's point: the log row count plus the stacking +/// selectors. +pub(super) fn bytecode_vars() -> usize { + stacked_bytecode().len().trailing_zeros() as usize +} + +fn fold_lsb(table: &mut Vec, challenge: F192) { + let half = table.len() / 2; + for i in 0..half { + table[i] = interp(table[2 * i], table[2 * i + 1], challenge); + } + table.truncate(half); +} + +/// Compressed product-sumcheck round message of `u·m`: (g1, g∞) with g0 recovered from the +/// running claim. +fn round_msg(u: &[F192], m: &[F192]) -> (F192, F192) { + (0..u.len() / 2).fold((F192::ZERO, F192::ZERO), |acc, i| { + let (u0, u1) = (u[2 * i], u[2 * i + 1]); + let (m0, m1) = (m[2 * i], m[2 * i + 1]); + (acc.0 + u1 * m1, acc.1 + (u0 + u1) * (m0 + m1)) + }) +} + +/// One round of a batching sumcheck, in the transcript order the guest mirrors +/// word for word: observe `(g1, g_inf)`, squeeze the fold challenge, record +/// both, and advance the running claim through the compressed round polynomial. +/// Returns the challenge, which the caller folds its tables with. +fn absorb_round( + transcript: &mut FiatShamirState, + messages: &mut Vec, + challenges: &mut Vec, + claim: &mut F192, + (g1, gi): (F192, F192), +) -> F192 { + transcript.observe(g1); + transcript.observe(gi); + let challenge = transcript.sample(); + messages.extend([g1, gi]); + challenges.push(challenge); + let g0 = *claim + g1; + let c1 = g0 + g1 + gi; + *claim = (gi * challenge + c1) * challenge + g0; + challenge +} + +/// Mirror the guest's `aggregate_claims` transcript and prove the batching +/// sumcheck over the stacked bytecode. +/// +/// Each child brings two claims, the fresh one its verification raised and the one it deferred; `sums` holds their +/// [`bytecode_sums`] in that order. Returns the guest hints and the single claim they reduce to, or +/// [`AggregateVerifyError::MalformedClaim`] when a child carried a value that is not the bytecode's evaluation. +pub(super) fn aggregate_deferred_claims( + subproofs: &[DeferredSubproof], + carried_claims: &[&DeferredClaim], + mut sums: Vec>, +) -> Result<(SubHints, DeferredClaim), AggregateVerifyError> { + let child_count = subproofs.len(); + assert_eq!(child_count, carried_claims.len(), "one carried claim per child"); + let kbcv = bytecode_vars(); + + // The count and each child's words are padded with zeros to whole rates, so the + // guest's loop over the children finds every child starting the same way. + let mut transcript = FiatShamirState::from_label(RECURSION_AGG_LABEL); + let pad = |transcript: &mut FiatShamirState, n: usize| { + for _ in 0..n.next_multiple_of(RATE_SCALARS) - n { + transcript.observe(F192::ZERO); + } + }; + transcript.observe(count(child_count)); + pad(&mut transcript, 1); + for (subproof, carried) in subproofs.iter().zip(carried_claims) { + let mut words = vec![subproof.public_input[0], subproof.public_input[1]]; + words.extend(&subproof.bytecode_point); + words.push(subproof.bytecode_value); + words.extend(carried.cells()); + for &value in &words { + transcript.observe(value); + } + pad(&mut transcript, words.len()); + } + + let _span = tracing::info_span!("Bytecode batch", vars = kbcv).entered(); + // Powers of one challenge: child i's fresh claim takes lam^(2i), its carried one lam^(2i+1). + let lam = transcript.sample(); + let gbc: Vec = std::iter::successors(Some(F192::ONE), |&power| Some(power * lam)) + .take(2 * child_count) + .collect(); + let points: Vec> = subproofs + .iter() + .zip(carried_claims) + .flat_map(|(subproof, carried)| [subproof.bytecode_point.clone(), carried.bytecode_point.clone()]) + .collect(); + let values: Vec = subproofs + .iter() + .zip(carried_claims) + .flat_map(|(subproof, carried)| [subproof.bytecode_value, carried.bytecode_value]) + .collect(); + let mut brun: F192 = (0..2 * child_count).map(|index| gbc[index] * values[index]).sum(); + let mut bscr = Vec::new(); + let mut r_bc = Vec::new(); + // The low rounds, on each claim's sums against its `eq` on the low rows, scaled by its power of `lam`. + assert_eq!(sums.len(), points.len(), "sums for every claim"); + let mut eq_lo: Vec> = points + .iter() + .zip(&gbc) + .map(|(point, &lambda)| { + eq_table(&point[..BLOCK_VARS]) + .into_iter() + .map(|eq| lambda * eq) + .collect() + }) + .collect(); + for _ in 0..BLOCK_VARS { + let msg = sums + .iter() + .zip(&eq_lo) + .fold((F192::ZERO, F192::ZERO), |acc, (sums, eq_lo)| { + let (g1, gi) = round_msg(sums, eq_lo); + (acc.0 + g1, acc.1 + gi) + }); + let r = absorb_round(&mut transcript, &mut bscr, &mut r_bc, &mut brun, msg); + for table in sums.iter_mut().chain(&mut eq_lo) { + fold_lsb(table, r); + } + } + // The rest: every row against the low challenges, zero outside the populated ones, and each claim's `eq` on + // the rows, scaled by what its low rows left. + let (rows, live) = (stacked_bytecode().as_chunks::().0, bytecode_rows()); + let low_eq = packed_eq(&r_bc); + let mut bt = vec![F192::ZERO; rows.len()]; + parallel::fill(&mut bt[live.clone()], |i| { + dot_base(&low_eq, &rows[live.start + i]).reduce() + }); + let mut wt = vec![F192::ZERO; rows.len()]; + for (point, eq_lo) in points.iter().zip(&eq_lo) { + for (weight, eq) in wt.iter_mut().zip(eq_table(&point[BLOCK_VARS..])) { + *weight += eq_lo[0] * eq; + } + } + for _ in BLOCK_VARS..kbcv { + let msg = round_msg(&bt, &wt); + let r = absorb_round(&mut transcript, &mut bscr, &mut r_bc, &mut brun, msg); + fold_lsb(&mut bt, r); + fold_lsb(&mut wt, r); + } + let v_bc = bt[0]; + if brun != v_bc * wt[0] { + return Err(AggregateVerifyError::MalformedClaim); + } + + drop(_span); + let hints = vec![("bc_sumcheck_msgs", bscr), ("bc_star_hint", vec![v_bc])]; + Ok(( + hints, + DeferredClaim { + bytecode_point: r_bc, + bytecode_value: v_bc, + }, + )) +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/error.rs b/src/ffi/crates/rec_aggregation/src/aggregation/error.rs new file mode 100644 index 0000000..39cf3b0 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/error.rs @@ -0,0 +1,144 @@ +//! Errors of aggregation and verification. + +use lean_da::{DA_LOG_K, DA_MAX_ROWS}; +use leanvm_core::cpu::ProveError; +use pcs::whir::{MAX_LOG_INV_RATE, MIN_LOG_INV_RATE}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AggregateVerifyError { + /// The signer set is unsorted or holds a duplicate. + MalformedSignerSet, + /// A deferred claim's point has the wrong number of coordinates, or its value is not the bytecode's evaluation there. + MalformedClaim, + /// The bytes are not a valid encoding of an aggregate. + MalformedEncoding, + /// The snark did not verify. From [`aggregate`](super::aggregate), which does not verify its children, it means a child's proof could not be read. + Snark(SnarkVerifyError), +} + +/// Why a proof did not verify. Opaque: its message is for debugging. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SnarkVerifyError(pub(super) verifier::VerifierError); + +impl std::fmt::Display for SnarkVerifyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for SnarkVerifyError {} + +/// Why the prover made no proof. Opaque: its message is for debugging. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SnarkProveError(pub(super) ProveError); + +impl std::fmt::Display for SnarkProveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for SnarkProveError {} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AggregationError { + /// A child's statement is malformed, or its proof cannot be walked. The walk fails on most tampering, the openings having to sit at query positions the transcript draws, so a statement that does not match the proof usually lands here as a Merkle opening error. Any other child that does not verify is a [`Self::ProofError`]: the guest's run fails on it. + InvalidChild(AggregateVerifyError), + /// No signature claims or DA root to publish. + Empty, + /// A declared claim is not one the contributions cover. A claim is a pubkey and a + /// message, so another message is another claim. + NotCovered, + /// A raw signature's randomness does not decode to a target-sum encoding, + /// so there is no witness to build for it. + MalformedRawSignature, + /// A raw leanSTARK dependency's witness does not parse, does not hash to its key, or its proof + /// does not verify ([`super::verify_generic`]). + InvalidGenericWitness, + /// More than [`MAX_GENERIC_DEPENDENCIES`](super::MAX_GENERIC_DEPENDENCIES) leanSTARK claims in a list. + TooManyGeneric, + /// More than [`MAX_RECURSIONS`](super::MAX_RECURSIONS) children. + TooLarge, + /// The payload has a partial row or exceeds [`DA_MAX_ROWS`]. + InvalidBlobSize { symbols: usize }, + /// The requested DA commitment is not a child's. + BlobNotCovered, + /// A node with blobs was asked to publish a DA commitment other than their matrix's. + BlobsUnpublished, + /// `log_inv_rate` is outside the range the WHIR configuration accepts. + InvalidRate { log_inv_rate: usize }, + /// A child's proof outgrows the guest's baked buffers (`MU_CAP`, `STREAM_CAP`). + ChildOutOfRange { log_committed: usize }, + /// The prover made no proof of this node: the guest rejects its input (a + /// forged signature fails the guest's run), or its witness is larger than any + /// verifier accepts (too many signatures for one proof). + ProofError(SnarkProveError), +} + +impl std::fmt::Display for AggregateVerifyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MalformedSignerSet => write!(f, "malformed signer set"), + Self::MalformedClaim => write!(f, "malformed deferred claim"), + Self::MalformedEncoding => write!(f, "not a valid aggregate encoding"), + Self::Snark(e) => write!(f, "the snark did not verify: {e}"), + } + } +} + +impl std::error::Error for AggregateVerifyError {} + +impl From for AggregationError { + fn from(error: lean_da::BlobSizeError) -> Self { + Self::InvalidBlobSize { symbols: error.symbols } + } +} + +impl std::fmt::Display for AggregationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidChild(_) => write!(f, "invalid child aggregate"), + Self::Empty => write!(f, "no signature claims or DA root to publish"), + Self::NotCovered => write!(f, "a declared claim is not covered by the children and raw signatures"), + Self::MalformedRawSignature => write!(f, "a raw signature does not decode to a target-sum encoding"), + Self::InvalidGenericWitness => write!(f, "a raw leanSTARK dependency's witness does not verify"), + Self::TooManyGeneric => write!(f, "too many leanSTARK claims in a list"), + Self::TooLarge => { + write!(f, "too many children") + } + Self::InvalidBlobSize { symbols } => { + write!( + f, + "{symbols} blob symbols do not form 1..={DA_MAX_ROWS} rows of {} symbols", + 1 << DA_LOG_K + ) + } + Self::BlobNotCovered => write!(f, "the requested blob commitment is not carried by any child"), + Self::BlobsUnpublished => write!(f, "a node with blobs publishes their matrix's root"), + Self::InvalidRate { log_inv_rate } => { + write!( + f, + "log_inv_rate {log_inv_rate} is outside {}..={}", + MIN_LOG_INV_RATE, MAX_LOG_INV_RATE + ) + } + Self::ChildOutOfRange { log_committed } => { + write!( + f, + "a child commits 2^{log_committed} words, outside the guest's opening arms" + ) + } + Self::ProofError(e) => write!(f, "proving failed: {e}"), + } + } +} + +impl std::error::Error for AggregationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InvalidChild(e) => Some(e), + Self::ProofError(e) => Some(e), + _ => None, + } + } +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/generic.rs b/src/ffi/crates/rec_aggregation/src/aggregation/generic.rs new file mode 100644 index 0000000..67a2764 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/generic.rs @@ -0,0 +1,187 @@ +//! leanSTARK (scheme `0x11`) dependencies: "the leanVM program `vk` commits to accepts `data_hash`". +//! +//! # Verification key +//! +//! `vk = keccak256(verifier::TAG ‖ table)`: upstream's own program digest, the Fiat-Shamir IV every +//! proof of the program is made under (`leanvm_core::cpu::fs_seed`). `table` is the program's stacked +//! bytecode ([`leanvm_core::cpu::bytecode_table`]): all `16 · 2^kgen` words, little-endian, zero padding +//! included, `kgen ≤` [`MAX_GENERIC_BYTECODE_LOG`]. +//! +//! The guest seeds the dependency's transcript with the claim's `vk` itself, so the table is bound +//! before the first challenge, and rehashes the hinted table into `vk` with every byte range-checked +//! (the guest's `gen_table` says why the bytes hashed are the words it evaluates). The guest and +//! [`verify_generic`] accept the same claims. +//! +//! # Witness +//! +//! `u8 kgen ‖ table (16 · 2^kgen words, 8 bytes each, little-endian) ‖ proof`, the proof in +//! [`leanvm_core::cpu::Proof::to_bytes`]'s encoding, of an execution of the table on the public input +//! `data_hash` (two 16-byte halves). + +use super::cells::count; +use super::cells::pack_16_bytes; +use super::error::AggregationError; +use super::hints::SubHints; +use leanvm_core::cpu::{Proof, from_scalar, scalar_proof}; +use primitives::field::F192; + +/// The most instructions, as a log, of a leanSTARK dependency's program. +pub const MAX_GENERIC_BYTECODE_LOG: usize = 11; +/// The most leanSTARK claims one aggregation list holds. +pub const MAX_GENERIC_DEPENDENCIES: usize = 16; + +/// The domain closing the dependency digest of a list with leanSTARK claims: `"eip8288/leanstark-deps"`, zero-padded. +pub const LEANSTARK_DEPS_DOMAIN: [u8; 32] = *b"eip8288/leanstark-deps\0\0\0\0\0\0\0\0\0\0"; + +/// Stacked table words a row of the bytecode: the table is `16 · 2^kgen` words. +const ROW_WORDS: usize = 16; + +/// A parsed leanSTARK witness. +pub(super) struct GenericWitness { + pub(super) kgen: usize, + pub(super) table: Vec, + pub(super) proof: Proof, +} + +fn table_bytes(table: &[u64]) -> Vec { + table.iter().flat_map(|word| word.to_le_bytes()).collect() +} + +/// `keccak256(verifier::TAG ‖ table)`, upstream's Fiat-Shamir IV of the table. +fn program_digest(table: &[u64]) -> [u8; 32] { + primitives::hash::Hasher::new() + .update(verifier::TAG) + .update(&table_bytes(table)) + .finalize() +} + +/// `kgen` of a table length `16 · 2^kgen`, within the cap. +fn table_log(len: usize) -> Option { + let rows = len / ROW_WORDS; + let kgen = rows.trailing_zeros() as usize; + (len == rows * ROW_WORDS && rows.is_power_of_two() && kgen <= MAX_GENERIC_BYTECODE_LOG).then_some(kgen) +} + +/// The verification key of a program, from its stacked bytecode table ([`leanvm_core::cpu::bytecode_table`]'s +/// words): its program digest. `None` if the table is not `16 · 2^kgen` words with `kgen ≤` +/// [`MAX_GENERIC_BYTECODE_LOG`]. +pub fn generic_verification_key(table_words: &[u64]) -> Option<[u8; 32]> { + table_log(table_words.len())?; + Some(program_digest(table_words)) +} + +/// The witness of a leanSTARK dependency on `table_words` with `proof`. +pub fn generic_witness(table_words: &[u64], proof: &Proof) -> Option> { + let kgen = table_log(table_words.len())?; + let mut out = vec![kgen as u8]; + out.extend(table_bytes(table_words)); + out.extend(proof.to_bytes()); + Some(out) +} + +pub(super) fn parse_witness(witness: &[u8]) -> Option { + let (&kgen, rest) = witness.split_first()?; + let kgen = kgen as usize; + if kgen > MAX_GENERIC_BYTECODE_LOG { + return None; + } + let (table, proof) = rest.split_at_checked(ROW_WORDS << kgen << 3)?; + let table = table + .as_chunks::<8>() + .0 + .iter() + .map(|b| u64::from_le_bytes(*b)) + .collect(); + Some(GenericWitness { + kgen, + table, + proof: Proof::from_bytes(proof)?, + }) +} + +/// A digest as the two canonical cells the VM takes it in. +pub(super) fn digest_cells(digest: &[u8; 32]) -> [F192; 2] { + [pack_16_bytes(&digest[..16]), pack_16_bytes(&digest[16..])] +} + +/// The verifier's verdict on a parsed witness, with what the guest's witness is built from. +pub(super) fn check( + data_hash: &[u8; 32], + vk: &[u8; 32], + w: &GenericWitness, +) -> Option<(verifier::Bytecode, verifier::Summary)> { + if program_digest(&w.table) != *vk { + return None; + } + let bytecode = verifier::Bytecode::new(&w.table); + let summary = verifier::verify_execution(&bytecode, data_hash, &scalar_proof(&w.proof)).ok()?; + Some((bytecode, summary)) +} + +/// Native check of a leanSTARK dependency: `witness` parses, its table's program digest is `vk`, and its +/// proof shows an execution of the table on `data_hash`. Accepts exactly what the aggregation guest accepts. +/// +/// Also builds the guest's hints, so a proof the native verifier accepts but the guest's fixed capacities +/// (`gen_verify`'s fit check) cannot take is rejected here rather than when the block is proved. +pub fn verify_generic(data_hash: &[u8; 32], vk: &[u8; 32], witness: &[u8]) -> bool { + parse_witness(witness).is_some_and(|w| { + check(data_hash, vk, &w).is_some_and(|(bytecode, summary)| sub_hints(&w, &w, data_hash, &bytecode, &summary).is_ok()) + }) +} + +/// The guest's hints for one leanSTARK claim (`verify_generic` in the guest): `kgen`, the sub-proof's +/// witness, then the table a row at a time, each byte in the exponent. +pub(super) fn generic_hints(data_hash: &[u8; 32], vk: &[u8; 32], witness: &[u8]) -> Result { + let w = parse_witness(witness).ok_or(AggregationError::InvalidGenericWitness)?; + // Test builds only: the sub-proof's hints come from an honest witness, the table from `witness`. + #[cfg(test)] + if let Some((honest_hash, honest)) = super::hints::HONEST_GENERIC.with(|h| h.borrow().clone()) { + let h = parse_witness(&honest).ok_or(AggregationError::InvalidGenericWitness)?; + let (bytecode, summary) = + check(&honest_hash, &program_digest(&h.table), &h).ok_or(AggregationError::InvalidGenericWitness)?; + return sub_hints(&w, &h, &honest_hash, &bytecode, &summary); + } + let unchecked = cfg!(feature = "forge") && std::env::var_os("EIP8288_NO_HOST_SIGCHECK").is_some(); + let (bytecode, summary) = match check(data_hash, vk, &w) { + Some(checked) => checked, + None if unchecked => { + let bytecode = verifier::Bytecode::new(&w.table); + let summary = verifier::replay_unchecked(&bytecode, data_hash, &scalar_proof(&w.proof)) + .map_err(|_| AggregationError::InvalidGenericWitness)?; + (bytecode, summary) + } + None => return Err(AggregationError::InvalidGenericWitness), + }; + sub_hints(&w, &w, data_hash, &bytecode, &summary) +} + +/// The hints of `shown`'s size and table around the sub-proof hints of `proved`'s proof. +fn sub_hints( + shown: &GenericWitness, + proved: &GenericWitness, + data_hash: &[u8; 32], + bytecode: &verifier::Bytecode, + summary: &verifier::Summary, +) -> Result { + let value = from_scalar( + bytecode + .evaluate(&summary.bytecode_point) + .map_err(|_| AggregationError::InvalidGenericWitness)?, + ); + // The witness depends on the program's sizes only, so any program of its length stands in. + let stand_in = leanvm_core::cpu::Program::assemble( + vec![leanvm_core::cpu::Op::Xor { a: 0, b: 0, c: 0 }; 1 << proved.kgen], + Default::default(), + 0, + ); + let (sub, _) = super::child::gen_verify(&stand_in, digest_cells(data_hash), &proved.proof, summary, value)?; + let mut hints: SubHints = vec![("gen_kbc", vec![count(shown.kgen)])]; + hints.extend(sub); + for row in shown.table.chunks(ROW_WORDS) { + hints.push(( + "gen_bytes", + table_bytes(row).into_iter().map(|b| count(b.into())).collect(), + )); + } + Ok(hints) +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/generic_tests.rs b/src/ffi/crates/rec_aggregation/src/aggregation/generic_tests.rs new file mode 100644 index 0000000..2d49a7f --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/generic_tests.rs @@ -0,0 +1,402 @@ +//! leanSTARK (`0x11`) dependencies: proving, recursion, the digest, and the guest's rejections. + +use super::cells::count; +use super::error::{AggregateVerifyError, AggregationError}; +use super::hints::{ALLOW_MANY_GENERIC, FORGE, Hints}; +use super::node::aggregate; +use super::proof::{EthereumClaims, EthereumProof}; +use super::*; +use crate::signers_cache::get_lean_sphincs_signers; +use fiat_shamir::merkle::hash_to_scalars; +use lean_compiler::{compile, parse}; +use leanvm_core::cpu::{Program, bytecode_table, prove}; +use primitives::field::F192; +use signatures::lean_sphincs::{LeanSphincsPublicKey, LeanSphincsSignature}; +use std::sync::OnceLock; + +const RATE: usize = leanvm_core::pcs::TEST_LOG_INV_RATE; + +/// Publishes `keccak256` of a hinted 64-byte preimage: it accepts exactly the hashes of preimages. +const PREIMAGE: &str = "\ +def main(): + pre = StackBuf(4) + hint_witness(pre, \"pre\") + c = StackBuf(2) + keccak(pre[0:2], pre[2:4], c) + p = 1 + p[1] = c[0] + p[GEN] = c[1] + return +"; + +/// Accepts any canonical public input, after a run of arithmetic that brings it near the cap. +fn wide_source() -> String { + let mut src = String::from("def main():\n h = StackBuf(2)\n hint_witness(h, \"pi\")\n acc = h[0]\n"); + for _ in 0..WIDE_STEPS { + src += " acc = acc * h[1] + h[0]\n"; + } + src += " p = 1\n p[1] = h[0]\n p[GEN] = h[1]\n return\n"; + src +} +const WIDE_STEPS: usize = 900; + +struct Generic { + program: Program, + table: Vec, + vk: [u8; 32], +} + +fn generic(src: &str) -> Generic { + let program = compile(&parse(src).expect("parse")); + let table: Vec = bytecode_table(program.prog()).iter().map(|w| w.0).collect(); + let vk = generic_verification_key(&table).expect("within the cap"); + Generic { program, table, vk } +} + +fn preimage_program() -> &'static Generic { + static P: OnceLock = OnceLock::new(); + P.get_or_init(|| generic(PREIMAGE)) +} + +fn wide_program() -> &'static Generic { + static P: OnceLock = OnceLock::new(); + P.get_or_init(|| generic(&wide_source())) +} + +fn cells(digest: &[u8; 32]) -> [F192; 2] { + hash_to_scalars(digest) +} + +/// A preimage-check dependency: `data_hash = keccak256(seed repeated 64 times)`. +fn preimage_dep(seed: u8) -> RawGeneric { + let g = preimage_program(); + let pre = [seed; 64]; + let data_hash = primitives::hash::hash(&pre); + let mut program = g.program.clone(); + let pre_cells: Vec = pre.chunks(16).map(super::cells::pack_16_bytes).collect(); + program.set_witness("pre", vec![pre_cells]); + let (proof, _) = prove(&program, cells(&data_hash), RATE).expect("the preimage program proves"); + (data_hash, g.vk, generic_witness(&g.table, &proof).expect("a witness")) +} + +/// A dependency on the near-cap program, for any `data_hash`. +fn wide_dep(data_hash: [u8; 32]) -> RawGeneric { + let g = wide_program(); + let mut program = g.program.clone(); + program.set_witness("pi", vec![cells(&data_hash).to_vec()]); + let (proof, _) = prove(&program, cells(&data_hash), RATE).expect("the wide program proves"); + (data_hash, g.vk, generic_witness(&g.table, &proof).expect("a witness")) +} + +type RawSphincs = (LeanSphincsPublicKey, lean_sphincs::Message, LeanSphincsSignature); + +fn sphincs(n: usize) -> Vec { + get_lean_sphincs_signers(n) +} + +fn claim(raw: &RawGeneric) -> GenericClaim { + (raw.0, raw.1) +} + +/// The D6 digest, from raw bytes: each list's 64-byte entries hashed, then combined. +fn independent_deps_hash(sphincs: &[LeanSphincsClaim], generic: &[GenericClaim]) -> [u8; 32] { + let s_bytes: Vec = sphincs + .iter() + .flat_map(|(m, pk)| m.iter().chain(&pk.root).chain(&pk.public_param).copied()) + .collect(); + let s = primitives::hash::hash(&s_bytes); + if generic.is_empty() { + return s; + } + let g_bytes: Vec = generic.iter().flat_map(|(d, vk)| d.iter().chain(vk).copied()).collect(); + let g = primitives::hash::hash(&g_bytes); + primitives::hash::hash(&[s.as_slice(), &g, b"eip8288/leanstark-deps\0\0\0\0\0\0\0\0\0\0"].concat()) +} + +/// A proof verifies as an aggregate and under its digest alone. +fn check(proof: &EthereumProof) { + proof.verify().expect("verifies"); + let claims = proof.claims(); + let hash = deps_hash(&claims.lean_sphincs, &claims.generic); + assert_eq!(hash, independent_deps_hash(&claims.lean_sphincs, &claims.generic)); + if claims.da_commitment.is_none() { + verify_deps_hash(&hash, &proof.proof_bytes()).expect("verifies under its digest"); + } +} + +#[test] +fn programs_have_the_intended_sizes() { + assert!(preimage_program().program.prog().len() <= 1 << 6); + let wide = wide_program().program.prog().len(); + assert_eq!( + wide, + 1 << MAX_GENERIC_BYTECODE_LOG, + "near the cap: {}", + wide_program().program.code_len() + ); +} + +#[test] +fn native_verifier_accepts_and_rejects() { + let (data_hash, vk, witness) = preimage_dep(1); + assert!(verify_generic(&data_hash, &vk, &witness)); + let mut other = data_hash; + other[0] ^= 1; + assert!(!verify_generic(&other, &vk, &witness)); + let mut bad_vk = vk; + bad_vk[31] ^= 1; + assert!(!verify_generic(&data_hash, &bad_vk, &witness)); + for at in [0, 1, 33, 33 + 8 * 3, witness.len() - 1] { + let mut tampered = witness.clone(); + tampered[at] ^= 1; + assert!(!verify_generic(&data_hash, &vk, &tampered), "byte {at}"); + } + let mut over = witness.clone(); + over[0] = (MAX_GENERIC_BYTECODE_LOG + 1) as u8; + assert!(!verify_generic(&data_hash, &vk, &over)); + assert!(generic_verification_key(&vec![0; 16 << (MAX_GENERIC_BYTECODE_LOG + 1)]).is_none()); +} + +#[test] +fn generic_only_leaf() { + leanvm_core::init_prover(); + let dep = preimage_dep(2); + let proof = aggregate(&[], vec![dep.clone()], vec![], &[], None, RATE).expect("proves"); + assert_eq!(proof.claims().generic, vec![claim(&dep)]); + check(&proof); +} + +#[test] +fn mixed_leaf_with_a_near_cap_program() { + leanvm_core::init_prover(); + let deps = vec![preimage_dep(3), wide_dep([9; 32])]; + let proof = aggregate(&[], deps, sphincs(2), &[], None, RATE).expect("proves"); + assert_eq!( + (proof.claims().generic.len(), proof.claims().lean_sphincs.len()), + (2, 2) + ); + check(&proof); +} + +#[test] +fn pure_lean_sphincs_digest_is_unchanged() { + let claims: Vec = sphincs(3).iter().map(|(pk, m, _)| (*m, *pk)).collect(); + assert_eq!(deps_hash(&claims, &[]), independent_deps_hash(&claims, &[])); + let entries: Vec = claims + .iter() + .flat_map(|(m, pk)| m.iter().chain(&pk.root).chain(&pk.public_param).copied()) + .collect(); + assert_eq!(deps_hash(&claims, &[]), primitives::hash::hash(&entries)); +} + +/// The same 64 bytes as a leanSPHINCS and as a leanSTARK claim are two dependencies: the digests differ, +/// and each list's claims are covered only by its own scheme. +#[test] +fn the_same_bytes_under_both_schemes_are_distinct() { + let (message, pk) = sphincs(1).iter().map(|(pk, m, _)| (*m, *pk)).next().unwrap(); + let mut key = [0; 32]; + key[..16].copy_from_slice(&pk.root); + key[16..].copy_from_slice(&pk.public_param); + let as_sphincs = deps_hash(&[(message, pk)], &[]); + let as_generic = deps_hash(&[], &[(message, key)]); + let both = deps_hash(&[(message, pk)], &[(message, key)]); + assert!(as_sphincs != as_generic && as_sphincs != both && as_generic != both); +} + +#[test] +fn recursion_mixes_schemes_dedups_and_discards() { + leanvm_core::init_prover(); + let (a, b) = (preimage_dep(4), preimage_dep(5)); + let signers = sphincs(2); + // A child with both lists, and a generic-only child sharing one of its generic claims. + let mixed = aggregate(&[], vec![a.clone(), b.clone()], signers.clone(), &[], None, RATE).expect("mixed leaf"); + let generic_leaf = aggregate(&[], vec![a.clone()], vec![], &[], None, RATE).expect("generic leaf"); + let sphincs_leaf = aggregate(&[], vec![], signers[..1].to_vec(), &[], None, RATE).expect("sphincs leaf"); + let merged = aggregate( + &[mixed.clone(), generic_leaf.clone(), sphincs_leaf], + vec![], + vec![], + &[], + None, + RATE, + ) + .expect("merges"); + let mut want = vec![claim(&a), claim(&b)]; + want.sort(); + assert_eq!(merged.claims().generic, want, "the shared claim is published once"); + assert_eq!(merged.claims().lean_sphincs.len(), 2); + check(&merged); + // Discard b: a node may publish less than its children cover. + let declare = EthereumClaims { + generic: vec![claim(&a)], + lean_sphincs: mixed.claims().lean_sphincs.clone(), + da_commitment: None, + }; + let discarded = aggregate(&[mixed, generic_leaf], vec![], vec![], &[], Some(&declare), RATE).expect("discards"); + assert_eq!(discarded.claims(), &declare); + check(&discarded); + // A generic leaf merged with a raw generic dependency at the same node. + let c = preimage_dep(6); + let raw_and_child = aggregate(&[discarded], vec![c.clone()], vec![], &[], None, RATE).expect("raw and child"); + assert!(raw_and_child.claims().generic.contains(&claim(&c))); + check(&raw_and_child); +} + +#[test] +fn claims_over_the_cap_are_refused() { + leanvm_core::init_prover(); + let deps: Vec = (0..=MAX_GENERIC_DEPENDENCIES as u8) + .map(|i| preimage_dep(100 + i)) + .collect(); + assert_eq!( + aggregate(&[], deps.clone(), vec![], &[], None, RATE).err(), + Some(AggregationError::TooManyGeneric) + ); + // The guest refuses them too. + ALLOW_MANY_GENERIC.with(|allow| allow.set(true)); + let result = aggregate(&[], deps, vec![], &[], None, RATE); + ALLOW_MANY_GENERIC.with(|allow| allow.set(false)); + assert!( + matches!(result, Err(AggregationError::ProofError(_))), + "{:?}", + result.err() + ); + // A list of 17 claims fails the native check as well. + let mut claims = EthereumClaims::default(); + claims.generic = (0..=MAX_GENERIC_DEPENDENCIES as u8) + .map(|i| ([i; 32], [0; 32])) + .collect(); + let proof = aggregate(&[], vec![preimage_dep(7)], vec![], &[], None, RATE).expect("proves"); + assert_eq!( + EthereumProof::from_proof_bytes(&proof.proof_bytes(), claims).err(), + Some(AggregateVerifyError::MalformedSignerSet) + ); +} + +#[test] +fn an_invalid_witness_is_refused_natively() { + leanvm_core::init_prover(); + let (data_hash, vk, witness) = preimage_dep(8); + let mut other = data_hash; + other[5] ^= 1; + for dep in [ + (other, vk, witness.clone()), + (data_hash, [7; 32], witness.clone()), + (data_hash, vk, witness[..40].to_vec()), + ] { + assert_eq!( + aggregate(&[], vec![dep], vec![], &[], None, RATE).err(), + Some(AggregationError::InvalidGenericWitness) + ); + } +} + +fn refused_by_the_guest(name: &str, result: Result) { + assert!( + matches!(result, Err(AggregationError::ProofError(_))), + "{name}: {:?}", + result.map(|_| ()) + ); +} + +/// False claims and witnesses the host's native check refuses, handed to the guest with an honest +/// proof's sub-proof hints: every run must fail. +#[test] +fn the_guest_refuses_false_generic_claims() { + leanvm_core::init_prover(); + let (data_hash, vk, witness) = preimage_dep(9); + let signer = sphincs(1); + let (message, pk) = (signer[0].1, signer[0].0); + let mut sphincs_bytes = [0; 32]; + sphincs_bytes[..16].copy_from_slice(&pk.root); + sphincs_bytes[16..].copy_from_slice(&pk.public_param); + let flip = |mut bytes: [u8; 32]| { + bytes[0] ^= 1; + bytes + }; + let table_word = { + let mut w = witness.clone(); + w[1 + 8 * 5] ^= 1; + w + }; + // The reviewer's attack: P's proof shown with another program's table of the same size, under P's + // key (the rehash of the table misses the key) or under that table's own key (the transcript, + // seeded with that key, is not the one P's proof was made under). + let other = generic(&PREIMAGE.replace("keccak(pre[0:2], pre[2:4], c)", "keccak(pre[2:4], pre[0:2], c)")); + assert_eq!(other.table.len(), preimage_program().table.len()); + assert_ne!(other.table, preimage_program().table); + let end = 1 + 8 * other.table.len(); + let swapped = [ + &witness[..1], + &other.table.iter().flat_map(|w| w.to_le_bytes()).collect::>(), + &witness[end..], + ] + .concat(); + let cases: Vec<(&str, RawGeneric)> = vec![ + ("another table under P's key", (data_hash, vk, swapped.clone())), + ("another table under its own key", (data_hash, other.vk, swapped)), + ("wrong data_hash", (flip(data_hash), vk, witness.clone())), + ("wrong vk", (data_hash, flip(vk), witness.clone())), + ("tampered table word", (data_hash, vk, table_word)), + ("0x10 claim in the 0x11 list", (message, sphincs_bytes, witness.clone())), + ]; + super::hints::HONEST_GENERIC.with(|h| *h.borrow_mut() = Some((data_hash, witness.clone()))); + // The honest dependency itself proves under the switch: only the false ones below must fail. + aggregate(&[], vec![(data_hash, vk, witness.clone())], vec![], &[], None, RATE).expect("the honest claim proves"); + for (name, dep) in cases { + refused_by_the_guest(name, aggregate(&[], vec![dep], vec![], &[], None, RATE)); + } + super::hints::HONEST_GENERIC.with(|h| *h.borrow_mut() = None); +} + +/// A leanSTARK claim's bytes presented as a leanSPHINCS claim have no signature: the host refuses it. +#[test] +fn a_generic_claim_is_no_lean_sphincs_claim() { + let (data_hash, vk, _) = preimage_dep(10); + let mut signer = sphincs(1).remove(0); + signer.0 = LeanSphincsPublicKey { + root: vk[..16].try_into().unwrap(), + public_param: vk[16..].try_into().unwrap(), + }; + signer.1 = data_hash; + assert_eq!( + aggregate(&[], vec![], vec![signer], &[], None, RATE).err(), + Some(AggregationError::MalformedRawSignature) + ); +} + +/// Hints edited after the host built them: every run must fail. +#[test] +fn forged_generic_hints_are_rejected() { + leanvm_core::init_prover(); + let dep = preimage_dep(11); + let flip = |hints: &mut Hints, name: &str, at: usize| { + let entry = hints.entry_mut(name, 0); + entry[at] += F192::ONE; + }; + let forgeries: Vec<(&str, Box)> = vec![ + ("tampered proof scalar", Box::new(move |h| flip(h, "stream", 40))), + ( + "kgen above the cap", + Box::new(|h| *h.entry_mut("gen_kbc", 0) = vec![count(MAX_GENERIC_BYTECODE_LOG + 1)]), + ), + // The generic program's bytecode claim is settled in the guest: a false value is not deferred. + ("smuggled bytecode value", Box::new(move |h| flip(h, "bytecode_val", 0))), + // The first word's bytes split another way, (b0 + x^8, b1 + 1): the same field value, other + // bytes. The first piece is out of the byte table's range. + ( + "forged byte split", + Box::new(|h| { + let word = preimage_program().table[0]; + let (b0, b1) = ((word & 0xff) as usize, (word >> 8 & 0xff) as usize); + let row = h.entry_mut("gen_bytes", 0); + row[0] = count(b0 ^ 0x100); + row[1] = count(b1 ^ 1); + }), + ), + ]; + for (name, forgery) in forgeries { + FORGE.with(|forge| *forge.borrow_mut() = Some(Box::new(move |h: &mut Hints| forgery(h)))); + refused_by_the_guest(name, aggregate(&[], vec![dep.clone()], vec![], &[], None, RATE)); + } +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/guest.rs b/src/ffi/crates/rec_aggregation/src/aggregation/guest.rs new file mode 100644 index 0000000..821ef8e --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/guest.rs @@ -0,0 +1,67 @@ +//! Compiling the guest to a fixed point on its own size. + +use super::deferred::{scalar_bytecode, stacked_bytecode}; +use super::placeholders::placeholder_map; +use lean_compiler::{compile, parse_with_replacements}; +use leanvm_core::cpu::Program; + +/// Build the guest bytecode and the stacked table its claims are about. Both are +/// cached, so this only moves the cost out of the first prove or verify. +pub fn warm_up() { + unified_guest(); + stacked_bytecode(); + scalar_bytecode(); +} + +/// The aggregation bytecode, compiled to a fixed point on its own size. +/// +/// The recursion placeholders are a function of the inner bytecode's log size, +/// and here the inner bytecode is this one, so the size has to agree with +/// itself. Its *digest* needs no such loop: it rides the statement rather than +/// the code. The guess converges in one or two rounds because the map's only +/// size-dependent part is a handful of unrolled sumcheck rounds. +pub fn unified_guest() -> &'static Program { + static GUEST: std::sync::OnceLock = std::sync::OnceLock::new(); + GUEST.get_or_init(|| { + // A guess at the guest's own size: right, it compiles once; wrong, one more round. + let mut guess = 19; + for _ in 0..8 { + let guest = compile_guest(guess); + let actual = guest.prog().len().trailing_zeros() as usize; + if actual == guess { + return guest; + } + guess = actual; + } + panic!("the aggregation bytecode's self-referential compile did not converge"); + }) +} + +pub(super) fn compile_guest(kbc: usize) -> Program { + let replacements = placeholder_map(kbc); + // `DBG_PLACEHOLDERS=path`: dump the baked guest constants, to read alongside + // a `DBG_PROF_DUMP` profile (the guest's shape is entirely in these). + if let Ok(path) = std::env::var("DBG_PLACEHOLDERS") { + let dump: String = replacements.iter().map(|(k, v)| format!("{k} = {v}\n")).collect(); + std::fs::write(&path, dump).expect("write DBG_PLACEHOLDERS"); + } + let guest = compile( + &parse_with_replacements(include_str!("../../guests/lean_ethereum.py"), &replacements) + .expect("the repository aggregation guest must parse"), + ); + // `DBG_DISASM=path`: dump the guest's disassembly, to read alongside a + // `DBG_PROF_DUMP` per-pc profile. Function boundaries lead the dump, so the + // pc a failed guest check reports can be resolved to a source function + // without re-deriving the layout by hand. + if let Ok(path) = std::env::var("DBG_DISASM") { + let mut ranges: Vec<_> = guest.fn_ranges.iter().collect(); + ranges.sort_by_key(|(_, entry, _)| *entry); + let mut dump = String::new(); + for (name, entry, len) in ranges { + dump += &format!("# fn {entry:>7}..{:<7} {name}\n", entry + len); + } + dump += &lean_compiler::disassemble(guest.prog()); + std::fs::write(&path, dump).expect("write DBG_DISASM"); + } + guest +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/hints.rs b/src/ffi/crates/rec_aggregation/src/aggregation/hints.rs new file mode 100644 index 0000000..93658c2 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/hints.rs @@ -0,0 +1,91 @@ +//! Hint streams and the raw signatures' witnesses. + +use super::cells::{count, pack_16_bytes}; +use super::error::AggregationError; +use leanvm_core::cpu::Program; +use primitives::field::F192; +use signatures::lean_sphincs; +use signatures::lean_sphincs::{LeanSphincsPublicKey, LeanSphincsSignature}; + +/// Named hint entries for a single sub-proof, ordered within each stream. +pub(super) type SubHints = Vec<(&'static str, Vec)>; + +/// An edit to a node's hints: how a test hands the guest a witness the honest host +/// would not. +#[cfg(test)] +pub(super) type Forgery = Box; + +#[cfg(test)] +thread_local! { + /// The forgery the next node this thread aggregates gets. + pub(super) static FORGE: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; +} + +#[cfg(test)] +thread_local! { + /// Lets the next nodes this thread aggregates take more leanSTARK claims than the cap, to see the guest refuse them. + pub(super) static ALLOW_MANY_GENERIC: std::cell::Cell = const { std::cell::Cell::new(false) }; + /// An honest leanSTARK `(data_hash, witness)` whose proof the next nodes' leanSTARK sub-proof hints + /// come from, whatever claim and witness they are handed: how a test shows the guest a false claim. + pub(super) static HONEST_GENERIC: std::cell::RefCell)>> = const { std::cell::RefCell::new(None) }; +} + +/// One `hint_witness` stream: a name and its entries, in the order the guest +/// pops them. +#[derive(Default)] +pub(super) struct Hints(Vec<(String, Vec>)>); + +impl Hints { + pub(super) fn push(&mut self, name: &str, entry: Vec) { + match self.0.iter_mut().find(|(n, _)| n == name) { + Some((_, entries)) => entries.push(entry), + None => self.0.push((name.to_string(), vec![entry])), + } + } + + /// Entry `i` of stream `name`, for a test to forge. + #[cfg(test)] + pub(super) fn entry_mut(&mut self, name: &str, i: usize) -> &mut Vec { + let (_, entries) = self.0.iter_mut().find(|(n, _)| n == name).expect("a hinted stream"); + &mut entries[i] + } + + pub(super) fn install(self, guest: &mut Program) { + for (name, entries) in self.0 { + guest.set_witness(name, entries); + } + } +} + +/// One NiceTry SPHINCS- v2 signature's witness (prototype): the randomizer, each FORS +/// tree's secret and path, then per layer the WOTS digits (in the exponent), the chain +/// values they start from, and the key's path. +pub(super) fn lean_sphincs_hints( + pk: &LeanSphincsPublicKey, + sig: &LeanSphincsSignature, + message: &lean_sphincs::Message, +) -> Result { + let trace = lean_sphincs::verify_trace(pk, message, sig); + // Test builds only: the switch hands the guest an invalid signature, to see it fault there. + let unchecked = cfg!(feature = "forge") && std::env::var_os("EIP8288_NO_HOST_SIGCHECK").is_some(); + if trace.signed[lean_sphincs::D] != pk.root && !unchecked { + return Err(AggregationError::MalformedRawSignature); + } + let mut hints: SubHints = vec![("sp_rand", vec![pack_16_bytes(&sig.randomizer)])]; + for (secret, path) in sig.fors_secrets.iter().zip(&sig.fors_paths) { + hints.push(("sp_fors_secrets", vec![pack_16_bytes(secret)])); + for sibling in path { + hints.push(("sp_fors_paths", vec![pack_16_bytes(sibling)])); + } + } + for (layer, digits) in sig.layers.iter().zip(&trace.digits) { + for (start, &digit) in layer.chains.iter().zip(digits) { + hints.push(("sp_digits", vec![count(digit as usize)])); + hints.push(("sp_chain_starts", vec![pack_16_bytes(start)])); + } + for sibling in &layer.path { + hints.push(("sp_siblings", vec![pack_16_bytes(sibling)])); + } + } + Ok(hints) +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/mod.rs b/src/ffi/crates/rec_aggregation/src/aggregation/mod.rs new file mode 100644 index 0000000..600a9ac --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/mod.rs @@ -0,0 +1,80 @@ +//! Recursive proofs of leanSTARK (`0x11`) and leanSPHINCS (`0x10`) dependency claims and LeanDA blob +//! well-formedness. One bytecode (`guests/lean_ethereum.py`) serves every node of an aggregation tree. +//! +//! A node verifies `n_raw_generic` leanVM execution proofs of other programs ([`verify_generic`]), +//! `n_raw_lean_sphincs` leanSPHINCS signatures and `n_children` sub-proofs **of this same bytecode**, +//! and by default publishes the sorted deduplicated union of their claim sets. A leanSTARK claim is +//! a `(data_hash, vk)` pair, a leanSPHINCS one a `(message, pubkey)` pair. Coverage is what carries the +//! security claim: a write-once slot per declared claim, written once by each raw +//! signature and each child claim, plus a final count, so every declared claim is +//! backed by a real signature or a verified child. +//! +//! Those slots are one contiguous region per scheme, so the one range check a write +//! already needs also keeps a signature off the other scheme's declared claims: that +//! is what makes the published split mean which scheme verified which claim, at +//! every level of the tree. +//! +//! A duplicate slot sits outside the prefix the digest hashes, so a claim in one is +//! covered and not published. `aggregate`'s `declare` rests on that. A slot holds +//! four cells, the pubkey and its message, so the guest reads each signature's message +//! out of the slot it verifies. +//! +//! The bytecode is compiled to a fixed point on its own size +//! ([`unified_guest`]): the recursion placeholders depend on the inner bytecode +//! size, and here the inner bytecode is this one. Its digest does not need a +//! fixed point, riding the statement instead of the code. +//! +//! One fixed polynomial, the stacked bytecode, is too +//! big to evaluate in-circuit, so each node exports one deferred claim on it +//! and batches its children's carried claims with the fresh ones its +//! verifications raise (`doc/leanvm/main.tex` §Deferred evaluation claims). Only +//! the root's is discharged natively, by [`EthereumProof::verify`]. +//! +//! `gen_verify` derives the guest's whole witness for a child from the real +//! `cpu::layout` of the inner program and the summary the `verifier` crate +//! reads out of the child's proof, so there is no hand-mirrored copy of the +//! protocol to drift. + +mod cells; +mod child; +mod claim_pool; +mod coverage; +mod deferred; +mod error; +mod generic; +mod guest; +mod hints; +mod node; +mod placeholders; +mod proof; +mod statement; +#[cfg(test)] +mod generic_tests; +#[cfg(test)] +mod tests; + +pub use error::{AggregateVerifyError, AggregationError, SnarkProveError, SnarkVerifyError}; +pub use generic::{ + LEANSTARK_DEPS_DOMAIN, MAX_GENERIC_BYTECODE_LOG, MAX_GENERIC_DEPENDENCIES, generic_verification_key, + generic_witness, verify_generic, +}; +pub use guest::{unified_guest, warm_up}; +pub use node::{aggregate, aggregate_with_stats}; +pub use proof::{EthereumClaims, EthereumProof, aggregated_vk, deps_hash, deps_public_input_bytes, verify_deps_hash}; +use signatures::lean_sphincs; +use signatures::lean_sphincs::LeanSphincsPublicKey; + +/// One leanSTARK (`0x11`) claim, an EIP-8288 dependency without its scheme word: `data_hash`, +/// then the program's verification key ([`generic_verification_key`]), in the EIP's bytewise order. +pub type GenericClaim = ([u8; 32], [u8; 32]); + +/// One raw leanSTARK dependency: `data_hash`, the key, and the witness ([`generic_witness`]). +pub type RawGeneric = ([u8; 32], [u8; 32], Vec); + +/// One leanSPHINCS claim, an EIP-8288 dependency without its scheme word: `data_hash`, then +/// the key (`pkRoot ‖ pkSeed`). The tuple's order is the EIP's bytewise order of the 64-byte entry. +pub type LeanSphincsClaim = (lean_sphincs::Message, LeanSphincsPublicKey); + +/// The most earlier aggregates one [`aggregate`] call can take, so the arity of +/// an aggregation tree. +pub const MAX_RECURSIONS: usize = 16; diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/node.rs b/src/ffi/crates/rec_aggregation/src/aggregation/node.rs new file mode 100644 index 0000000..50d57d2 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/node.rs @@ -0,0 +1,375 @@ +//! The entry points that prove one node of an aggregation tree. + +use super::cells::count; +use super::child::gen_verify; +use super::coverage::plan_coverage; +use super::deferred::{DeferredClaim, aggregate_deferred_claims, bytecode_sums, bytecode_value, scalar_bytecode}; +use super::error::{AggregateVerifyError, AggregationError, SnarkProveError, SnarkVerifyError}; +use super::guest::unified_guest; +#[cfg(test)] +use super::hints::FORGE; +use super::generic::{MAX_GENERIC_DEPENDENCIES, generic_hints}; +use super::hints::{Hints, lean_sphincs_hints}; +use super::proof::{EthereumClaims, EthereumProof, check_signer_set}; +use super::statement::{generic_claim_cells, lean_sphincs_claim_cells, list_split, statement_digest}; +use super::{GenericClaim, LeanSphincsClaim, MAX_RECURSIONS, RawGeneric}; +use fiat_shamir::merkle::hash_to_scalars; +use lean_da::{BLOB_SYMBOLS, CELL_SYMBOLS, CELLS_PER_ROW, CODEWORD_SYMBOLS}; +use leanvm_core::cpu::{from_scalar, prove, public_input_bytes, scalar_proof}; +use primitives::field::{F64, F192}; +use signatures::lean_sphincs; +use signatures::lean_sphincs::{LeanSphincsPublicKey, LeanSphincsSignature}; + +/// Test builds only: whether this thread lifts the host's leanSTARK cap, so the guest's own is tested. +fn allow_many_generic() -> bool { + #[cfg(test)] + return super::hints::ALLOW_MANY_GENERIC.with(std::cell::Cell::get); + #[cfg(not(test))] + false +} + +/// Prove existence of signatures and valid encoding of PQ-blobs, potentially using recursive children. +/// +/// - `children`: child proofs; at most [`MAX_RECURSIONS`]. +/// - `raw_generic`: leanSTARK (`0x11`) dependencies `(data_hash, vk, witness)` +/// ([`generic_witness`](super::generic_witness)), any order; at most +/// [`MAX_GENERIC_DEPENDENCIES`] distinct ones. +/// - `raw_lean_sphincs`: list of `(public_key, message, signature)`, any order. +/// - `blobs`: concatenated blobs, each containing [`BLOB_SYMBOLS`] little-endian `u64` symbols; +/// at most [`lean_da::DA_MAX_ROWS`] blobs. A node with blobs publishes their matrix's root, and none +/// of its children's. +/// - `declare`: `None` keeps all signature claims and publishes the blob matrix's root, or else +/// the first child's that has one; `Some` specifies exactly the signatures and DA root to +/// publish, which must be the blob matrix's if there are blobs. Every declared claim must be +/// covered by the inputs above. +/// - `log_inv_rate`: PCS code rate `2^-log_inv_rate`, higher `log_inv_rate` means a smaller proof +/// but slower proving; in [`MIN_LOG_INV_RATE`](pcs::whir::MIN_LOG_INV_RATE)..=[`MAX_LOG_INV_RATE`](pcs::whir::MAX_LOG_INV_RATE). +/// +/// IMPORTANT: +/// - `aggregate` should not be called more than once at a time in parallel per process. +/// - Raw signatures are assumed valid: one that does not decode returns +/// [`AggregationError::MalformedRawSignature`], and any other invalid one fails the guest's run, +/// [`AggregationError::ProofError`]. A raw leanSTARK witness is checked natively first +/// ([`verify_generic`](super::verify_generic)): one that fails returns +/// [`AggregationError::InvalidGenericWitness`]. +/// - Children are assumed verified by the caller ([`EthereumProof::verify`]): a child the replay +/// cannot walk, or one carrying a false deferred value, returns [`AggregationError::InvalidChild`], +/// and any other invalid child fails the guest's run, [`AggregationError::ProofError`] (TODO +/// return the index of invalid proof). +pub fn aggregate( + children: &[EthereumProof], + raw_generic: Vec, + raw_lean_sphincs: Vec<(LeanSphincsPublicKey, lean_sphincs::Message, LeanSphincsSignature)>, + blobs: &[u64], + declare: Option<&EthereumClaims>, + log_inv_rate: usize, +) -> Result { + aggregate_with_stats(children, raw_generic, raw_lean_sphincs, blobs, declare, log_inv_rate).map(|(sig, _)| sig) +} + +/// [`aggregate`], under the same contract, also returning the prover's statistics (what the CLI's benchmarks report). +pub fn aggregate_with_stats( + children: &[EthereumProof], + raw_generic: Vec, + raw_lean_sphincs: Vec<(LeanSphincsPublicKey, lean_sphincs::Message, LeanSphincsSignature)>, + blobs: &[u64], + declare: Option<&EthereumClaims>, + log_inv_rate: usize, +) -> Result<(EthereumProof, leanvm_core::cpu::Stats), AggregationError> { + let declared_root = declare.map(|claims| claims.da_commitment); + // `cpu::prove` checks the rate too, but only after the witness is built. + if !(leanvm_core::pcs::MIN_LOG_INV_RATE..=leanvm_core::pcs::MAX_LOG_INV_RATE).contains(&log_inv_rate) { + return Err(AggregationError::InvalidRate { log_inv_rate }); + } + if children.len() > MAX_RECURSIONS { + return Err(AggregationError::TooLarge); + } + let direct = if blobs.is_empty() { + None + } else { + let _span = tracing::info_span!("LeanDA commit").entered(); + Some(lean_da::commit(blobs)?) + }; + let direct_root = direct.as_ref().map(|&(commitment, _)| commitment); + if direct_root.is_some() && declared_root.is_some_and(|declared| declared != direct_root) { + return Err(AggregationError::BlobsUnpublished); + } + // The published root and the guest's slot for it: a child's, or `children.len()` for + // the node's own, its blobs' or none. + let (da_pick, da_root) = match declared_root { + _ if direct_root.is_some() => (children.len(), direct_root), + None => children + .iter() + .position(|child| child.claims.da_commitment.is_some()) + .map_or((children.len(), None), |i| (i, children[i].claims.da_commitment)), + Some(None) => (children.len(), None), + Some(Some(root)) => { + let i = children + .iter() + .position(|child| child.claims.da_commitment == Some(root)) + .ok_or(AggregationError::BlobNotCovered)?; + (i, Some(root)) + } + }; + + let guest = unified_guest(); + // On the whole claim: one claim needs one proof. + let mut raw_generic = raw_generic; + raw_generic.sort_by(|(a, ak, _), (b, bk, _)| (a, ak).cmp(&(b, bk))); + raw_generic.dedup_by(|(a, ak, _), (b, bk, _)| (a, ak) == (b, bk)); + let mut raw_lean_sphincs = raw_lean_sphincs; + // Test builds only: `EIP8288_FORGE` plays a malicious host, to show what the guest does not enforce. + let forge = if cfg!(feature = "forge") { std::env::var("EIP8288_FORGE").unwrap_or_default() } else { String::new() }; + if raw_generic.len() > MAX_GENERIC_DEPENDENCIES && !allow_many_generic() { + return Err(AggregationError::TooManyGeneric); + } + raw_lean_sphincs.sort_by_key(|(pk, message, _)| (*message, *pk)); + if forge != "dup" { + raw_lean_sphincs.dedup_by(|(a, am, _), (b, bm, _)| (a, am) == (b, bm)); + } + + // The children are assumed checked. Nothing here verifies one: the replay + // only reads out of its proof the summary `gen_verify` derives the guest's + // witness from, pinning the Merkle paths it reads, and fails on a proof it + // cannot walk. A child that does not + // verify fails in the guest instead, which verifies every child in the VM, + // so `prove` below returns the error. Its deferred claim is likewise not + // recomputed: the batching sumcheck below forces every batched value to be + // the true evaluation, refusing a false one, and the root discharges the one + // claim they reduce to. + let _span = tracing::info_span!("Replay children").entered(); + let mut public_inputs = Vec::with_capacity(children.len()); + let child_da: Vec<[u8; 32]> = children + .iter() + .map(|child| verifier::da::digest(child.claims.da_commitment)) + .collect(); + for child in children { + check_signer_set(&child.claims).map_err(AggregationError::InvalidChild)?; + public_inputs.push(child.vm_public_input()); + } + // One single-threaded replay per worker: nothing inside dispatches. + let bytecode = scalar_bytecode(); + let summaries = parallel::map_collect(children.len(), |i| { + let public_input = public_input_bytes(&public_inputs[i]); + verifier::replay_unchecked(bytecode, &public_input, &scalar_proof(&children[i].proof)) + }); + let mut replayed = Vec::with_capacity(children.len()); + for (pi, summary) in public_inputs.into_iter().zip(summaries) { + let summary = + summary.map_err(|e| AggregationError::InvalidChild(AggregateVerifyError::Snark(SnarkVerifyError(e))))?; + replayed.push((pi, summary)); + } + + drop(_span); + + let _span = tracing::info_span!("Build witness").entered(); + let raw_generic_claims: Vec = raw_generic.iter().map(|(data_hash, vk, _)| (*data_hash, *vk)).collect(); + let raw_lean_sphincs_claims: Vec = raw_lean_sphincs + .iter() + .map(|(pk, message, _)| (*message, *pk)) + .collect(); + let mut cover = plan_coverage(&raw_generic_claims, &raw_lean_sphincs_claims, children, declare)?; + if cover.generic.signers.len() > MAX_GENERIC_DEPENDENCIES && !allow_many_generic() { + return Err(AggregationError::TooManyGeneric); + } + match forge.as_str() { + // Publish the duplicate slots too: the declared list repeats a claim. + "dup" => { + let region = &mut cover.lean_sphincs; + region.signers.append(&mut region.dups); + } + // Publish the first two claims out of order. + "swap" => { + let region = &mut cover.lean_sphincs; + region.signers.swap(0, 1); + for slot in &mut region.raw { + *slot = [1, 0].get(*slot).copied().unwrap_or(*slot); + } + } + _ => {} + } + + let mut hints = Hints::default(); + hints.push( + "meta", + vec![ + count(cover.generic.signers.len()), + count(cover.generic.dups.len()), + count(raw_generic.len()), + count(cover.lean_sphincs.signers.len()), + count(cover.lean_sphincs.dups.len()), + count(raw_lean_sphincs.len()), + count(children.len()), + count(usize::from(!blobs.is_empty())), + ], + ); + let fs_seed = leanvm_core::cpu::fs_seed(guest); + hints.push("fs_seed", vec![fs_seed[0], fs_seed[1]]); + // Both lists in the guest's order, leanSTARK first: each one's split, its declared + // claims, then its duplicates. + let generic_cells: Vec<[F192; 4]> = cover.generic.signers.iter().map(generic_claim_cells).collect(); + let lean_sphincs_cells: Vec<[F192; 4]> = cover + .lean_sphincs + .signers + .iter() + .map(lean_sphincs_claim_cells) + .collect(); + for cells in [&generic_cells, &lean_sphincs_cells] { + if !cells.is_empty() { + hints.push("signers_split", list_split(cells.len())); + } + for claim in cells { + hints.push("claims", claim.to_vec()); + } + } + let dups = cover.generic.dups.iter().map(generic_claim_cells); + for claim in dups.chain(cover.lean_sphincs.dups.iter().map(lean_sphincs_claim_cells)) { + hints.push("dup_claims", claim.to_vec()); + } + // Each slot is hinted as an offset into its scheme's region, which is how one + // range check keeps a scheme's writers off the other's claims. + // A leanSTARK sub-proof pops the same streams as a child's, ahead of every child's. + // Sequential: the native verification inside dispatches on the pool itself. + let generic_hints: Vec<_> = + raw_generic.iter().map(|(data_hash, vk, witness)| generic_hints(data_hash, vk, witness)).collect(); + for (&offset, sub) in cover.generic.raw.iter().zip(generic_hints) { + hints.push("raw_index", vec![count(offset)]); + for (name, entry) in sub? { + hints.push(name, entry); + } + } + let lean_sphincs_hints = parallel::map_collect(raw_lean_sphincs.len(), |i| { + let (pk, message, sig) = &raw_lean_sphincs[i]; + lean_sphincs_hints(pk, sig, message) + }); + for (&offset, sub) in cover.lean_sphincs.raw.iter().zip(lean_sphincs_hints) { + hints.push("sp_raw_index", vec![count(offset)]); + for (name, entry) in sub? { + hints.push(name, entry); + } + } + + // Both claims of each child, in the batch's order: the fresh one, then the one it carried. The fresh + // claim's value is read off the sums the batch runs on. + let claim_points: Vec> = replayed + .iter() + .zip(children) + .flat_map(|((_, summary), child)| { + let fresh = summary.bytecode_point.iter().copied().map(from_scalar).collect(); + [fresh, child.defer.bytecode_point.clone()] + }) + .collect(); + let sums = bytecode_sums(&claim_points); + // Each child's witness is its own, one child per worker: nothing inside dispatches. + let verified = parallel::map_collect(children.len(), |i| { + let (pi, summary) = &replayed[i]; + let value = bytecode_value(&sums[2 * i], &claim_points[2 * i]); + gen_verify(guest, *pi, &children[i].proof, summary, value) + }); + let mut subs = Vec::with_capacity(children.len()); + let mut carried = Vec::with_capacity(children.len()); + for (i, (child, verified)) in children.iter().zip(verified).enumerate() { + hints.push( + "child_meta", + vec![count(child.claims.generic.len()), count(child.claims.lean_sphincs.len())], + ); + for offsets in [&cover.generic.children[i], &cover.lean_sphincs.children[i]] { + if offsets.is_empty() { + continue; + } + hints.push("signers_split", list_split(offsets.len())); + // Strictly increasing slots go as gaps (`child_claim_list_mono`). + if offsets.is_sorted_by(|a, b| a < b) { + hints.push("child_mono", vec![count(1)]); + let mut next = 0; + for &offset in offsets { + hints.push("child_claim_gap", vec![count(offset - next)]); + next = offset + 1; + } + } else { + hints.push("child_mono", vec![count(0)]); + for &offset in offsets { + hints.push("child_claim_index", vec![count(offset)]); + } + } + } + hints.push("child_defer", child.defer.cells()); + hints.push("child_da", hash_to_scalars(&child_da[i]).to_vec()); + let (sub_hints, defer) = verified?; + for (name, entry) in sub_hints { + hints.push(name, entry); + } + subs.push(defer); + carried.push(&child.defer); + } + + drop(_span); + + let defer = if children.is_empty() { + let leaf = DeferredClaim::leaf(); + hints.push("leaf_defer", vec![leaf.bytecode_value]); + leaf + } else { + let _span = tracing::info_span!("Batch deferred claims").entered(); + let (agg_hints, reduced) = + aggregate_deferred_claims(&subs, &carried, sums).map_err(AggregationError::InvalidChild)?; + for (name, entry) in agg_hints { + hints.push(name, entry); + } + reduced + }; + + let mut da = child_da.get(da_pick).copied().unwrap_or_default(); + if let Some((commitment, witness)) = direct { + let n_rows = blobs.len() / BLOB_SYMBOLS; + da = verifier::da::digest(Some(commitment)); + hints.push( + "da_challenge", + vec![from_scalar(verifier::da::membership_challenge(&commitment))], + ); + hints.push( + "da_shape", + vec![count(n_rows), count(n_rows.next_power_of_two().ilog2() as usize)], + ); + // Padding rows are constants the guest bakes, so only the real rows' + // symbols ride the stream. + let (c, m) = (CELL_SYMBOLS, CODEWORD_SYMBOLS); + for j in 0..CELLS_PER_ROW { + for i in 0..n_rows { + hints.push( + "da_symbols", + witness.codewords[i * m + j * c..i * m + (j + 1) * c] + .iter() + .map(|&w| F192::from(F64(w))) + .collect(), + ); + } + } + } + hints.push("da_pick", vec![count(da_pick)]); + if cover.generic.signers.is_empty() && cover.lean_sphincs.signers.is_empty() && da_root.is_none() { + return Err(AggregationError::Empty); + } + + let public_input = statement_digest(&cover.generic.signers, &cover.lean_sphincs.signers, da, &defer); + let mut program = guest.clone(); + #[cfg(test)] + if let Some(forge) = FORGE.with(|forge| forge.borrow_mut().take()) { + forge(&mut hints); + } + hints.install(&mut program); + let (proof, stats) = + prove(&program, public_input, log_inv_rate).map_err(|e| AggregationError::ProofError(SnarkProveError(e)))?; + Ok(( + EthereumProof { + claims: EthereumClaims { + generic: cover.generic.signers, + lean_sphincs: cover.lean_sphincs.signers, + da_commitment: da_root, + }, + defer, + proof, + }, + stats, + )) +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/placeholders.rs b/src/ffi/crates/rec_aggregation/src/aggregation/placeholders.rs new file mode 100644 index 0000000..d20d5b4 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/placeholders.rs @@ -0,0 +1,932 @@ +//! The constants baked into the guest. + +use super::MAX_RECURSIONS; +use super::cells::{dsl_u128, pack_16_bytes}; +use super::child::{ + KBC_SOURCE, MU_CAP, QUERY_WINDOW_BITS, QUERY_WINDOWS_CAP, STREAM_CAP, block_groups, block_sources, col_sources, col_groups, merkle_cap_depth, whir_shape, +}; +use super::claim_pool::{ClaimSite, coord_difference, coord_kind, coord_scale, push_coord_terms, walk_claims}; +use super::deferred::RECURSION_AGG_LABEL; +use super::generic::{LEANSTARK_DEPS_DOMAIN, MAX_GENERIC_BYTECODE_LOG, MAX_GENERIC_DEPENDENCIES}; +use super::statement::{BIG_GROUPS, CLAIM_GROUP, STATEMENT_HEADER}; +use fiat_shamir::merkle::hash_to_scalars; +use lean_compiler::f192_literal; +use lean_da::{CELLS_PER_ROW, DA_LOG_CELL, DA_LOG_K, DA_MAX_ROWS}; +use leanvm_core::constraints::xi_offsets; +use leanvm_core::cpu::{KeccakLane, keccak_value}; +use leanvm_core::leaf::{Block, Coord}; +use leanvm_core::tables::N_TABLES; +use leanvm_core::transcript::{FiatShamirState, RATE_SCALARS}; +use pcs::whir::{MAX_LOG_INV_RATE, MIN_LOG_INV_RATE}; +use primitives::field::{F64, F192, g_pow}; +use signatures::lean_sphincs; +use std::collections::BTreeMap; +use std::ops::Range; + +struct CoordinateDescriptor { + kind: usize, + constant: u128, + /// Its value on its table's padding row; zero for a framework coordinate. + padding: u64, + fresh: usize, + claim_slot: usize, + terms: Range, +} + +struct ClaimDescriptor { + buffer: usize, + column: usize, + qkeccak_slot: usize, + /// The kappa source `(src, adj)` of the claim's low dimension. + source: (usize, usize), +} + +// The guest's `POINT_BUF_*`, the point a pooled claim is made at: the bus's `zeta`, the table +// sumcheck's point, the public input's, that point on a slot of `q_keccak`, and none for an +// output lane's, which the Keccak reduction takes as its claim on the permutation's output and +// the opening never sees. +const POINT_BUF_ZETA: usize = 0; +const POINT_BUF_RHO: usize = 1; +const POINT_BUF_PI: usize = 2; +const POINT_BUF_QKECCAK_RHO: usize = 3; +const POINT_BUF_KECCAK_OUT: usize = 4; + +fn literals(values: impl IntoIterator) -> String { + format!( + "[{}]", + values.into_iter().map(|v| v.to_string()).collect::>().join(", ") + ) +} + +fn flds(v: &[F192]) -> String { + format!( + "[{}]", + v.iter().map(|&x| f192_literal(x)).collect::>().join(", ") + ) +} + +/// A greedy (Paar) straight-line program computing each row's sum, or product where a row repeats an input: +/// value `inputs + k` is op `k`, the sum or product of two earlier values, the pair most rows share first. +/// Returns the ops and the value holding each (nonempty) row. +fn straight_line(rows: &[Vec], inputs: usize) -> (Vec<(usize, usize)>, Vec) { + let mut rows: Vec> = rows.to_vec(); + let mut ops = Vec::new(); + let pairs = |row: &Vec| { + let mut out = std::collections::BTreeSet::new(); + for (i, &a) in row.iter().enumerate() { + for &b in &row[i + 1..] { + out.insert((a.min(b), a.max(b))); + } + } + out + }; + loop { + let mut counts = std::collections::BTreeMap::<(usize, usize), usize>::new(); + for row in &rows { + for pair in pairs(row) { + *counts.entry(pair).or_default() += 1; + } + } + // The most shared pair, the first of them; once none is shared, any pair left. + let Some((&(a, b), _)) = counts.iter().rev().max_by_key(|&(_, &n)| n) else { + break; + }; + let new = inputs + ops.len(); + ops.push((a, b)); + for row in &mut rows { + if let Some(i) = row.iter().position(|&v| v == a) { + row.remove(i); + if let Some(j) = row.iter().position(|&v| v == b) { + row.remove(j); + row.push(new); + } else { + row.insert(i, a); + } + } + } + } + let outputs = rows + .iter() + .map(|row| { + assert_eq!(row.len(), 1, "a straight-line row is nonempty"); + row[0] + }) + .collect(); + (ops, outputs) +} + +/// Binary polynomials: the quotient and the remainder of `a` by `m`. +fn poly_divmod(mut a: u64, m: u64) -> (u64, u64) { + let dm = m.ilog2(); + let mut q = 0; + while a != 0 && a.ilog2() >= dm { + let shift = a.ilog2() - dm; + q |= 1 << shift; + a ^= m << shift; + } + (q, a) +} + +/// `p`'s irreducible factors, with multiplicity, smallest first. +fn poly_factors(mut p: u64) -> Vec { + let mut factors = Vec::new(); + let mut q = 2u64; + while p > 1 && 64 - q.leading_zeros() <= (64 - p.leading_zeros()) { + let (quotient, rem) = poly_divmod(p, q); + if rem == 0 { + factors.push(q); + p = quotient; + } else { + q += 1; + } + } + factors +} + +struct OpeningShape { + n_levels: usize, + yr_level: usize, + yr_log_len: usize, + folds: Vec, + log_message_columns: Vec, + queries: Vec, + tree_depths: Vec, + positions_per_squeeze: Vec, + squeezes: Vec, + interleaving: Vec, + query_grinding_bits: Vec, + cap_depths: Vec, + fold_offsets: Vec, + residual_fold_offsets: Vec, + ood_samples: Vec, +} + +/// The recursion program's placeholder map depends on table structure and bytecode +/// size, so one compiled guest serves every proof layout. +pub(super) fn placeholder_map(kbc: usize) -> BTreeMap { + // Only block and coordinate structure is used here; dummy instructions and + // table sizes let us derive it before the guest's bytecode exists. + let stand_in = vec![leanvm_core::cpu::Op::Xor { a: 0, b: 0, c: 0 }; 1 << kbc]; + let layout = leanvm_core::cpu::layout(&stand_in, 20, [1 << 10; N_TABLES], [F192::ZERO, F192::ZERO]); + + let mut rep = BTreeMap::new(); + let mut ps = |k: &str, v: String| { + rep.insert(format!("{k}_PLACEHOLDER"), v); + }; + ps("STREAM_CAP", STREAM_CAP.to_string()); + ps("KBC_SRC", KBC_SOURCE.to_string()); + ps("QW", QUERY_WINDOW_BITS.to_string()); + ps("QUERY_WINDOWS_CAP", QUERY_WINDOWS_CAP.to_string()); + ps("MIN_LOG_MEM", verifier::MIN_LOG_MEMORY.to_string()); + ps("MAX_LOG_MEM", verifier::MAX_LOG_MEMORY.to_string()); + ps("INV_GEN", dsl_u128(F192::from(F64::G.inv())).to_string()); + ps("MU_CAP", MU_CAP.to_string()); + ps("NO_TABLE", N_TABLES.to_string()); + ps("GKR_ROUNDS_CAP", (MU_CAP * (MU_CAP + 1) / 2 + MU_CAP + 2).to_string()); + ps("GKR_POINTS_CAP", ((MU_CAP + 1) * MU_CAP).to_string()); + let (nclaims, nbcv) = bus_placeholders(&layout, kbc, &mut ps); + claim_placeholders(&layout, nclaims, &mut ps); + table_placeholders(&mut ps); + keccak_placeholders(&mut ps); + field_placeholders(&mut ps); + opening_placeholders(&mut ps); + bytecode_placeholders(kbc, nbcv, &mut ps); + da_placeholders(&mut ps); + signature_placeholders(&mut ps); + rep +} + +/// The bus blocks and their coordinates and terms. Returns the bus claim count and the public coordinates' count. +fn bus_placeholders( + layout: &leanvm_core::cpu::Layout, + kbc: usize, + ps: &mut impl FnMut(&str, String), +) -> (usize, usize) { + let sides: [&[Block]; 3] = [&layout.push, &layout.pull, &layout.count]; + + // ---- flattened block/coord descriptors (structural) ---- + let mut sblk = vec![0usize]; + let mut block_coords = Vec::new(); + let mut coordinates = Vec::new(); + let mut terms = Vec::new(); + let (mut nclaims, mut nbcv, mut nblocks) = (0usize, 0usize, 0usize); + // Claim dedup (mirrors leaf.rs): per coord, fresh = first (group, col, + // kappa) occurrence gets the next pool slot; duplicates point at it. + let mut slot_of: std::collections::HashMap<(usize, usize), usize> = Default::default(); + // A TABLE block's coordinates, flattened into terms: the guest rebuilds each as + // `Σ_terms`, so a derived value (an XOR/MUL result, a DEREF store, a JUMP + // successor) costs terms rather than columns. A framework coordinate has none: + // it decomposes into pooled claims instead. + // The table sumcheck settles table claims; only framework blocks stream column values. + let sch_pm = leanvm_core::cpu::schema(); + let owner_pm: Vec> = block_sources() + .into_iter() + .map(|(src, _)| src.checked_sub(2).filter(|&t| t < N_TABLES)) + .collect(); + for blocks in sides.iter() { + for blk in blocks.iter() { + block_coords.push(coordinates.len()..coordinates.len() + blk.coords.len()); + let owner = owner_pm[nblocks]; + nblocks += 1; + for (i, c) in blk.coords.iter().enumerate() { + // One COORD_FRESH/COORD_CLAIM_SLOT entry PER coord (the guest + // indexes them by global coord offset); only a framework block's + // Col/GCol raises a claim. + let (mut fresh, mut slot) = (0usize, 0usize); + if let (Coord::Col(i) | Coord::GCol(i, _), None) = (c, owner) { + let key = (*i, blk.kappa); + if let Some(&known) = slot_of.get(&key) { + slot = known; + } else { + slot_of.insert(key, nclaims); + fresh = 1; + slot = nclaims; + nclaims += 1; + } + } + let start = terms.len(); + if let Some(t) = owner { + push_coord_terms(c, sch_pm.base[t], &mut terms); + } + nbcv += usize::from(matches!(c, Coord::Public(_))); + coordinates.push(CoordinateDescriptor { + kind: coord_kind(c), + constant: dsl_u128(coord_scale(c)), + padding: blk.padding.get(i).map_or(0, |v| v.0), + fresh, + claim_slot: slot, + terms: start..terms.len(), + }); + } + } + sblk.push(nblocks); + } + ps("SIDE_BLOCK_START", literals(&sblk)); + ps("N_BLOCKS", nblocks.to_string()); + let bks = block_sources(); + // Push and pull emit bus blocks in matched pairs, so their baked kappa-source + // segments are identical; the guest computes only push's side total and + // aliases pull's mu to push's on this basis. + assert_eq!( + bks[sblk[0]..sblk[1]], + bks[sblk[1]..sblk[2]], + "push/pull kappa sources must match" + ); + // The guest also reuses push's padding term for pull, which needs the same padding + // tuples (a padding row's counts are zero, so a count's `g·` changes nothing). + assert!( + layout + .push + .iter() + .zip(&layout.pull) + .all(|(a, b)| a.padding == b.padding), + "push/pull padding tuples must match" + ); + ps("BLOCK_KAPPA_SRC", literals(bks.iter().map(|&(s, _)| s))); + let bgroups = block_groups(&bks, &sblk); + ps("N_BLOCK_GROUPS", bgroups.len().to_string()); + ps( + "BLOCK_GROUP_START", + literals(bgroups.iter().copied().chain([bks.len()])), + ); + ps( + "SIDE_GROUP_START", + literals(sblk.iter().map(|&b| bgroups.partition_point(|&start| start < b))), + ); + ps("BLOCK_KAPPA_ADJ", literals(bks.iter().map(|&(_, a)| a))); + ps( + "BLOCK_TABLE", + literals(bks.iter().map(|&(s, _)| if (2..2 + N_TABLES).contains(&s) { s - 2 } else { N_TABLES })), + ); + let mut block_side = Vec::new(); + for (s, blocks) in sides.iter().enumerate() { + block_side.extend(std::iter::repeat_n(s, blocks.len())); + } + ps("BLOCK_SIDE", literals(&block_side)); + ps("BLOCK_COORD_OFF", literals(block_coords.iter().map(|r| r.start))); + ps("BLOCK_COORD_COUNT", literals(block_coords.iter().map(|r| r.len()))); + ps("COORD_TYPE", literals(coordinates.iter().map(|c| c.kind))); + // The pull boundary's final pc `g^(2^kbc - 1)` is the one constant that depends on the inner + // program: the guest reads it from its runtime bytecode log instead. + let final_pc = dsl_u128(F192::from(g_pow((1 << kbc) - 1))); + let final_pc_coord: Vec = (0..coordinates.len()) + .filter(|&c| coordinates[c].kind == coord_kind(&Coord::Const(F64::ONE)) && coordinates[c].constant == final_pc) + .collect(); + assert_eq!(final_pc_coord.len(), 1, "one final pc coordinate"); + ps("FINAL_PC_COORD", final_pc_coord[0].to_string()); + ps("COORD_CONST", literals(coordinates.iter().map(|c| c.constant))); + ps("COORD_PADDING", literals(coordinates.iter().map(|c| c.padding))); + ps("COORD_FRESH", literals(coordinates.iter().map(|c| c.fresh))); + // A fresh coordinate's place in the run of words the decomposition absorbs, which + // decides where it waits in the Fiat-Shamir sponge. + ps( + "COORD_FRESH_POS", + literals(xi_offsets(coordinates.iter().map(|c| c.fresh))), + ); + ps( + "N_FRESH", + coordinates.iter().map(|c| c.fresh).sum::().to_string(), + ); + ps("COORD_CLAIM_SLOT", literals(coordinates.iter().map(|c| c.claim_slot))); + ps("COORD_TERM_OFF", literals(coordinates.iter().map(|c| c.terms.start))); + ps("COORD_TERM_COUNT", literals(coordinates.iter().map(|c| c.terms.len()))); + // A pull table-block coordinate's difference from its push twin's, as merged terms + // appended after every coordinate's own: the guest counts a pull block's form as + // push's plus these, the two blocks sharing eq_hi. + let (mut diff_off, mut diff_count) = (vec![0usize; coordinates.len()], vec![0usize; coordinates.len()]); + for b in sblk[1]..sblk[2] { + let (pull, push) = (block_coords[b].clone(), block_coords[b - sblk[1]].clone()); + assert!( + bks[b] == bks[b - sblk[1]] && pull.len() == push.len(), + "pull block {b} must mirror its push twin" + ); + for (p, q) in pull.zip(push) { + let merged = coord_difference( + &terms[coordinates[p].terms.clone()], + &terms[coordinates[q].terms.clone()], + ); + diff_off[p] = terms.len(); + diff_count[p] = merged.len(); + terms.extend(merged); + } + } + ps("COORD_DIFF_OFF", literals(&diff_off)); + ps("COORD_DIFF_COUNT", literals(&diff_count)); + ps("TERM_TYPE", literals(terms.iter().map(|t| t.kind))); + ps("TERM_CONST", literals(terms.iter().map(|t| t.constant))); + ps("TERM_COL_A", literals(terms.iter().map(|t| t.column_a))); + ps("TERM_COL_B", literals(terms.iter().map(|t| t.column_b))); + ps("N_BUS_CLAIMS", nclaims.to_string()); + let idxc: Vec = (0..34) + .map(|i| { + let mut g2k = F192::from(F64::G); + for _ in 0..i { + g2k = g2k * g2k; + } + dsl_u128(F192::ONE + g2k) + }) + .collect(); + ps("INDEX_MLE_FACTORS", literals(&idxc)); + (nclaims, nbcv) +} + +/// The claim pool's descriptors. +fn claim_placeholders( + layout: &leanvm_core::cpu::Layout, + nclaims: usize, + ps: &mut impl FnMut(&str, String), +) { + let evtot: usize = leanvm_core::tables::tables() + .iter() + .map(|t| t.n_committed_columns()) + .sum(); + let ncl = nclaims + evtot + 3; // bus + constraint + the three PI memory-limb claims + + // ---- claim descriptor buffer ids (structural) ---- + let col_sources_pm = col_sources(); + let mut compact_col_pm = vec![usize::MAX; col_sources_pm.len()]; + let mut n_committed = 0usize; + for (global, source) in col_sources_pm.iter().enumerate() { + if source.is_some() { + compact_col_pm[global] = n_committed; + n_committed += 1; + } + } + let qkeccak_compact = compact_col_pm[leanvm_core::cpu::QKECCAK]; + assert_ne!(qkeccak_compact, usize::MAX, "QKECCAK must be committed"); + let mut claims = Vec::new(); + walk_claims(layout, |site| { + let descriptor = match site { + ClaimSite::Framework { column, source } => { + let column = compact_col_pm[column]; + assert_ne!(column, usize::MAX, "framework claim must target a committed column"); + ClaimDescriptor { + buffer: POINT_BUF_ZETA, + column, + qkeccak_slot: 0, + source, + } + } + ClaimSite::TableColumn { column, table } => { + // A table column's claim is at its table's chi[..tau]. + let source = (2 + table, 0); + match keccak_value(column) { + None => ClaimDescriptor { + buffer: POINT_BUF_RHO, + column: compact_col_pm[column], + qkeccak_slot: 0, + source, + }, + Some(KeccakLane::Slot(slot)) => ClaimDescriptor { + buffer: POINT_BUF_QKECCAK_RHO, + column: qkeccak_compact, + qkeccak_slot: slot, + source, + }, + Some(KeccakLane::Output(_)) => ClaimDescriptor { + buffer: POINT_BUF_KECCAK_OUT, + column: qkeccak_compact, + qkeccak_slot: 0, + source, + }, + } + } + ClaimSite::MemoryLimb { column } => ClaimDescriptor { + buffer: POINT_BUF_PI, + column: compact_col_pm[column], + qkeccak_slot: 0, + source: (1, 0), + }, + }; + claims.push(descriptor); + }); + assert_eq!(claims.len(), ncl, "descriptor count == pool size"); + ps("N_CLAIMS", ncl.to_string()); + ps("CLAIM_POINT_BUF", literals(claims.iter().map(|c| c.buffer))); + ps("CLAIM_COMMITTED_COL", literals(claims.iter().map(|c| c.column))); + ps("CLAIM_KAPPA_SRC", literals(claims.iter().map(|c| c.source.0))); + ps("CLAIM_KAPPA_ADJ", literals(claims.iter().map(|c| c.source.1))); + let slot_log = leanvm_core::keccak::SLOT_LOG; + let cpqbits: Vec = claims + .iter() + .flat_map(|c| (0..slot_log).map(move |k| (c.qkeccak_slot >> k) & 1)) + .collect(); + // The output lanes' claims, consecutive in the pool: the reduction's claim on the output. + let out_pool = claims + .iter() + .position(|c| c.buffer == POINT_BUF_KECCAK_OUT) + .expect("the output lanes' claims"); + assert!((0..claims.len()).all(|j| (claims[j].buffer == POINT_BUF_KECCAK_OUT) + == (out_pool..out_pool + primitives::hash::STATE_LANES).contains(&j))); + ps("KECCAK_OUT_POOL", out_pool.to_string()); + ps("CLAIM_QKECCAK_SLOT_BITS", literals(&cpqbits)); + ps("QKECCAK_COMMITTED_COL", qkeccak_compact.to_string()); + ps("QKECCAK_VARS_CAP", (33 + slot_log).to_string()); +} + +/// The table sumcheck's tables and xi layout. +fn table_placeholders(ps: &mut impl FnMut(&str, String)) { + ps("N_TABLES", N_TABLES.to_string()); + ps("FLOORS", literals(leanvm_core::tables::TABLE_LOG_FLOORS)); + // The table sumcheck's xi layout, from the native verifier's own numbers: + // a disjoint range of identities per table, then THREE powers shared by every + // table, one per bus side. Sharing is what lets the target be derived from the + // three leaf claims instead of trusted (leanvm_core::cpu::xi_form_base). + let n_id: Vec = leanvm_core::tables::tables() + .iter() + .map(|t| t.n_constraints()) + .collect(); + let form_base = leanvm_core::cpu::xi_form_base(); + ps("ETA_OFFSET", literals(xi_offsets(n_id.iter().copied()))); + ps("ETA_FORM_BASE", form_base.to_string()); + ps("N_ETA_POWS", (form_base + 3).to_string()); + let committed: Vec = leanvm_core::tables::tables() + .iter() + .map(|t| t.n_committed_columns()) + .collect(); + ps("N_TABLE_COLS", literals(&committed)); + ps("TABLE_COL_BASE", literals(xi_offsets(committed.iter().copied()))); + ps("N_COL_EVALS", committed.iter().sum::().to_string()); + ps("TABLE_COLS_CAP", (committed.iter().max().unwrap() + 1).to_string()); +} + +/// The Keccak reduction (`keccak_gkr`), as index lists the guest unrolls. +fn keccak_placeholders(ps: &mut impl FnMut(&str, String)) { + use keccak_gkr::rmfe::{EMBED_BITS, PACK, PROD_BITS, RMFE}; + let shape = keccak_gkr::Shape::new(leanvm_core::keccak::MIN_STEPS, leanvm_core::keccak::DEPTH); + let (depth, segments) = (shape.depth(), shape.segments()); + // `starts[i]..starts[i + 1]` indexes row `i`'s entries of the flat list: each row's + // offset, then the end. + let lists = |rows: Vec>| { + let starts = xi_offsets(rows.iter().map(Vec::len).chain([0])); + (literals(starts), literals(rows.concat())) + }; + let bits = |word: u64, n: usize| (0..n).filter(|b| word >> b & 1 == 1).collect::>(); + assert!(segments > 1, "the guest opens a committed segment output"); + ps("KECCAK_DEPTH", depth.to_string()); + ps("KECCAK_SEGMENTS", segments.to_string()); + ps("KECCAK_SEG_LOG", shape.seg_log().to_string()); + ps("KECCAK_PACK", PACK.to_string()); + ps("KECCAK_EMBED_BITS", EMBED_BITS.to_string()); + ps("KECCAK_PROD_BITS", PROD_BITS.to_string()); + // Each product coefficient's reader, the sum of the instance weights whose projection reads + // it, as a straight line of sums over the PACK weights. + let (ops, outputs) = straight_line( + &(0..PROD_BITS) + .map(|c| (0..PACK).filter(|&i| RMFE.proj[i] >> c & 1 == 1).collect::>()) + .collect::>(), + PACK, + ); + ps("KECCAK_PROJ_A", literals(ops.iter().map(|op| op.0))); + ps("KECCAK_PROJ_B", literals(ops.iter().map(|op| op.1))); + ps("KECCAK_PROJ_OUT", literals(outputs)); + ps("KECCAK_PROJ_CELLS", (PACK + ops.len()).to_string()); + // Each unit vector's embedding is a product of small irreducible factors (the modulus's, + // by the CRT): the factors' monomials, then a straight line of products over them. + let factored: Vec> = RMFE.emb.iter().map(|&e| poly_factors(e as u64)).collect(); + let clmul = |a: u64, b: u64| (0..64).filter(|i| b >> i & 1 == 1).fold(0, |acc, i| acc ^ a << i); + for (row, &e) in factored.iter().zip(&RMFE.emb) { + assert_eq!(row.iter().fold(1, |acc, &f| clmul(acc, f)), e as u64); + } + let mut factors: Vec = factored.concat(); + factors.sort_unstable(); + factors.dedup(); + let (starts, idx) = lists(factors.iter().map(|&f| bits(f, EMBED_BITS)).collect()); + ps("KECCAK_FAC_START", starts); + ps("KECCAK_FAC_IDX", idx); + ps( + "KECCAK_FAC_DEG", + (factors.iter().map(|f| 63 - f.leading_zeros()).max().unwrap() + 1).to_string(), + ); + let (ops, outputs) = straight_line( + &factored + .iter() + .map(|row| { + row.iter() + .map(|f| factors.binary_search(f).unwrap()) + .collect::>() + }) + .collect::>(), + factors.len(), + ); + ps("KECCAK_EMB_A", literals(ops.iter().map(|op| op.0))); + ps("KECCAK_EMB_B", literals(ops.iter().map(|op| op.1))); + ps("KECCAK_EMB_OUT", literals(outputs)); + ps("KECCAK_FAC_N", factors.len().to_string()); + ps("KECCAK_EMB_CELLS", (factors.len() + ops.len()).to_string()); + // The positions iota flips are `2^i - 1`; per (layer, segment), which `i`. + let (starts, idx) = lists( + (0..depth * segments) + .map(|at| { + let round = shape.round(at % segments, at / segments); + (0..7).filter(|i| keccak_gkr::iota_bit(round, (1 << i) - 1)).collect() + }) + .collect(), + ); + ps("KECCAK_IOTA_START", starts); + ps("KECCAK_IOTA_IDX", idx); + ps("KECCAK_PI", literals(primitives::hash::PI)); + ps("KECCAK_RHO", literals(primitives::hash::RHO)); + // The rotations the linear layer's transpose reads a slice table at. + let mut used = [0usize; 64]; + for rot in primitives::hash::RHO { + used[rot as usize] = 1; + used[(rot as usize + 1) % 64] = 1; + } + ps("KECCAK_SHIFT_USED", literals(used)); + ps("KECCAK_STATE", leanvm_core::keccak::STATE_CELLS.to_string()); +} + +/// The F192 tower and its coordinate basis. +fn field_placeholders(ps: &mut impl FnMut(&str, String)) { + // Tower F192 = F64[Y]/(Y^3+Y+1), Y = new(0,1,0). Y_TOWER embeds Y for + // AIR lane reassembly; Y_INV splits a canonical word into its two K limbs. + let y_tower = F192::new(0, 1, 0); + ps("Y_TOWER", dsl_u128(y_tower).to_string()); + ps("Y_INV", f192_literal(y_tower.inv())); + // Coordinate basis e_i of F192 over F2 (spans the whole field): the 64 + // binary basis vectors in each of the three tower limbs. The guest uses + // these vectors to reconstruct a word from its 192 coordinate bits. + let coord_basis: Vec = (0..192) + .map(|i| match i / 64 { + 0 => F192::new(1u64 << i, 0, 0), + 1 => F192::new(0, 1u64 << (i - 64), 0), + 2 => F192::new(0, 0, 1u64 << (i - 128)), + _ => unreachable!(), + }) + .collect(); + ps("COORD_BASIS", flds(&coord_basis)); + ps("QKECCAK_SLOT_LOG", leanvm_core::keccak::SLOT_LOG.to_string()); +} + +/// The stacked opening's candidate tables, one per committed size and rate: every size the native verifier accepts of +/// an execution of any bytecode, `MIN_STACKED_LOG..=MAX_STACKED_LOG`. +fn opening_placeholders(ps: &mut impl FnMut(&str, String)) { + let oshape = |m: usize, log_inv_rate: usize| { + let shape = whir_shape(m, log_inv_rate); + let (vc, sh) = (&shape.config, &shape.levels); + let (cn, cr) = (sh.levels, vc.level_steps); + // Every cap root must match a transcript-bound root, including the final level. + assert_eq!(cr, cn - 1, "the yr level must be the last one"); + let (ck, cyr) = (&sh.ks, sh.yr_log_n); + let cq = &vc.queries; + let (cd, cp) = (&shape.depth, &shape.per_squeeze); + let cs: Vec = (0..cn).map(|i| cq[i].div_ceil(cp[i])).collect(); + let cni: Vec = ck.iter().map(|&k| 1usize << k).collect(); + assert!( + cni.iter().enumerate().all(|(lv, &n)| { + let bytes = if lv == 0 { 8 * n } else { 24 * n }; + bytes <= 1024 && n % 4 == 0 + }), + "recursive WHIR guest supports Merkle rows of at most 1024 bytes, packed four words to two cells" + ); + let psum = |f: &dyn Fn(usize) -> usize| -> Vec { + let mut offsets = Vec::with_capacity(cn); + let mut acc = 0; + for lv in 0..cn { + offsets.push(acc); + acc += f(lv); + } + offsets + }; + let cap_depths: Vec<_> = (0..cn).map(|lv| merkle_cap_depth(cq[lv], cd[lv])).collect(); + let c_foldbase = psum(&|lv| ck[lv]); + let c_risstart: Vec = (0..cn).map(|k| c_foldbase[k] + ck[k]).collect(); + OpeningShape { + n_levels: cn, + yr_level: cr, + yr_log_len: cyr, + folds: shape.levels.ks, + log_message_columns: shape.levels.log_msg_cols, + queries: shape.config.queries, + tree_depths: shape.depth, + positions_per_squeeze: shape.per_squeeze, + squeezes: cs, + interleaving: cni, + query_grinding_bits: shape.config.grinding_bits, + cap_depths, + fold_offsets: c_foldbase, + residual_fold_offsets: c_risstart, + ood_samples: shape.config.ood_samples, + } + }; + let (minm, maxm) = (verifier::MIN_STACKED_LOG, verifier::MAX_STACKED_LOG); + let rates = pcs::whir::MIN_LOG_INV_RATE..=pcs::whir::MAX_LOG_INV_RATE; + let cands: Vec<_> = rates.flat_map(|r| (minm..=maxm).map(move |m| oshape(m, r))).collect(); + let maxlev = cands.iter().map(|c| c.n_levels).max().unwrap(); + let maxood = cands.iter().flat_map(|c| &c.ood_samples).copied().max().unwrap_or(0); + ps("LIG_MAX_LEVELS", maxlev.to_string()); + ps("LIG_MAX_OOD_SAMPLES", maxood.to_string()); + ps("LIG_MIN_LOG_SIZE", minm.to_string()); + let cks: Vec<(usize, usize)> = col_sources().into_iter().flatten().collect(); + ps("N_COMMITTED_COLS", cks.len().to_string()); + ps("N_COLUMN_LOGS", (verifier::MAX_STACKED_LOG + 1).to_string()); + ps("COL_KAPPA_SRC", literals(cks.iter().map(|&(s, _)| s))); + ps("COL_KAPPA_ADJ", literals(cks.iter().map(|&(_, a)| a))); + let groups = col_groups(&cks); + ps("N_COL_GROUPS", groups.len().to_string()); + ps("COL_GROUP_START", literals(groups.iter().copied().chain([cks.len()]))); + ps("PCS_MIN_MU", verifier::MIN_STACKED_LOG.to_string()); + ps( + "LIG_LOG_MSG_COLS_CAP", + cands + .iter() + .map(|c| *c.log_message_columns.iter().max().unwrap()) + .max() + .unwrap() + .to_string(), + ); + ps( + "YR_LOG_CAP", + cands.iter().map(|c| c.yr_log_len).max().unwrap().to_string(), + ); + { + let flat = |f: &dyn Fn(&OpeningShape) -> Vec| { + let rows: Vec = cands + .iter() + .flat_map(|c| { + let mut row = f(c); + row.resize(maxlev, 0); + row + }) + .collect(); + literals(rows) + }; + let scal = |f: &dyn Fn(&OpeningShape) -> usize| literals(cands.iter().map(f)); + ps("LIG_N_LEVELS", scal(&|c| c.n_levels)); + ps("LIG_YR_LEVEL", scal(&|c| c.yr_level)); + // The guest rotates the terminal point by the lane-fold count to index it by + // witness coordinate, and the residual segment is what it rotates the last + // lane challenges past, so the residual may never be longer than that fold + // (`RESIDUAL_MAX_LOG` < `INITIAL_FOLDING_FACTOR` keeps this true by a margin). + assert!( + cands.iter().all(|c| c.yr_log_len <= c.folds[0]), + "residual longer than the lane fold: the guest's point rotation has no room" + ); + ps("LIG_YR_LOG_LEN", scal(&|c| c.yr_log_len)); + ps("LIG_YR_LEN", scal(&|c| 1usize << c.yr_log_len)); + ps("LIG_TOTAL_FOLDS", scal(&|c| c.folds.iter().sum())); + ps("LIG_MAX_INTERLEAVE", scal(&|c| *c.interleaving.iter().max().unwrap())); + let row_cap = cands + .iter() + .flat_map(|c| { + c.interleaving + .iter() + .enumerate() + .map(|(level, &n)| n * if level == 0 { 1 } else { 3 }) + }) + .max() + .unwrap(); + ps("LIG_ROW_CAP", row_cap.to_string()); + ps("LIG_PACKED_ROW_CAP", (row_cap / 2).to_string()); + // `decode_query_windows`: a squeezed word's positions, each its path bits QW at a time, then the cap + // child bit and the cap index, read back through `win_tabs`' 128 bit patterns. + let windows = cands.iter().flat_map(|c| { + (0..c.n_levels).map(|lv| { + let (depth, cap) = (c.tree_depths[lv], c.cap_depths[lv]); + assert!((2..=8).contains(&cap), "a cap of {cap} levels"); + c.positions_per_squeeze[lv] * ((depth - cap).div_ceil(QUERY_WINDOW_BITS) + 2) + }) + }); + assert!( + windows.max().unwrap() <= QUERY_WINDOWS_CAP, + "a squeezed word's windows past QUERY_WINDOWS_CAP" + ); + ps( + "LIG_PATH_CAP", + cands + .iter() + .flat_map(|c| { + c.tree_depths + .iter() + .zip(&c.cap_depths) + .map(|(depth, cap)| 4 * (depth - cap)) + }) + .max() + .unwrap() + .to_string(), + ); + // The guest reads every level's query nonce, which a grind of no bits would not send. + assert!( + cands.iter().all(|c| c.query_grinding_bits.iter().all(|&bits| bits > 0)), + "a query phase grinds no bits" + ); + ps("LIG_QUERY_GRIND_BITS", flat(&|c| c.query_grinding_bits.clone())); + ps("LIG_OOD_SAMPLES", flat(&|c| c.ood_samples.clone())); + ps("LIG_QUERIES", flat(&|c| c.queries.clone())); + ps("LIG_FOLDS", flat(&|c| c.folds.clone())); + ps("LIG_INTERLEAVE", flat(&|c| c.interleaving.clone())); + // 128-bit cells per leaf row: level 0's committed rows are base-field + // F64 (8 bytes/lane); deeper levels are native F192 (24 bytes/word, + // received as three embedded K limbs each). + ps( + "LIG_LEAF_CELLS", + flat(&|c| { + c.interleaving + .iter() + .enumerate() + .map(|(level, &n)| if level == 0 { n / 2 } else { 3 * n / 2 }) + .collect() + }), + ); + ps("LIG_TREE_DEPTH", flat(&|c| c.tree_depths.clone())); + ps("LIG_CAP_DEPTH", flat(&|c| c.cap_depths.clone())); + ps("LIG_SQUEEZES", flat(&|c| c.squeezes.clone())); + ps("LIG_LOG_MSG_COLS", flat(&|c| c.log_message_columns.clone())); + ps("LIG_RESIDUAL_FOLD_OFF", flat(&|c| c.residual_fold_offsets.clone())); + ps( + "LIG_RESIDUAL_PREFIX_LEN", + flat(&|c| { + c.log_message_columns + .iter() + .map(|&columns| columns - c.yr_log_len) + .collect() + }), + ); + ps("LIG_FOLDS_OFF", flat(&|c| c.fold_offsets.clone())); + } + let n_log_sizes = maxm - minm + 1; + let n_rates = MAX_LOG_INV_RATE - MIN_LOG_INV_RATE + 1; + // `open_stacked` has one arm per size and reads these at the size's first-rate row: a rate changes none of them. + fn size_only(c: &OpeningShape) -> (usize, usize, usize, [&Vec; 7]) { + let lists = [ + &c.folds, + &c.log_message_columns, + &c.interleaving, + &c.fold_offsets, + &c.residual_fold_offsets, + &c.ood_samples, + &c.query_grinding_bits, + ]; + (c.n_levels, c.yr_level, c.yr_log_len, lists) + } + for (i, c) in cands.iter().enumerate() { + assert!( + size_only(c) == size_only(&cands[i % n_log_sizes]), + "a size-only opening table varies by rate" + ); + } + let level_positions = cands + .iter() + .flat_map(|c| (0..c.n_levels).map(|lv| c.squeezes[lv] * c.positions_per_squeeze[lv])); + ps("LIG_LEVEL_POSITIONS_CAP", level_positions.max().unwrap().to_string()); + // The subspace-vanishing values of a level's message columns and their inverses, by the column count: block `n` + // at n(n+1)/2. + let max_cols = cands + .iter() + .flat_map(|c| &c.log_message_columns) + .copied() + .max() + .unwrap(); + let by_cols: Vec = (0..=max_cols) + .flat_map(|cols| { + let values = pcs::whir::eval_sk_at_vks(cols); + assert_eq!(values.len(), cols + 1); + values + }) + .collect(); + let inverses: Vec = by_cols.iter().map(|v| F192::from(v.inv())).collect(); + ps( + "VANISH_BY_COLS", + flds(&by_cols.into_iter().map(F192::from).collect::>()), + ); + ps("VANISH_INVS_BY_COLS", flds(&inverses)); + ps("LIG_N_LOG_SIZES", n_log_sizes.to_string()); + ps("LIG_N_RATES", n_rates.to_string()); + ps("LIG_MIN_SHIFT_INV", dsl_u128(F192::from(g_pow(minm).inv())).to_string()); +} + +/// The stacked bytecode's claim, the batch's seed, and the statement's layout. +fn bytecode_placeholders(kbc: usize, nbcv: usize, ps: &mut impl FnMut(&str, String)) { + ps("BYTECODE_LOG", kbc.to_string()); + // The stacked bytecode: nbcv/2 encoding columns per side, aligned with the bus + // tuple, so their slots span the fingerprint's own bits. The defer region is + // the claim's point (row, then slot) and its value. + let bc_cols = nbcv / 2; + let log2_bc_cols = leanvm_core::leaf::N_TUPLE_BITS; + ps("BYTECODE_COLS", bc_cols.to_string()); + ps("LOG2_BYTECODE_COLS", log2_bc_cols.to_string()); + ps("DEFER_SIZE", (kbc + log2_bc_cols + 1).to_string()); + ps("BYTECODE_VARS", (kbc + log2_bc_cols).to_string()); + let agg_seed = FiatShamirState::from_label(RECURSION_AGG_LABEL).capacity_cells(); + for (c, cell) in agg_seed.iter().enumerate() { + ps(&format!("AGG_SEED_{c}"), dsl_u128(*cell).to_string()); + } + // A child's words: its statement's two, its fresh claim, and its carried claim's cells. + let child_words = 2 + (kbc + log2_bc_cols + 1) + (kbc + log2_bc_cols + 1); + ps( + "AGG_CHILD_PAD", + (child_words.next_multiple_of(RATE_SCALARS) - child_words).to_string(), + ); + let defer_cells = kbc + log2_bc_cols + 1; + ps("STMT_HEADER", STATEMENT_HEADER.to_string()); + let pairs = defer_cells.div_ceil(2); + ps("STMT_PAIRS", pairs.to_string()); +} + +/// LeanDA (`doc/leanvm` §sec:leanda). +fn da_placeholders(ps: &mut impl FnMut(&str, String)) { + let pad_blob = hash_to_scalars(&lean_da::padding_root()); + ps("DA_LOG_K", DA_LOG_K.to_string()); + ps("DA_LOG_CELL", DA_LOG_CELL.to_string()); + ps("DA_MAX_ROWS", DA_MAX_ROWS.to_string()); + ps("DA_LOG_MAX_ROWS", DA_MAX_ROWS.ilog2().to_string()); + ps("DA_PAD_BLOB_0", f192_literal(pad_blob[0])); + ps("DA_PAD_BLOB_1", f192_literal(pad_blob[1])); + // `Vhat_j` at each parent node `n` of a product tree over bits `top, top - 1, ...`, in heap + // order: `j` the bit `n` splits, the point its path's bits make. + let vhat = lean_da::subspace_above(); + let tree = |top: usize, depth: usize| -> Vec { + (0..1usize << depth) + .map(|n| n.checked_ilog2().map_or(0, |d| vhat(top - d as usize, n - (1 << d)).0)) + .collect() + }; + let cell_bits = CELLS_PER_ROW.ilog2() as usize; + let (top_tree, cell_tree) = (tree(DA_LOG_K, cell_bits), tree(DA_LOG_CELL - 1, DA_LOG_CELL)); + ps("DA_VHAT", literals(top_tree.into_iter().chain(cell_tree))); + // `Vhat_j(beta_(DA_LOG_CELL + b))` for each factor `j` inside a cell and bit `b` of the cell index. + let column = (0..cell_bits).flat_map(|b| (0..DA_LOG_CELL).map(move |j| (j, b))); + ps( + "DA_VHAT_COLUMN", + literals(column.map(|(j, b)| vhat(j, 1 << (DA_LOG_CELL - 1 - j + b)).0)), + ); +} + +/// The signature scheme, the leanSTARK caps and domains, and the coverage caps. +fn signature_placeholders(ps: &mut impl FnMut(&str, String)) { + ps("MAX_GENERIC_BYTECODE_LOG", MAX_GENERIC_BYTECODE_LOG.to_string()); + ps("MAX_GENERIC_DEPENDENCIES", MAX_GENERIC_DEPENDENCIES.to_string()); + ps("DEPS_DOMAIN_0", dsl_u128(pack_16_bytes(&LEANSTARK_DEPS_DOMAIN[..16])).to_string()); + ps("DEPS_DOMAIN_1", dsl_u128(pack_16_bytes(&LEANSTARK_DEPS_DOMAIN[16..])).to_string()); + // The leanVM tag ahead of a program's table in its digest: its first 8 bytes as a lane, its 9th byte. + let tag: &[u8; 9] = verifier::TAG.try_into().expect("a 9-byte tag"); + ps("TAG_LANE0", u64::from_le_bytes(tag[..8].try_into().unwrap()).to_string()); + ps("TAG_BYTE8", tag[8].to_string()); + ps("CLAIM_GROUP", CLAIM_GROUP.to_string()); + ps("BIG_GROUPS", BIG_GROUPS.to_string()); + ps("MAX_RECURSIONS", MAX_RECURSIONS.to_string()); + + // NiceTry SPHINCS- v2 (prototype). + for (name, value) in [ + ("SP_K", lean_sphincs::K), + ("SP_A", lean_sphincs::A), + ("SP_D", lean_sphincs::D), + ("SP_HP", lean_sphincs::SUBTREE_H), + ("SP_H", lean_sphincs::H), + ("SP_W", lean_sphincs::W), + ("SP_LEN1", lean_sphincs::LEN1), + ("SP_LEN2", lean_sphincs::LEN2), + ("SP_MAX_CSUM", lean_sphincs::MAX_CSUM), + ] { + ps(name, value.to_string()); + } + // Digest bit b (big-endian 256-bit integer) as an index into the bit run of the + // digest's last three lanes. + let bit_index: Vec = (0..3 * 64) + .map(|b| { + let byte = 31 - b / 8; + (64 * (byte / 8 - 1) + 8 * (byte % 8) + b % 8).to_string() + }) + .collect(); + ps("SP_BIT_INDEX", format!("[{}]", bit_index.join(", "))); +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/proof.rs b/src/ffi/crates/rec_aggregation/src/aggregation/proof.rs new file mode 100644 index 0000000..c61047f --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/proof.rs @@ -0,0 +1,232 @@ +//! The aggregate, its wire format and its native verification. + +use super::deferred::{DeferredClaim, scalar_bytecode}; +use super::error::{AggregateVerifyError, SnarkVerifyError}; +use super::cells::cell_bytes; +use super::guest::unified_guest; +use super::generic::MAX_GENERIC_DEPENDENCIES; +use super::statement::{deps_digest_cells, statement_digest, statement_digest_of}; +use fiat_shamir::merkle::hash_to_scalars; +use super::{GenericClaim, LeanSphincsClaim}; +use leanvm_core::cpu::{public_input_bytes, scalar_proof}; +use primitives::field::F192; + +/// What an aggregate attests: every `(data_hash, vk)` in [`Self::generic`] is backed by a valid +/// leanVM execution proof of the program `vk` commits to on public input `data_hash`, every +/// `(message, pubkey)` in [`Self::lean_sphincs`] by a valid leanSPHINCS signature, and the [`Self::da_commitment`], if any, +/// commits to blob rows that are valid Reed-Solomon codewords. +/// +/// The two lists remain separate because the proof says which scheme verified which claim (the +/// module docs give the coverage argument). A proof's lists are each strictly sorted and deduplicated +/// on the whole `(pubkey, message)` pair, and [`EthereumProof::from_proof_bytes`] and +/// [`EthereumProof::verify`] reject claims that are not. Either list +/// may be empty, and both are in a blob-only proof. +/// +/// **Neither length counts signers, only claims.** One pubkey may appear under several messages, so a +/// committee threshold has to count distinct pubkeys itself. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct EthereumClaims { + /// The leanSTARK (`0x11`) claims, at most [`MAX_GENERIC_DEPENDENCIES`]. + pub generic: Vec, + pub lean_sphincs: Vec, + /// `None` makes no blob claim. The statement carries its [`verifier::da::digest`]. + pub da_commitment: Option<[u8; 32]>, +} + +/// A proof of its [`EthereumClaims`], established directly or carried from verified child proofs. +/// +/// Until [`Self::verify`] returns `Ok` they are claims, not attestation. A receiver that decoded the +/// proof with [`Self::from_proof_bytes`] supplied them itself; otherwise, as for [`super::aggregate`]'s +/// output, they are the prover's choice, and a caller has to compare them against what it expected. +#[derive(Clone, Debug)] +pub struct EthereumProof { + pub(super) claims: EthereumClaims, + /// What this aggregate defers to whoever discharges it: its parent, in + /// circuit, or [`Self::verify`], natively. + pub(super) defer: DeferredClaim, + pub(super) proof: leanvm_core::cpu::Proof, +} + +// The wire format: the deferred point as a little-endian u64 count of limb triples `c0, c1, c2`, +// each a little-endian u64, then the VM proof in its own encoding, to the end. The claims are not +// sent: the receiver knows what it expects a proof to attest, and supplies it. +// +// Given the claims, every byte string decodes to at most one aggregate and back to itself: the VM +// proof's decoder rejects trailing bytes. Otherwise anything downstream that dedupes or indexes on the +// serialized bytes could be made to see one aggregate as many. + +pub(super) fn encode(point: &[F192], proof: &[u8]) -> Vec { + let mut out = (point.len() as u64).to_le_bytes().to_vec(); + out.extend(point.iter().flat_map(|x| x.to_limbs()).flat_map(u64::to_le_bytes)); + out.extend(proof); + out +} + +/// The deferred point and the VM proof's bytes. The point's limbs are cut from the input before +/// any is read, so a forged count allocates nothing. +pub(super) fn decode(bytes: &[u8]) -> Option<(Vec, &[u8])> { + let (count, rest) = bytes.split_first_chunk::<8>()?; + let count = usize::try_from(u64::from_le_bytes(*count)).ok()?; + let (limbs, proof) = rest.split_at_checked(count.checked_mul(24)?)?; + let limb = |bytes: &[u8; 8]| u64::from_le_bytes(*bytes); + let triples = limbs.as_chunks::<8>().0.as_chunks::<3>().0; + let point = triples + .iter() + .map(|[c0, c1, c2]| F192::new(limb(c0), limb(c1), limb(c2))) + .collect(); + Some((point, proof)) +} + +/// Reject a signer set that the coverage argument does not cover: strict sorting +/// within each list is what stops one claim being counted many times, so each +/// list's length is a count of distinct `(pubkey, message)` claims. Either list may be +/// empty; both may be empty for a blob proof. Their sizes are the guest's to bound, by its +/// memory. +pub(super) fn check_signer_set(claims: &EthereumClaims) -> Result<(), AggregateVerifyError> { + if !claims.generic.windows(2).all(|w| w[0] < w[1]) + || !claims.lean_sphincs.windows(2).all(|w| w[0] < w[1]) + || claims.generic.len() > MAX_GENERIC_DEPENDENCIES + { + return Err(AggregateVerifyError::MalformedSignerSet); + } + Ok(()) +} + +impl EthereumClaims { + /// The signature claims, as many as the coverage table's declared slots. + /// + /// NOT a count of distinct signers: a pubkey of either scheme may hold several + /// claims, one per message it signed. A caller that wants signers has to + /// deduplicate by pubkey itself. + pub fn num_signature_claims(&self) -> usize { + self.generic.len() + self.lean_sphincs.len() + } +} + +/// EIP-8288 `get_deps_hash` with `DEPS_HASH` = Keccak-256. `S` hashes `data_hash ‖ pkRoot ‖ pkSeed` for +/// each leanSPHINCS (`0x10`) dependency and `G` `data_hash ‖ vk` for each leanSTARK (`0x11`) one, each +/// list in the order given; the digest is `S` without leanSTARK dependencies (the published +/// `get_deps_hash`), else `keccak256(S ‖ G ‖ LEANSTARK_DEPS_DOMAIN)`. The caller canonicalizes (sorts, +/// deduplicates) each list. +pub fn deps_hash(lean_sphincs: &[LeanSphincsClaim], generic: &[GenericClaim]) -> [u8; 32] { + cell_bytes(deps_digest_cells(lean_sphincs, generic)) + .try_into() + .expect("two 16-byte cells") +} + +/// What stands for EIP-8288 `AGGREGATED_VK`: the guest's Fiat-Shamir IV, +/// `keccak256(verifier::TAG ‖ stacked bytecode table)`, which seeds every transcript and leads the statement. +pub fn aggregated_vk() -> [u8; 32] { + cell_bytes(leanvm_core::cpu::fs_seed(unified_guest())) + .try_into() + .expect("two 16-byte cells") +} + +/// The VM public input of a dependency-only aggregate with dependency digest `deps_hash`: +/// `keccak256(AGGREGATED_VK ‖ deps_hash ‖ 0^32 ‖ deferred cells)`. +fn deps_public_input(deps_hash: &[u8; 32], defer: &DeferredClaim) -> [F192; 2] { + statement_digest_of(hash_to_scalars(deps_hash), [0; 32], defer) +} + +/// The 32-byte VM public input [`verify_deps_hash`] checks `proof_bytes` against, and the deferred +/// cells' bytes that end its preimage (three little-endian limbs a cell, zero-padded to an even count). +pub fn deps_public_input_bytes( + deps_hash: &[u8; 32], + proof_bytes: &[u8], +) -> Result<([u8; 32], Vec), AggregateVerifyError> { + let (point, _) = decode(proof_bytes).ok_or(AggregateVerifyError::MalformedEncoding)?; + let defer = DeferredClaim::recompute_scalar(point)?; + let mut cells = defer.cells(); + cells.resize(cells.len().next_multiple_of(2), F192::ZERO); + let tail = cells.iter().flat_map(|x| x.to_limbs()).flat_map(u64::to_le_bytes).collect(); + Ok((public_input_bytes(&deps_public_input(deps_hash, &defer)), tail)) +} + +/// EIP-8288 verification: `verify_leanSTARK((deps_hash, AGGREGATED_VK), AGGREGATED_VK, proof)`, holding +/// only the digest and the proof. An empty dependency list has the empty proof. +pub fn verify_deps_hash(deps_hash: &[u8; 32], proof_bytes: &[u8]) -> Result<(), AggregateVerifyError> { + if proof_bytes.is_empty() { + return if *deps_hash == primitives::hash::hash(&[]) { + Ok(()) + } else { + Err(AggregateVerifyError::MalformedEncoding) + }; + } + let (point, proof) = decode(proof_bytes).ok_or(AggregateVerifyError::MalformedEncoding)?; + let proof = leanvm_core::cpu::Proof::from_bytes(proof).ok_or(AggregateVerifyError::MalformedEncoding)?; + let defer = DeferredClaim::recompute_scalar(point)?; + let pi = deps_public_input(deps_hash, &defer); + verifier::verify_execution(scalar_bytecode(), &public_input_bytes(&pi), &scalar_proof(&proof)) + .map(|_| ()) + .map_err(|e| AggregateVerifyError::Snark(SnarkVerifyError(e))) +} + +impl EthereumProof { + /// The VM's public input for this aggregate: the digest of its claims and its deferred claim. + pub(super) fn vm_public_input(&self) -> [F192; 2] { + let da = verifier::da::digest(self.claims.da_commitment); + statement_digest(&self.claims.generic, &self.claims.lean_sphincs, da, &self.defer) + } + + /// What this aggregate claims, which [`Self::verify`] checks. + pub fn claims(&self) -> &EthereumClaims { + &self.claims + } + + /// What goes on the wire: the deferred point and the VM proof. Neither the claims nor the + /// deferred claim's value is transmitted: the receiver supplies the claims to + /// [`Self::from_proof_bytes`], which recomputes the value, so there is nothing to lie about. + pub fn proof_bytes(&self) -> Vec { + encode(&self.defer.bytecode_point, &self.proof.to_bytes()) + } + + /// The aggregate `bytes` encode, as a proof of `claims`: what the receiver expects it to attest. + /// Claims other than the aggregated ones fail [`Self::verify`]. + /// + /// WARNING: Parsing does NOT verify: the proof is untouched, only shapes are checked. + /// Call [`Self::verify`] before believing any of it. + pub fn from_proof_bytes(bytes: &[u8], claims: EthereumClaims) -> Result { + let (bytecode_point, proof) = decode(bytes).ok_or(AggregateVerifyError::MalformedEncoding)?; + // Cheap rejections first. `recompute_scalar` below is a pass over the whole stacked + // bytecode, on a point a peer chose, so anything decidable without it has to be + // decided before it. + check_signer_set(&claims)?; + let proof = leanvm_core::cpu::Proof::from_bytes(proof).ok_or(AggregateVerifyError::MalformedEncoding)?; + Ok(Self { + claims, + // The wire carries no value, only the point to derive it from. + defer: DeferredClaim::recompute_scalar(bytecode_point)?, + proof, + }) + } + + /// Verify the aggregate's internal consistency: the signer set is well + /// formed, the deferred bytecode claim holds at its + /// transmitted point, and the VM proof satisfies the statement built from + /// all of it. + /// + /// This says "the program each `(data_hash, vk)` in the claims' `generic` commits to accepts + /// `data_hash`, and every `(message, pubkey)` in their `lean_sphincs` is a valid leanSPHINCS + /// signature", and that the `da_commitment`, if any, commits to a well-formed blob matrix: + /// exactly the claims held, which are the receiver's own after [`Self::from_proof_bytes`]. + /// + /// Every field operation, the proof check and its Merkle hashing run in the `verifier` crate: one thread, no explicit SIMD. + pub fn verify(&self) -> Result<(), AggregateVerifyError> { + check_signer_set(&self.claims)?; + // Discharging the deferred claim IS recomputing it: its value has + // to be the true evaluation, or the recursion below proves nothing about + // the bytecode. + let _s = tracing::info_span!("Recompute deferred claims").entered(); + let defer = DeferredClaim::recompute_scalar(self.defer.bytecode_point.clone())?; + drop(_s); + if defer != self.defer { + return Err(AggregateVerifyError::MalformedClaim); + } + let _s = tracing::info_span!("Statement digest").entered(); + let pi = self.vm_public_input(); + drop(_s); + verifier::verify_execution(scalar_bytecode(), &public_input_bytes(&pi), &scalar_proof(&self.proof)) + .map(|_| ()) + .map_err(|e| AggregateVerifyError::Snark(SnarkVerifyError(e))) + } +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/statement.rs b/src/ffi/crates/rec_aggregation/src/aggregation/statement.rs new file mode 100644 index 0000000..d621e25 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/statement.rs @@ -0,0 +1,138 @@ +//! Claim-list digests and a node's public statement. + +use super::cells::{cell_bytes, count, pack_16_bytes}; +use super::deferred::DeferredClaim; +use super::guest::unified_guest; +use super::generic::{LEANSTARK_DEPS_DOMAIN, digest_cells}; +use super::{GenericClaim, LeanSphincsClaim}; +use fiat_shamir::merkle::hash_to_scalars; +use primitives::field::F192; + +/// Claims the guest hashes a loop frame: 68 cells, four whole block pairs. +pub(super) const CLAIM_GROUP: usize = 17; +const _: () = assert!((CLAIM_GROUP * 64).is_multiple_of(2 * primitives::hash::RATE)); +/// Groups of [`CLAIM_GROUP`] the guest's main list loop hashes a frame. +pub(super) const BIG_GROUPS: usize = 8; +const CLAIM_BIG: usize = CLAIM_GROUP * BIG_GROUPS; + +/// How the guest splits a list of `n` claims: the whole frames of [`CLAIM_BIG`], then +/// the whole groups of [`CLAIM_GROUP`], then the claims after them. Its own product +/// identity and range checks pin all three, so this only has to agree with them +/// (`list_split` in the guest). A list is never empty here: both claim lists are +/// guarded at the call, since the guest hashes an empty one without a split at all. +pub(super) fn list_split(n: usize) -> Vec { + assert!(n > 0, "an empty list has no split"); + let rest = n % CLAIM_BIG; + vec![ + count(n / CLAIM_BIG), + count(rest / CLAIM_GROUP), + count(rest % CLAIM_GROUP), + ] +} + +/// One scheme's declared claims, hashed plainly: 64 bytes a claim, its pubkey then +/// the message it signed, and an empty list hashes the empty string. The guest +/// computes this same digest [`CLAIM_GROUP`] claims at a time (`claim_list_digest`). +pub(super) fn claim_list_digest(claims: impl IntoIterator) -> [F192; 2] { + hash_to_scalars(&primitives::hash::hash(&cell_bytes(claims.into_iter().flatten()))) +} + +/// EIP-8288 entry order: `data_hash ‖ vk`, as the guest's `verify_generic` reads its slot. +pub(super) fn generic_claim_cells((data_hash, vk): &GenericClaim) -> [F192; 4] { + let [d0, d1] = digest_cells(data_hash); + let [k0, k1] = digest_cells(vk); + [d0, d1, k0, k1] +} + +/// The EIP-8288 dependency digest of the two lists' digests: the leanSPHINCS one alone when there +/// is no leanSTARK claim, else `keccak256(S ‖ G ‖ LEANSTARK_DEPS_DOMAIN)` (the guest's `deps_digest`). +pub(super) fn deps_digest_of(lean_sphincs_digest: [F192; 2], generic_digest: [F192; 2], has_generic: bool) -> [F192; 2] { + if !has_generic { + return lean_sphincs_digest; + } + let mut preimage = cell_bytes(lean_sphincs_digest.into_iter().chain(generic_digest)); + preimage.extend(LEANSTARK_DEPS_DOMAIN); + hash_to_scalars(&primitives::hash::hash(&preimage)) +} + +/// EIP-8288 entry order: `data_hash ‖ pkRoot ‖ pkSeed`, so the list digest is `get_deps_hash`. +pub(super) fn lean_sphincs_claim_cells((message, pk): &LeanSphincsClaim) -> [F192; 4] { + [ + pack_16_bytes(&message[..16]), + pack_16_bytes(&message[16..]), + pack_16_bytes(&pk.root), + pack_16_bytes(&pk.public_param), + ] +} + +/// The statement's fixed header, ahead of the deferred cells: the seed, the dependency digest of +/// the two claim lists ([`deps_digest_of`]), and the DA digest ([`verifier::da::digest`]). +/// Fed to the guest as `STMT_HEADER`, which pins where its deferred cells start; +/// the header cells themselves are written out again there, and only the +/// end-to-end statement check holds the two spellings together. +pub(super) const STATEMENT_HEADER: usize = 6; + +/// A plain hash over a lane stream, two lanes a 128-bit cell: what the guest gets +/// from `keccak_cells` over the same cells. +fn lane_hash(lanes: impl Iterator) -> [F192; 2] { + let bytes: Vec = lanes.flat_map(u64::to_le_bytes).collect(); + hash_to_scalars(&primitives::hash::hash(&bytes)) +} + +/// A node's public statement, hashed to the two words the VM publishes. The +/// guest's `statement_digest` computes exactly this, both for itself and when it +/// rebuilds a child's, which is what forces a whole tree onto one bytecode and +/// each child's claims, bound by the two list digests, onto its parent's lists. +/// +/// Fixed-length preimage, so a plain hash, with no domain tag of its own: every +/// field has a fixed width, and the header leads with the environment digest, which +/// binds this bytecode and the proof system's version and so already separates the +/// preimage from every other use of the hash here. The header is hashed as the +/// canonical cells it already is (two lanes each, whence the assert, the guest being +/// unable to hash a third), then all three lanes of each deferred cell. Each claim +/// list's digest binds its own length through the hash's padding, so the pair binds +/// both lists whole. +pub(super) fn statement_digest( + generic: &[GenericClaim], + lean_sphincs_signers: &[LeanSphincsClaim], + da_digest: [u8; 32], + defer: &DeferredClaim, +) -> [F192; 2] { + statement_digest_of(deps_digest_cells(lean_sphincs_signers, generic), da_digest, defer) +} + +/// The dependency digest of the two lists as two cells. +pub(super) fn deps_digest_cells(lean_sphincs: &[LeanSphincsClaim], generic: &[GenericClaim]) -> [F192; 2] { + deps_digest_of( + claim_list_digest(lean_sphincs.iter().map(lean_sphincs_claim_cells)), + claim_list_digest(generic.iter().map(generic_claim_cells)), + !generic.is_empty(), + ) +} + +/// [`statement_digest`] from the dependency digest: all a holder of `deps_hash` alone needs. +pub(super) fn statement_digest_of( + deps_digest: [F192; 2], + da_digest: [u8; 32], + defer: &DeferredClaim, +) -> [F192; 2] { + let seed = leanvm_core::cpu::fs_seed(unified_guest()); + let da_digest = hash_to_scalars(&da_digest); + let header: [F192; STATEMENT_HEADER] = [ + seed[0], + seed[1], + deps_digest[0], + deps_digest[1], + da_digest[0], + da_digest[1], + ]; + let mut cells = defer.cells(); + if !cells.len().is_multiple_of(2) { + cells.push(F192::ZERO); // the guest pairs the odd cell with a zero scalar + } + let head = header.iter().flat_map(|x| { + assert_eq!(x.c2, 0, "a header value is a canonical cell"); + [x.c0, x.c1] + }); + lane_hash(head.chain(cells.iter().flat_map(|x| x.to_limbs()))) +} diff --git a/src/ffi/crates/rec_aggregation/src/aggregation/tests.rs b/src/ffi/crates/rec_aggregation/src/aggregation/tests.rs new file mode 100644 index 0000000..66fdf63 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/aggregation/tests.rs @@ -0,0 +1,662 @@ +use super::cells::count; +use super::deferred::{DeferredClaim, bytecode_sums, bytecode_value, bytecode_vars, stacked_bytecode}; +use super::error::{AggregateVerifyError, AggregationError}; +use super::guest::{compile_guest, unified_guest}; +use super::hints::{FORGE, Forgery, Hints}; +use super::node::aggregate; +use super::placeholders::placeholder_map; +use super::proof::{EthereumClaims, EthereumProof, decode, encode}; +use super::statement::{CLAIM_GROUP, claim_list_digest, list_split}; +use fiat_shamir::merkle::hash_to_scalars; +use lean_compiler::{compile, parse_with_replacements}; +use lean_da::{BLOB_SYMBOLS, CELL_SYMBOLS, CELLS_PER_ROW, CODEWORD_SYMBOLS, DA_LOG_K, DA_MAX_ROWS}; +use leanvm_core::cpu::from_scalar; +use leanvm_core::cpu::verify; +use primitives::field::{F64, F192, g_pow}; +use primitives::multilinear::mle_eval_par; +use rand::SeedableRng; +use rand::rngs::StdRng; +use signatures::lean_sphincs::{LeanSphincsPublicKey, LeanSphincsSignature}; +use signatures::lean_sphincs; + +use crate::signers_cache::get_lean_sphincs_signers; + +const SMALL_LEAF_SIZE: usize = 6; +const LOG_INV_RATE: usize = leanvm_core::pcs::TEST_LOG_INV_RATE; + +#[test] +fn the_wire_layout_is_pinned() { + let le = u64::to_le_bytes; + let (point, proof) = (vec![F192::new(1, 2, 3), F192::new(4, 5, 6)], vec![9, 8, 7]); + let bytes = encode(&point, &proof); + assert_eq!( + bytes, + [&le(2)[..], &le(1), &le(2), &le(3), &le(4), &le(5), &le(6), &proof].concat() + ); + assert_eq!(decode(&bytes), Some((point, &proof[..]))); + // A count beyond the input, or beyond any input, fails whatever it claims. + assert_eq!(decode(&[&le(3)[..], &bytes[8..]].concat()), None); + assert_eq!(decode(&le(u64::MAX)), None); +} + +/// The guest compiles to one program, always. +/// +/// `unified_guest` finds a fixed point by compiling repeatedly and comparing +/// the result's log size, so a compiler that read a hash seed would not just +/// produce two incompatible transcripts, it could fail to converge at all. +/// The small programs in `lean_compiler`'s `determinism` suite pin the +/// digests; this pins the one program large enough to hit every path that +/// walks a map. A fixed `kbc` is enough: reproducibility does not depend on +/// the size being the fixed point. +#[test] +fn guest_compiles_reproducibly() { + let (one, two) = (compile_guest(20), compile_guest(20)); + assert_eq!( + format!("{:?}", one.prog()), + format!("{:?}", two.prog()), + "two compilations of the guest produced different bytecode, \ + so the compiler is reading a hash seed" + ); +} + +fn prove_leaf(signers: &[RawSphincs]) -> EthereumProof { + aggregate(&[], vec![], signers.to_vec(), &[], None, LOG_INV_RATE).expect("leaf aggregates") +} + +type RawSphincs = (LeanSphincsPublicKey, lean_sphincs::Message, LeanSphincsSignature); + +fn prove_lean_sphincs_leaf(signers: &[RawSphincs]) -> EthereumProof { + aggregate(&[], vec![], signers.to_vec(), &[], None, LOG_INV_RATE).expect("leaf aggregates") +} + +#[test] +fn aggregate_one_lean_sphincs_signer() { + leanvm_core::init_prover(); + let aggregate = prove_lean_sphincs_leaf(&get_lean_sphincs_signers(1)); + aggregate.verify().expect("verifies"); + assert!(aggregate.claims.generic.is_empty()); + assert_eq!(aggregate.claims.lean_sphincs.len(), 1); +} + +/// A leanSPHINCS signature's digest and openings are bound in-circuit: a node whose +/// hints put the digest's unused bytes off by one, order a FORS node and its sibling +/// the other way, or give a layer another counter fails the guest's run. +#[test] +fn forged_lean_sphincs_hints_are_rejected() { + leanvm_core::init_prover(); + let signers = get_lean_sphincs_signers(1); + let bump = |name: &'static str, i: usize| -> (&'static str, Forgery) { + (name, Box::new(move |hints: &mut Hints| hints.entry_mut(name, i)[0] += F192::ONE)) + }; + let digit = |i: usize| -> Forgery { Box::new(move |hints: &mut Hints| hints.entry_mut("sp_digits", i)[0] *= count(1)) }; + let forgeries: Vec<(&str, Forgery)> = vec![ + bump("sp_rand", 0), + bump("sp_fors_secrets", 0), + bump("sp_fors_paths", 0), + bump("sp_chain_starts", 0), + bump("sp_siblings", 0), + ("message digit", digit(0)), + ("checksum digit", digit(lean_sphincs::LEN1)), + ]; + for (name, forgery) in forgeries { + FORGE.with(|forge| *forge.borrow_mut() = Some(forgery)); + let result = aggregate(&[], vec![], signers.clone(), &[], None, LOG_INV_RATE); + assert!(matches!(result, Err(AggregationError::ProofError(_))), "{name}: {:?}", result.map(|_| ())); + } +} + +/// The repeat the statement allows: one pubkey signing two messages is two +/// claims, ordered bytewise by the 64-byte entry (message first), each needing its own signature. Generated +/// here rather than cached, the cache holding one message per pubkey. +#[test] +fn aggregate_one_key_two_messages() { + leanvm_core::init_prover(); + let (secret_key, public_key) = lean_sphincs::key_gen_from_seed([77; lean_sphincs::SEED_LEN]); + let raw: Vec = [3u8, 9] + .into_iter() + .map(|tag| { + let signed: lean_sphincs::Message = std::array::from_fn(|i| tag.wrapping_mul(i as u8 + 1)); + let signature = lean_sphincs::sign(&secret_key, &signed).expect("signs"); + (public_key, signed, signature) + }) + .collect(); + let aggregate = prove_lean_sphincs_leaf(&raw); + aggregate.verify().expect("verifies"); + assert_eq!(aggregate.claims.lean_sphincs.len(), 2); + let (first, second) = (aggregate.claims.lean_sphincs[0], aggregate.claims.lean_sphincs[1]); + assert_eq!(first.1, second.1, "the same pubkey, twice"); + assert!(first.0 < second.0, "ordered by the message, the entry's first 32 bytes"); + let declare = EthereumClaims { lean_sphincs: aggregate.claims.lean_sphincs.clone(), ..Default::default() }; + assert_eq!( + super::node::aggregate(&[], vec![], raw[..1].to_vec(), &[], Some(&declare), LOG_INV_RATE).err(), + Some(AggregationError::NotCovered), + "one signature covers one claim" + ); +} + +#[test] +fn guest_column_selectors_match_native_eq() { + leanvm_core::init_prover(); + let (helpers, _) = include_str!("../../guests/lean_ethereum.py") + .split_once("\ndef main():") + .unwrap(); + let source = format!( + r#"{helpers} +def main(): + # The selector tables read whole nibbles of coordinates, zero past the stack. + point = HeapBuf(4 * SEL_NIBBLES) + hint_witness(point[0:MAX_STACK_LOG], "point") + for k in unroll(MAX_STACK_LOG, 4 * SEL_NIBBLES): + point[GEN ** k] = 0 + point1 = HeapBuf(4 * SEL_NIBBLES) + for k in unroll(0, 4 * SEL_NIBBLES): + point1[GEN ** k] = 1 + point[GEN ** k] + sel_eq = selector_tables(point, point1) + g_squares = exponent_tables() + sel_g = g_squares * GEN ** SIZE_BITS + offset = hint_witness("offset") + kappa_g = hint_witness("kappa") + weight = match(log(kappa_g), range(0, N_COLUMN_LOGS), lambda kappa: column_selector(offset, sel_eq, sel_g, kappa)) + public = GEN ** 0 + assert public[1] == weight + return +"# + ); + let kbc = 18; + let guest = compile(&parse_with_replacements(&source, &placeholder_map(kbc)).unwrap()); + let run = |point: &[F192], offset: usize, kappa: usize, expected: F192| { + let mut hints = Hints::default(); + hints.push("point", point.to_vec()); + hints.push("offset", vec![count(offset)]); + hints.push("kappa", vec![count(kappa)]); + let mut program = guest.clone(); + hints.install(&mut program); + program.execute([expected, F192::ZERO]) + }; + let mut rng = StdRng::seed_from_u64(813); + for mu in verifier::min_execution_stacked_log(kbc)..=verifier::MAX_STACKED_LOG { + let mut point: Vec = (0..mu) + .map(|_| { + let (c0, c1, c2) = rand::Rng::random(&mut rng); + F192::new(c0, c1, c2) + }) + .collect(); + point.resize(verifier::MAX_STACKED_LOG, F192::ZERO); + for kappa in 0..=mu { + let mask = ((1usize << mu) - 1) & !((1usize << kappa) - 1); + for offset in [0, mask, 0x0555_5555 & mask] { + let selector: Vec<_> = (kappa..mu) + .map(|k| F192::from(F64(((offset >> k) & 1) as u64))) + .collect(); + let expected = primitives::multilinear::eq_eval(&selector, &point[kappa..mu]); + assert!( + run(&point, offset, kappa, expected) + .unwrap() + .unconstrained_reads + .is_empty() + ); + } + } + } +} + +/// The right leaf holds more claims than one loop frame of its list hashes +/// ([`CLAIM_GROUP`]), so both that leaf and the parent rebuilding it run the +/// list's group loop and then a partial last group, while the left leaf's list is +/// a partial group alone. +#[test] +fn aggregate_two_to_one() { + leanvm_core::init_prover(); + let big = 2 * CLAIM_GROUP + 3; + let signers = get_lean_sphincs_signers(SMALL_LEAF_SIZE + big); + let left = prove_leaf(&signers[..SMALL_LEAF_SIZE]); + let right = prove_leaf(&signers[SMALL_LEAF_SIZE..]); + let children = [left, right]; + let node = aggregate(&children, vec![], vec![], &[], None, LOG_INV_RATE).expect("node aggregates"); + node.verify().expect("node verifies"); + // `cpu::verify` accepts the same proof of the same statement. + verify(unified_guest(), &node.vm_public_input(), &node.proof).expect("node verifies natively"); + assert_eq!(node.claims.lean_sphincs.len(), SMALL_LEAF_SIZE + big); +} + +fn da_rows(n_rows: usize, seed: u64) -> Vec { + let mut rng = ::seed_from_u64(seed); + (0..n_rows * (1 << DA_LOG_K)) + .map(|_| rand::Rng::random(&mut rng)) + .collect() +} + +/// A LeanDA payload, proven without signatures and published in the +/// statement. The node has to reach the native committer's root, and the +/// aggregate has to verify against a statement that carries it. +#[test] +fn aggregate_with_a_da_payload() { + leanvm_core::init_prover(); + let rows = da_rows(3, 97); + + let node = aggregate(&[], vec![], vec![], &rows, None, LOG_INV_RATE).expect("node aggregates"); + node.verify().expect("node verifies"); + assert_eq!(node.claims.num_signature_claims(), 0); + + let (commitment, _) = lean_da::commit(&rows).unwrap(); + assert_eq!( + node.claims.da_commitment, + Some(commitment), + "the guest committed to something else" + ); + // The receiver supplies the claims it expects: those verify, any others fail. + let bytes = node.proof_bytes(); + let expected = EthereumClaims { + da_commitment: Some(commitment), + ..EthereumClaims::default() + }; + EthereumProof::from_proof_bytes(&bytes, expected.clone()) + .unwrap() + .verify() + .unwrap(); + let other = EthereumClaims { + da_commitment: Some([0; 32]), + ..EthereumClaims::default() + }; + let wrong = EthereumProof::from_proof_bytes(&bytes, other).unwrap(); + assert!(matches!(wrong.verify(), Err(AggregateVerifyError::Snark(_)))); + let claim = ([0; 32], [0; 32]); + let unsorted = EthereumClaims { + generic: vec![claim, claim], + ..expected.clone() + }; + let malformed = EthereumProof::from_proof_bytes(&bytes, unsorted); + assert!(matches!(malformed, Err(AggregateVerifyError::MalformedSignerSet))); + let trailing = EthereumProof::from_proof_bytes(&[&bytes[..], &[0]].concat(), expected); + assert!(matches!(trailing, Err(AggregateVerifyError::MalformedEncoding))); +} + +/// A node publishes a root it holds, and with blobs only their matrix's. +#[test] +fn invalid_da_selections_are_rejected_before_building_the_proof() { + let select = EthereumClaims { + da_commitment: Some([0xa5; 32]), + ..EthereumClaims::default() + }; + assert_eq!( + aggregate(&[], vec![], vec![], &[], Some(&select), LOG_INV_RATE).unwrap_err(), + AggregationError::BlobNotCovered + ); + assert_eq!( + aggregate(&[], vec![], vec![], &da_rows(1, 3), Some(&select), LOG_INV_RATE).unwrap_err(), + AggregationError::BlobsUnpublished + ); + let unpublished = EthereumClaims::default(); + assert_eq!( + aggregate(&[], vec![], vec![], &da_rows(1, 3), Some(&unpublished), LOG_INV_RATE).unwrap_err(), + AggregationError::BlobsUnpublished + ); +} + +/// A proof carries one blob matrix. A node without blobs passes on one child's +/// root, the first's by default or the one it selects, or none; a node with blobs +/// publishes their matrix's, whatever its children hold. +#[test] +fn a_node_passes_on_one_childs_da_root() { + leanvm_core::init_prover(); + let signers = get_lean_sphincs_signers(SMALL_LEAF_SIZE); + let mut children = vec![prove_leaf(&signers)]; + for (n_rows, seed) in [(1, 509), (2, 510)] { + let rows = da_rows(n_rows, seed); + children.push(aggregate(&[], vec![], signers.clone(), &rows, None, LOG_INV_RATE).unwrap()); + } + let (first, second) = ( + children[1].claims.da_commitment.unwrap(), + children[2].claims.da_commitment.unwrap(), + ); + assert_ne!(first, second); + let pass_on = |selected: Option>, blobs: &[u64]| { + let declare = selected.map(|da_commitment| EthereumClaims { + generic: vec![], + lean_sphincs: children[0].claims.lean_sphincs.clone(), + da_commitment, + }); + let node = aggregate(&children, vec![], vec![], blobs, declare.as_ref(), LOG_INV_RATE).unwrap(); + node.verify().unwrap(); + node.claims.da_commitment + }; + assert_eq!(pass_on(None, &[]), Some(first)); + assert_eq!(pass_on(Some(Some(second)), &[]), Some(second)); + assert_eq!(pass_on(Some(None), &[]), None); + let rows = da_rows(1, 511); + assert_eq!(pass_on(None, &rows), Some(lean_da::commit(&rows).unwrap().0)); + + // Passed on again, one level up. + let middle = aggregate(&children[1..], vec![], vec![], &[], None, LOG_INV_RATE).unwrap(); + let top = aggregate(&[children[0].clone(), middle], vec![], vec![], &[], None, LOG_INV_RATE).unwrap(); + top.verify().unwrap(); + assert_eq!(top.claims.da_commitment, Some(first)); +} + +/// The guest reads each child's root off its statement and publishes the one the +/// node picks, which must be its own with blobs: a child's root other than the one +/// its proof carries, a pick past the children, a child's root picked over the +/// node's blobs, or a membership challenge other than the root's fails the guest's run. +#[test] +fn forged_da_hints_are_rejected() { + leanvm_core::init_prover(); + let signers = get_lean_sphincs_signers(SMALL_LEAF_SIZE); + let child = aggregate(&[], vec![], signers.clone(), &da_rows(1, 7), None, LOG_INV_RATE).unwrap(); + let forgeries: [(Forgery, bool); 4] = [ + ( + Box::new(|hints: &mut Hints| hints.entry_mut("child_da", 0)[0] += F192::ONE), + false, + ), + ( + Box::new(|hints: &mut Hints| hints.entry_mut("da_pick", 0)[0] *= count(2)), + false, + ), + ( + Box::new(|hints: &mut Hints| hints.entry_mut("da_pick", 0)[0] = count(0)), + true, + ), + ( + Box::new(|hints: &mut Hints| hints.entry_mut("da_challenge", 0)[0] += F192::ONE), + true, + ), + ]; + for (forgery, with_blobs) in forgeries { + FORGE.with(|forge| *forge.borrow_mut() = Some(forgery)); + let blobs = if with_blobs { da_rows(1, 8) } else { vec![] }; + let result = aggregate(std::slice::from_ref(&child), vec![], vec![], &blobs, None, LOG_INV_RATE); + assert!( + matches!(result, Err(AggregationError::ProofError(_))), + "a forged DA hint is rejected" + ); + } +} + +/// A list hash's split is advice the guest then dispatches on, so the claim count +/// after the last group has to be bounded before the `match`: a negative +/// exponent that satisfies the product identity must fail the range check in +/// `list_split` itself, not reach the dispatch as a wild jump. +#[test] +fn list_split_rejects_a_negative_rest() { + let (helpers, _) = include_str!("../../guests/lean_ethereum.py") + .split_once("\ndef main():") + .unwrap(); + let source = format!( + r#"{helpers} +def main(): + n_g = hint_witness("n") + bigs, body, rest = list_split(n_g) + public = GEN ** 0 + assert public[1] == body + assert public[GEN] == rest + return +"# + ); + let guest = compile(&parse_with_replacements(&source, &placeholder_map(20)).unwrap()); + let run = |body: F192, rest: F192| { + let mut hints = Hints::default(); + hints.push("n", vec![count(6)]); + hints.push("signers_split", vec![count(0), body, rest]); + let mut program = guest.clone(); + hints.install(&mut program); + program.execute([body, rest]) + }; + // Six claims: no whole group, then six. + assert!(run(count(0), count(6)).is_ok()); + // One group and -11 claims after it: the product identity holds, the range + // check must not. + let negative = F192::from(g_pow(11).inv()); + let Err(err) = run(count(1), negative) else { + panic!("a negative last-block count was accepted"); + }; + assert!(err.to_string().contains("list_split"), "rejected elsewhere: {err}"); +} + +/// The guest accepts committed codewords, and rejects a matrix with one symbol of +/// one row flipped, though its root and vector are that matrix's: the row is no codeword. +#[test] +fn da_guest_checks_codeword_membership() { + leanvm_core::init_prover(); + let source = include_str!("../../guests/lean_ethereum.py"); + let (helpers, _) = source.split_once("\ndef main():").unwrap(); + let source = format!( + "{helpers}\ndef main():\n squares = exponent_tables()\n a, b, v0, v1 = da_verify(squares)\n digest = StackBuf(2)\n keccak([a, b], [v0, v1], digest)\n public = GEN ** 0\n assert public[1] == digest[0]\n assert public[GEN] == digest[1]\n return\n" + ); + let guest = compile(&parse_with_replacements(&source, &placeholder_map(20)).unwrap()); + let n_rows = 3usize; + let codewords = lean_da::encode_rows(&da_rows(3, 101)); + let run = |words: &[u64]| { + let (commitment, _) = lean_da::commit_codewords(words.to_vec()); + let mut hints = Hints::default(); + hints.push( + "da_shape", + vec![count(n_rows), count(n_rows.next_power_of_two().ilog2() as usize)], + ); + for j in 0..CELLS_PER_ROW { + for i in 0..n_rows { + let start = i * CODEWORD_SYMBOLS + j * CELL_SYMBOLS; + hints.push( + "da_symbols", + words[start..start + CELL_SYMBOLS] + .iter() + .map(|&w| F192::from(F64(w))) + .collect(), + ); + } + } + hints.push( + "da_challenge", + vec![from_scalar(verifier::da::membership_challenge(&commitment))], + ); + let mut program = guest.clone(); + hints.install(&mut program); + program.execute(hash_to_scalars(&verifier::da::digest(Some(commitment)))) + }; + assert!(run(&codewords).unwrap().unconstrained_reads.is_empty()); + for position in [0, BLOB_SYMBOLS + 5] { + let mut corrupted = codewords.clone(); + corrupted[CODEWORD_SYMBOLS + position] ^= 1; + assert!(run(&corrupted).is_err(), "a flip at {position} passed"); + } +} + +#[test] +fn invalid_blob_sizes_are_rejected() { + for symbols in [1, BLOB_SYMBOLS - 1, BLOB_SYMBOLS + 1, (DA_MAX_ROWS + 1) * BLOB_SYMBOLS] { + let rows = vec![0; symbols]; + assert!(matches!( + aggregate(&[], vec![], vec![], &rows, None, LOG_INV_RATE), + Err(AggregationError::InvalidBlobSize { symbols: n }) if n == symbols + )); + } +} + +/// The row count is a run-time parameter, so payloads of different heights +/// must prove against the *same* bytecode and each reach its own committer's +/// root. Powers of two and the counts between them alike: 3 pads to 4 and 5 to +/// 8, exercising two different arms of the tree dispatch and a non-empty gap. +#[test] +fn da_row_count_is_a_run_time_parameter() { + leanvm_core::init_prover(); + let signers = get_lean_sphincs_signers(SMALL_LEAF_SIZE); + for n_rows in [1usize, 3, 4, 5] { + let rows = da_rows(n_rows, 200 + n_rows as u64); + let node = aggregate(&[], vec![], signers.clone(), &rows, None, LOG_INV_RATE).expect("node aggregates"); + node.verify().expect("node verifies"); + let (commitment, _) = lean_da::commit(&rows).unwrap(); + assert_eq!(node.claims.da_commitment, Some(commitment), "{n_rows} rows"); + } +} + +/// A leaf carrying no payload publishes zeros for its root. +#[test] +fn no_payload_publishes_no_root() { + leanvm_core::init_prover(); + let signers = get_lean_sphincs_signers(SMALL_LEAF_SIZE); + let node = prove_leaf(&signers); + assert_eq!(node.claims.da_commitment, None); +} + +#[test] +fn aggregate_overlapping_signers() { + leanvm_core::init_prover(); + let signers = get_lean_sphincs_signers(40); + let left = prove_leaf(&signers[..25]); + let right = prove_leaf(&signers[15..]); + let node = aggregate(&[left, right], vec![], vec![], &[], None, LOG_INV_RATE).expect("node aggregates"); + node.verify().expect("node verifies"); + assert_eq!(node.claims.lean_sphincs.len(), 40); + assert!(node.claims.lean_sphincs.windows(2).all(|w| w[0] < w[1])); +} + +/// A child list walked by gaps (`child_claim_list_mono`) moves strictly forward. A +/// child may declare a claim twice, and a gap of `g^-1` would walk the copy back onto +/// the slot it just covered, with the same coverage mark, so write-once memory alone +/// would not see it: only the gap's `assert_gpow` does. +#[test] +fn a_child_walk_cannot_step_back() { + let (helpers, _) = include_str!("../../guests/lean_ethereum.py") + .split_once("\ndef main():") + .unwrap(); + let source = format!( + r#"{helpers} +def main(): + n_c = hint_witness("n") + table = HeapBuf(GEN ** 8) + hint_witness(table[0:8], "claims") + cover = HeapBuf(GEN ** 2) + d0, d1 = child_claim_list_digest(table, cover, GEN ** 3, GEN ** 2, n_c) + public = GEN ** 0 + assert public[1] == d0 + assert public[GEN] == d1 + return +"# + ); + let guest = compile(&parse_with_replacements(&source, &placeholder_map(20)).unwrap()); + let claim = |i: u64| [1, 2, 3, 4].map(|k| F192::new(i * 7919 + k, k, 0)); + let table: Vec = [claim(0), claim(1)].concat(); + let run = |child: &[[F192; 4]], gaps: &[F192]| { + let mut hints = Hints::default(); + hints.push("n", vec![count(child.len())]); + hints.push("claims", table.clone()); + hints.push("signers_split", list_split(child.len())); + hints.push("child_mono", vec![count(1)]); + for &gap in gaps { + hints.push("child_claim_gap", vec![gap]); + } + let mut program = guest.clone(); + hints.install(&mut program); + program.execute(claim_list_digest(child.iter().copied())) + }; + assert!(run(&[claim(0), claim(1)], &[count(0), count(0)]).is_ok()); + let back = F192::from(g_pow(1).inv()); + assert!( + run(&[claim(0), claim(0)], &[count(0), back]).is_err(), + "a walk revisited a slot" + ); +} + +#[test] +#[ignore] +fn aggregate_round_trips_through_the_wire() { + leanvm_core::init_prover(); + let signers = get_lean_sphincs_signers(2 * SMALL_LEAF_SIZE); + let left = prove_leaf(&signers[..SMALL_LEAF_SIZE]); + let right = prove_leaf(&signers[SMALL_LEAF_SIZE..]); + // Mixed, so both published lists are non-empty. + let node = aggregate( + &[left, right], + vec![], + get_lean_sphincs_signers(3), + &[], + None, + LOG_INV_RATE, + ) + .expect("node"); + node.verify().expect("the honest node verifies"); + + let received = EthereumProof::from_proof_bytes(&node.proof_bytes(), node.claims.clone()).expect("round trip"); + assert_eq!( + received.proof_bytes(), + node.proof_bytes(), + "the wire format round-trips, recomputed claim value included" + ); + received.verify().expect("the receiver's claims verify"); +} + +/// The all-zeros fast path in `DeferredClaim::recompute` must agree with the +/// full pass it replaces, or every leaf would verify against the wrong +/// statement. +#[test] +fn leaf_claim_matches_the_general_path() { + let leaf = DeferredClaim::leaf(); + assert_eq!( + leaf.bytecode_value, + mle_eval_par(stacked_bytecode(), &leaf.bytecode_point) + ); +} + +/// The scalar verifier's deferred values are the prover's, away from the leaf shortcut. +#[test] +fn scalar_deferred_claim_matches_the_prover() { + let mut rng = StdRng::seed_from_u64(7); + let mut point = |len: usize| -> Vec { + let limbs = (0..len).map(|_| rand::Rng::random::<(u64, u64, u64)>(&mut rng)); + limbs.map(|(c0, c1, c2)| F192::new(c0, c1, c2)).collect() + }; + let bytecode_point = point(bytecode_vars()); + assert_eq!( + DeferredClaim::recompute_scalar(bytecode_point.clone()), + DeferredClaim::recompute(bytecode_point) + ); +} + +/// Each claim's value read off its sums is the bytecode's evaluation, at a random point and a leaf's. +#[test] +fn bytecode_sums_give_the_bytecode_value() { + let mut rng = StdRng::seed_from_u64(11); + let limbs = (0..bytecode_vars()).map(|_| rand::Rng::random::<(u64, u64, u64)>(&mut rng)); + let points = [ + limbs.map(|(c0, c1, c2)| F192::new(c0, c1, c2)).collect(), + vec![F192::ZERO; bytecode_vars()], + ]; + for (sums, point) in bytecode_sums(&points).iter().zip(&points) { + assert_eq!(bytecode_value(sums, point), mle_eval_par(stacked_bytecode(), point)); + } +} + +/// The same for a leanSPHINCS claim, whose witness walk is equally fallible: a +/// counter that does not encode has no witness, and that is an error rather +/// than a panic inside the prover. +#[test] +fn malformed_raw_lean_sphincs_signature_is_an_error() { + let (public_key, signed, signature) = get_lean_sphincs_signers(1).pop().expect("one signer"); + // The bottom layer's counter follows the randomizer and the FORS openings. + let mut bytes = signature.to_bytes(); + bytes[lean_sphincs::RANDOMIZER_LEN + lean_sphincs::K * (1 + lean_sphincs::A) * lean_sphincs::N] ^= 1; + let signature = LeanSphincsSignature::from_bytes(&bytes); + assert!(lean_sphincs::verify(&public_key, &signed, &signature).is_err()); + let raw = vec![(public_key, signed, signature)]; + assert_eq!( + aggregate(&[], vec![], raw, &[], None, LOG_INV_RATE).err(), + Some(AggregationError::MalformedRawSignature) + ); +} + +/// `verify_sub` takes the inner program's bytecode log at run time: the guest's verification +/// constants are the same for every inner bytecode size, bar the final pc's baked stand-in and the +/// guest's OWN deferred-claim layout. +#[test] +fn verification_constants_do_not_depend_on_the_inner_bytecode_size() { + let own = ["BYTECODE_LOG", "BYTECODE_VARS", "DEFER_SIZE", "STMT_PAIRS", "AGG_CHILD_PAD", "COORD_CONST"] + .map(|k| format!("{k}_PLACEHOLDER")); + let (a, b) = (placeholder_map(11), placeholder_map(19)); + for (key, value) in &a { + if !own.contains(key) { + assert_eq!(Some(value), b.get(key), "{key}"); + } + } + let coords = |m: &std::collections::BTreeMap| m["COORD_CONST_PLACEHOLDER"].split(", ").map(str::to_string).collect::>(); + let final_pc: usize = a["FINAL_PC_COORD_PLACEHOLDER"].parse().unwrap(); + let (ca, cb) = (coords(&a), coords(&b)); + assert!((0..ca.len()).all(|i| i == final_pc || ca[i] == cb[i])); +} diff --git a/src/ffi/crates/rec_aggregation/src/hash_chain.rs b/src/ffi/crates/rec_aggregation/src/hash_chain.rs new file mode 100644 index 0000000..8215c6d --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/hash_chain.rs @@ -0,0 +1,126 @@ +//! End-to-end proof of an unrolled zkDSL Keccak hash chain. + +use std::time::Instant; + +use lean_compiler::{compile, parse}; +use leanvm_core::cpu::{prove, verify}; +use leanvm_core::vmhash::compress; +use primitives::{ + field::{F64, F192}, + pretty_f64, pretty_integer, +}; + +fn instruction_counts(source: &str, public_input: [F192; 2]) -> [usize; leanvm_core::cpu::Stats::TABLES.len()] { + compile(&parse(source).expect("parse")) + .execute(public_input) + .unwrap() + .rows() +} + +fn chain_source(steps: usize, unroll: usize) -> String { + assert!( + unroll >= 1 && steps.is_multiple_of(unroll), + "N must be a positive multiple of UNROLL" + ); + let blocks = steps / unroll; + let result_cell = 2 * blocks; + + let mut body = String::new(); + body.push_str(" b = i * i\n"); + body.push_str(" h0 = StackBuf(2)\n"); + body.push_str(" h0[0] = buff[b]\n"); + body.push_str(" h0[1] = buff[b * GEN]\n"); + for step in 1..=unroll { + body.push_str(&format!(" h{step} = StackBuf(2)\n")); + body.push_str(&format!( + " keccak(h{previous}[0:2], h{previous}[0:2], h{step})\n", + previous = step - 1 + )); + } + for word in 0..2 { + body.push_str(&format!(" buff[b * GEN ** {}] = h{unroll}[{word}]\n", 2 + word)); + } + + format!( + "def main():\n\ + \x20 buff = HeapBuf({size})\n\ + \x20 buff[1] = 0\n\ + \x20 buff[GEN] = 0\n\ + \x20 for i in mul_range(1, GEN ** {blocks}):\n\ + {body}\ + \x20 output = 1\n\ + \x20 output[1] = buff[GEN ** {result_cell}]\n\ + \x20 output[GEN] = buff[GEN ** {next_cell}]\n\ + \x20 return\n", + size = result_cell + 2, + next_cell = result_cell + 1, + ) +} + +#[test] +fn keccak_hash_chain() { + let env_usize = |key: &str, default: usize| { + std::env::var(key) + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(default) + }; + let unroll = env_usize("LEANVM_HASH_UNROLL", 4); + let steps = env_usize("LEANVM_HASH_N", 8); + assert!( + steps.is_multiple_of(unroll), + "LEANVM_HASH_N must be a multiple of LEANVM_HASH_UNROLL" + ); + + let mut digest = [F64::ZERO; 4]; + for _ in 0..steps { + digest = compress(digest, digest); + } + let public_input = [ + F192::new(digest[0].0, digest[1].0, 0), + F192::new(digest[2].0, digest[3].0, 0), + ]; + + let source = chain_source(steps, unroll); + let program = compile(&parse(&source).expect("parse")); + + let started = Instant::now(); + let (proof, stats) = prove(&program, public_input, leanvm_core::pcs::TEST_LOG_INV_RATE).expect("proves"); + let prove_time = started.elapsed(); + let started = Instant::now(); + verify(&program, &public_input, &proof).expect("hash-chain proof verifies"); + let verify_time = started.elapsed(); + + assert_eq!(instruction_counts(&source, public_input)[5], steps); + + println!( + "\nKeccak hash chain, N = {}, unroll = {}", + pretty_integer(steps), + pretty_integer(unroll) + ); + println!(" cycles (VM steps) : {}", pretty_integer(stats.cycles)); + for (name, &c) in leanvm_core::cpu::Stats::TABLES.iter().zip(&stats.rows) { + let pow = if c == 0 { + "0".to_string() + } else { + format!("2^{}", pretty_f64((c as f64).log2())) + }; + println!(" {name:<6} instructions : {pow}"); + } + println!( + " committed witness size : 2^{:.3}", + (stats.committed as f64).log2() + ); + let proof_bytes = proof.to_bytes().len(); + println!(" proof size : {:.1} KiB", proof_bytes as f64 / 1024.0); + println!(" proving : {prove_time:?}"); + println!( + " verifying : {} ms", + pretty_f64(verify_time.as_secs_f64() * 1000.0) + ); + let hashes_per_second = (steps as f64 / prove_time.as_secs_f64()).round() as u64; + println!( + " throughput : {} hashes/s", + pretty_integer(hashes_per_second) + ); +} diff --git a/src/ffi/crates/rec_aggregation/src/lib.rs b/src/ffi/crates/rec_aggregation/src/lib.rs new file mode 100644 index 0000000..c5503e7 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/lib.rs @@ -0,0 +1,14 @@ +pub mod aggregation; +/// The Keccak hash chain, proven end to end. A `src` module rather than its own +/// test binary so it shares the process with the other workloads. +#[cfg(test)] +mod hash_chain; +#[cfg(any(test, feature = "signers-cache"))] +pub mod signers_cache; + +pub use aggregation::{ + AggregateVerifyError, AggregationError, EthereumClaims, EthereumProof, LeanSphincsClaim, GenericClaim, RawGeneric, + LEANSTARK_DEPS_DOMAIN, MAX_GENERIC_BYTECODE_LOG, MAX_GENERIC_DEPENDENCIES, generic_verification_key, + generic_witness, verify_generic, + MAX_RECURSIONS, SnarkProveError, SnarkVerifyError, aggregate, aggregated_vk, deps_hash, deps_public_input_bytes, verify_deps_hash, aggregate_with_stats, warm_up, +}; diff --git a/src/ffi/crates/rec_aggregation/src/signers_cache.rs b/src/ffi/crates/rec_aggregation/src/signers_cache.rs new file mode 100644 index 0000000..623e964 --- /dev/null +++ b/src/ffi/crates/rec_aggregation/src/signers_cache.rs @@ -0,0 +1,306 @@ +//! Persistent cache for deterministic benchmark signatures, one file per +//! scheme. +//! +//! The cache grows as needed and is memoized in-process. Its filename binds the +//! parameters, hash construction, encoding predicate, and key derivation. Loaded +//! signatures are also verified, so stale entries are regenerated from the first +//! invalid one. + +use std::collections::BTreeMap; +use std::collections::hash_map::DefaultHasher; +use std::fs; +use std::hash::{Hash, Hasher}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::time::Instant; + +use primitives::{pretty_f64, pretty_integer}; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; +use signatures::lean_xmss::*; +use signatures::{lean_sphincs, lean_xmss}; + +type CachedSignature = (LeanXmssPublicKey, LeanXmssSignature); + +const SCHEMA_VERSION: u32 = 7; + +/// One cached leanXMSS signer, as fixed-size bytes: the public key, then the signature. +const LEAN_XMSS_RECORD: usize = PUB_KEY_SIZE + SIG_SIZE; + +/// The leaf indices every cached key signs at: mock keys (`lean_xmss::mock`), real +/// only over this range, which a full key's top tree would make far slower to +/// generate. +const KEY_START: LeafIndex = 3_000_000_000; +const KEY_END: LeafIndex = 3_000_000_015; + +/// The message [`get_signers`] signs. leanSPHINCS signers carry their own. +pub fn message() -> Message { + std::array::from_fn(|i| (i * 5 + 1) as u8) +} + +/// The message of cache `message_id`: distinct per id, and exactly [`message`] at 0. +pub(crate) fn message_for(message_id: u32) -> Message { + let mut msg = message(); + for (byte, delta) in msg.iter_mut().zip(message_id.to_le_bytes()) { + *byte ^= delta; + } + msg +} + +/// The leaf index signer `index` signs [`message_for`]`(message_id)` at, shifted per +/// message so one key signs messages fewer than 16 ids apart at distinct leaf indices. +fn leaf_index_of(index: usize, message_id: u32) -> LeafIndex { + let span = (KEY_END - KEY_START + 1) as usize; + KEY_START + ((index + message_id as usize) % span) as LeafIndex +} + +fn compute_signer(index: usize, message_id: u32) -> CachedSignature { + // The index over its full width: a one-byte seed repeats every 256 signers, + // and a repeated signer is invisible until something deduplicates the set, + // at which point a batch of 900 quietly becomes one of 256. + // One seed per signer, always over the one range KEY_START..=KEY_END: a seed + // must back one key only (`lean_xmss::mock`). + let mut seed = [10u8; 32]; + seed[..8].copy_from_slice(&(index as u64).to_le_bytes()); + let (sk, pk) = mock::key_gen_from_seed(seed, KEY_START, KEY_END); + let sig = mock::sign(&sk, &message_for(message_id), leaf_index_of(index, message_id)).expect("sign"); + (pk, sig) +} + +fn cache_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/signers-cache") +} + +/// The cache file of `message_id`, named after signer 0's key and signature, which +/// any change to the scheme, its parameters, key derivation or grinding changes. +fn cache_path(message_id: u32) -> PathBuf { + let mut hasher = DefaultHasher::new(); + ( + SCHEMA_VERSION, + LEAN_XMSS_RECORD, + message_id, + compute_signer(0, message_id), + ) + .hash(&mut hasher); + cache_dir().join(format!("lean_xmss_signers_{:016x}.bin", hasher.finish())) +} + +fn try_load_cache(path: &Path, message_id: u32) -> Option> { + let bytes = fs::read(path).ok()?; + let msg = message_for(message_id); + let mut signers = Vec::with_capacity(bytes.len() / LEAN_XMSS_RECORD); + for record in bytes.as_chunks::().0 { + let (pk, sig) = record.split_at(PUB_KEY_SIZE); + let pk = LeanXmssPublicKey::from_bytes(pk.try_into().unwrap()); + let sig = LeanXmssSignature::from_bytes(sig.try_into().unwrap()); + if sig.leaf_index() != leaf_index_of(signers.len(), message_id) || lean_xmss::verify(&pk, &msg, &sig).is_err() { + eprintln!( + "warning: signers cache {} is stale (signer {} no longer verifies); regenerating from there", + path.display(), + signers.len() + ); + break; + } + signers.push((pk, sig)); + } + Some(signers) +} + +fn save_cache(path: &Path, signers: &[CachedSignature]) { + let mut bytes = Vec::with_capacity(signers.len() * LEAN_XMSS_RECORD); + for (pk, sig) in signers { + bytes.extend_from_slice(&pk.to_bytes()); + bytes.extend_from_slice(&sig.to_bytes()); + } + write_cache(path, &bytes); +} + +fn write_cache(path: &Path, bytes: &[u8]) { + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + if let Err(error) = fs::write(path, bytes) { + eprintln!("warning: could not write signers cache to {}: {error}", path.display()); + } +} + +fn generate_range(start: usize, end: usize, message_id: u32) -> Vec { + let total = end - start; + let started = Instant::now(); + let mut signers = Vec::with_capacity(total); + for (done, index) in (start..end).enumerate() { + signers.push(compute_signer(index, message_id)); + print!( + "\r generating leanXMSS signers (one-time, then cached): {}/{}", + pretty_integer(done + 1), + pretty_integer(total) + ); + let _ = std::io::stdout().flush(); + } + println!( + "\r generated {} leanXMSS in {} s (cached to disk) ", + pretty_integer(total), + pretty_f64(started.elapsed().as_secs_f64()) + ); + signers +} + +static POOLS: Mutex>> = Mutex::new(BTreeMap::new()); + +/// The first `n` cached signers of [`message`]. +pub fn get_signers(n: usize) -> Vec { + get_signers_for(n, 0) +} + +/// The first `n` cached signers, signing [`message_for`]`(message_id)`, signer `i` at +/// `leaf_index_of(i, message_id)`: one cache file per message, the keys shared +/// across them. +pub(crate) fn get_signers_for(n: usize, message_id: u32) -> Vec { + let mut pools = POOLS.lock().unwrap(); + let pool = pools.entry(message_id).or_default(); + if pool.len() < n { + let path = cache_path(message_id); + if let Some(disk) = try_load_cache(&path, message_id) + && disk.len() > pool.len() + { + *pool = disk; + } + if pool.len() < n { + let mut fresh = generate_range(pool.len(), n, message_id); + pool.append(&mut fresh); + save_cache(&path, pool); + } + } + pool[..n].to_vec() +} + +/// A leanSPHINCS signer, generated the same way, with the message it signed. +/// Signing is stateless, so unlike leanXMSS there is no leaf index and no key range: +/// one key answers for every index. +type CachedSphincsSignature = ( + lean_sphincs::LeanSphincsPublicKey, + lean_sphincs::Message, + lean_sphincs::LeanSphincsSignature, +); + +/// Signer `index`'s own message, distinct from every other's and from the +/// leanXMSS ones, so a test that mixed them up would fail rather than pass. +pub(crate) fn lean_sphincs_message(index: usize) -> lean_sphincs::Message { + let mut msg = [0u8; lean_sphincs::MESSAGE_LEN]; + msg[..8].copy_from_slice(&(index as u64).to_le_bytes()); + msg[8..].copy_from_slice(&[0xC5; lean_sphincs::MESSAGE_LEN - 8]); + msg +} + +/// One cached leanSPHINCS signer, as fixed-size bytes: the scheme's own +/// serializations, so nothing here has to agree with a derived one. +const LEAN_SPHINCS_RECORD: usize = lean_sphincs::PUB_KEY_SIZE + lean_sphincs::MESSAGE_LEN + lean_sphincs::SIG_SIZE; + +fn compute_lean_sphincs_signer(index: usize) -> CachedSphincsSignature { + let mut rng = StdRng::seed_from_u64(0x5F1A_C500 ^ index as u64); + let (secret_key, public_key) = lean_sphincs::key_gen_from_seed(rng.random()); + let message = lean_sphincs_message(index); + let signature = lean_sphincs::sign(&secret_key, &message).expect("sign"); + (public_key, message, signature) +} + +/// Keyed, as the leanXMSS cache is, by signer 0's key, message and signature, so any +/// change to the scheme invalidates the file. Signing fans out, so this is computed +/// once, outside any `parallel` task. +fn lean_sphincs_cache_path() -> PathBuf { + static PATH: std::sync::OnceLock = std::sync::OnceLock::new(); + PATH.get_or_init(|| { + let (public_key, message, signature) = compute_lean_sphincs_signer(0); + let mut hasher = DefaultHasher::new(); + ( + SCHEMA_VERSION, + LEAN_SPHINCS_RECORD, + public_key, + message, + signature.to_bytes(), + // Signer 0's message has a zero index; signer 1's binds how it is encoded. + lean_sphincs_message(1), + ) + .hash(&mut hasher); + cache_dir().join(format!("lean_sphincs_signers_{:016x}.bin", hasher.finish())) + }) + .clone() +} + +fn try_load_lean_sphincs_cache() -> Option> { + let bytes = fs::read(lean_sphincs_cache_path()).ok()?; + let mut signers = Vec::with_capacity(bytes.len() / LEAN_SPHINCS_RECORD); + for record in bytes.as_chunks::().0 { + let (key_bytes, rest) = record.split_at(lean_sphincs::PUB_KEY_SIZE); + let (message_bytes, signature_bytes) = rest.split_at(lean_sphincs::MESSAGE_LEN); + let public_key = lean_sphincs::LeanSphincsPublicKey::from_bytes(key_bytes.try_into().unwrap()); + let message: lean_sphincs::Message = message_bytes.try_into().unwrap(); + let signature = lean_sphincs::LeanSphincsSignature::from_bytes(signature_bytes.try_into().unwrap()); + if lean_sphincs::verify(&public_key, &message, &signature).is_err() { + eprintln!( + "warning: signers cache {} is stale (signer {} no longer verifies); regenerating from there", + lean_sphincs_cache_path().display(), + signers.len() + ); + break; + } + signers.push((public_key, message, signature)); + } + Some(signers) +} + +fn save_lean_sphincs_cache(signers: &[CachedSphincsSignature]) { + let mut bytes = Vec::with_capacity(signers.len() * LEAN_SPHINCS_RECORD); + for (public_key, message, signature) in signers { + bytes.extend_from_slice(&public_key.to_bytes()); + bytes.extend_from_slice(message); + bytes.extend_from_slice(&signature.to_bytes()); + } + write_cache(&lean_sphincs_cache_path(), &bytes); +} + +static LEAN_SPHINCS_POOL: Mutex> = Mutex::new(Vec::new()); + +pub fn get_lean_sphincs_signers(n: usize) -> Vec { + let mut pool = LEAN_SPHINCS_POOL.lock().unwrap(); + if pool.len() < n { + if let Some(disk) = try_load_lean_sphincs_cache() + && disk.len() > pool.len() + { + *pool = disk; + } + // Key generation and signing build whole trees, and fan out + // internally, so this loop stays sequential. + let started = Instant::now(); + let missing = n.saturating_sub(pool.len()); + for index in pool.len()..n { + pool.push(compute_lean_sphincs_signer(index)); + print!( + "\r generating leanSPHINCS signers (one-time, then cached): {}/{}", + pretty_integer(index + 1 - (n - missing)), + pretty_integer(missing) + ); + let _ = std::io::stdout().flush(); + } + if missing > 0 { + println!( + "\r generated {} leanSPHINCS in {} s (cached to disk) ", + pretty_integer(missing), + pretty_f64(started.elapsed().as_secs_f64()) + ); + save_lean_sphincs_cache(&pool); + } + } + pool[..n].to_vec() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn signer_seed_uses_more_than_one_byte() { + assert_ne!(compute_signer(0, 0).0, compute_signer(256, 0).0); + } +} diff --git a/src/ffi/crates/signatures/Cargo.toml b/src/ffi/crates/signatures/Cargo.toml new file mode 100644 index 0000000..c6d0d60 --- /dev/null +++ b/src/ffi/crates/signatures/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "signatures" +version.workspace = true +edition.workspace = true +publish = false + +[lints] +workspace = true + +[features] +mock = ["upstream_signatures/mock"] + +[dependencies] +upstream_signatures.workspace = true +primitives.workspace = true +parallel.workspace = true +rand.workspace = true diff --git a/src/ffi/crates/signatures/LICENSE-APACHE b/src/ffi/crates/signatures/LICENSE-APACHE new file mode 100644 index 0000000..ea7ef84 --- /dev/null +++ b/src/ffi/crates/signatures/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for describing the origin of the Work and + reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Support. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or support. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Succinct Labs, Benedikt Bunz, and William Wang + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing permissions + and limitations under the License. \ No newline at end of file diff --git a/src/ffi/crates/signatures/LICENSE-MIT b/src/ffi/crates/signatures/LICENSE-MIT new file mode 100644 index 0000000..cafe401 --- /dev/null +++ b/src/ffi/crates/signatures/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 leanEthereum + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/ffi/crates/signatures/src/lean_sphincs/fors.rs b/src/ffi/crates/signatures/src/lean_sphincs/fors.rs new file mode 100644 index 0000000..c91676e --- /dev/null +++ b/src/ffi/crates/signatures/src/lean_sphincs/fors.rs @@ -0,0 +1,115 @@ +//! FORS: `k = 19` Merkle trees of `2^a = 512` secret leaves, one leaf opened per +//! tree at the index the message digest picks. +//! +//! An instance is keyed by the hypertree leaf the digest picks (FIPS 205's +//! split): tree address `htIdx >> h'`, key pair `htIdx mod 2^h'`, and the `k` +//! trees folded into one index space, node `j` at height `z` of tree `i` being +//! tree index `(i << (a - z)) | j`. + +use super::*; + +/// `(d >> (i·a)) & (2^a - 1)` for each tree `i`. +pub fn fors_indices(d: &[u8; 32]) -> [u32; K] { + std::array::from_fn(|i| digest_bits(d, i * A, A)) +} + +/// `htIdx = (d >> k·a) & (2^h - 1)`, the hypertree leaf. +pub fn ht_index(d: &[u8; 32]) -> u32 { + digest_bits(d, K * A, H) +} + +/// An address of the FORS instance at hypertree leaf `ht_idx`. +pub fn fors_adrs(ht_idx: u32, typ: u32, height: usize, tree_index: u32) -> Adrs { + Adrs::new( + 0, + u64::from(ht_idx >> SUBTREE_H), + typ, + ht_idx & ((1 << SUBTREE_H) - 1), + height as u32, + tree_index, + ) +} + +/// Leaf `leaf` of tree `i`: `F` of its secret at height 0. +pub fn fors_leaf(pp: &PublicParam, ht_idx: u32, i: usize, leaf: u32, secret: &Digest) -> Digest { + th( + pp, + &fors_adrs(ht_idx, FORS_TREE, 0, ((i as u32) << A) | leaf), + &[*secret], + ) +} + +/// A node of tree `i` at height `height >= 1`. +pub fn fors_node( + pp: &PublicParam, + ht_idx: u32, + i: usize, + height: usize, + index: u32, + left: &Digest, + right: &Digest, +) -> Digest { + th( + pp, + &fors_adrs(ht_idx, FORS_TREE, height, ((i as u32) << (A - height)) | index), + &[*left, *right], + ) +} + +/// The FORS public key: `T_k` of the `k` roots under `FORS_ROOTS`, 672 bytes. +pub fn fors_pk_of_roots(pp: &PublicParam, ht_idx: u32, roots: &[Digest; K]) -> Digest { + th(pp, &fors_adrs(ht_idx, FORS_ROOTS, 0, 0), roots) +} + +/// The FORS key a signature recovers. +pub fn fors_pk_from_sig( + pp: &PublicParam, + ht_idx: u32, + indices: &[u32; K], + secrets: &[Digest; K], + paths: &[[Digest; A]; K], +) -> Digest { + let roots = std::array::from_fn(|i| { + let leaf = fors_leaf(pp, ht_idx, i, indices[i], &secrets[i]); + paths[i].iter().enumerate().fold(leaf, |node, (z, sibling)| { + let (left, right) = if (indices[i] >> z) & 1 == 0 { + (node, *sibling) + } else { + (*sibling, node) + }; + fors_node(pp, ht_idx, i, z + 1, indices[i] >> (z + 1), &left, &right) + }) + }); + fors_pk_of_roots(pp, ht_idx, &roots) +} + +/// Leaf `leaf` of tree `i`'s secret, `PRF` under `FORS_PRF`. +fn fors_secret(sk_seed: &[u8; N], ht_idx: u32, i: usize, leaf: u32) -> Digest { + prf(sk_seed, &fors_adrs(ht_idx, FORS_PRF, 0, ((i as u32) << A) | leaf)) +} + +/// Open the instance at `ht_idx` at `indices`: the secrets, the paths, the key. +pub(crate) fn fors_sign( + pp: &PublicParam, + sk_seed: &[u8; N], + ht_idx: u32, + indices: &[u32; K], +) -> ([Digest; K], [[Digest; A]; K], Digest) { + let trees = parallel::map_collect(K, |i| { + let leaves = (0..1 << A) + .map(|leaf| fors_leaf(pp, ht_idx, i, leaf, &fors_secret(sk_seed, ht_idx, i, leaf))) + .collect(); + let levels = merkle_levels(leaves, |height, index, l, r| { + fors_node(pp, ht_idx, i, height, index, l, r) + }); + ( + fors_secret(sk_seed, ht_idx, i, indices[i]), + auth_path::(&levels, indices[i]), + levels[A][0], + ) + }); + let secrets = std::array::from_fn(|i| trees[i].0); + let paths = std::array::from_fn(|i| trees[i].1); + let roots = std::array::from_fn(|i| trees[i].2); + (secrets, paths, fors_pk_of_roots(pp, ht_idx, &roots)) +} diff --git a/src/ffi/crates/signatures/src/lean_sphincs/hash.rs b/src/ffi/crates/signatures/src/lean_sphincs/hash.rs new file mode 100644 index 0000000..9bcd0f6 --- /dev/null +++ b/src/ffi/crates/signatures/src/lean_sphincs/hash.rs @@ -0,0 +1,121 @@ +//! The framing every hash shares: Keccak-256 over 32-byte words, the FIPS 205 +//! address, the message digest, and the signer's secret derivation. + +use super::*; +use primitives::hash::hash; + +/// An `n`-byte value as the word it is hashed as: top-aligned, `v ‖ 0^16`. +pub fn word(v: &[u8; N]) -> [u8; 32] { + let mut out = [0; 32]; + out[..N].copy_from_slice(v); + out +} + +/// A hash output truncated to `n` bytes (the verifier's `N_MASK`). +pub fn truncate(digest: &[u8; 32]) -> Digest { + digest[..N].try_into().unwrap() +} + +// FIPS 205 address types (Table 1). +pub const WOTS_HASH: u32 = 0; +pub const WOTS_PK: u32 = 1; +pub const TREE: u32 = 2; +pub const FORS_TREE: u32 = 3; +pub const FORS_ROOTS: u32 = 4; +/// Secret derivation (signer-private: no verifier ever sees these addresses). +pub const WOTS_PRF: u32 = 5; +pub const FORS_PRF: u32 = 6; + +/// The 32-byte address (FIPS 205 §4.2, uncompressed), every field big-endian: +/// layer in bytes `0..4`, the 96-bit tree address in `4..16`, the type in +/// `16..20`, then three type-dependent words in `20..24`, `24..28`, `28..32` +/// (key pair, chain or height, hash address or tree index). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct Adrs { + pub layer: u32, + pub tree: u64, + pub typ: u32, + pub word1: u32, + pub word2: u32, + pub word3: u32, +} + +impl Adrs { + pub const fn new(layer: u32, tree: u64, typ: u32, word1: u32, word2: u32, word3: u32) -> Self { + Self { + layer, + tree, + typ, + word1, + word2, + word3, + } + } + + pub fn to_bytes(&self) -> [u8; 32] { + let mut out = [0; 32]; + out[0..4].copy_from_slice(&self.layer.to_be_bytes()); + out[8..16].copy_from_slice(&self.tree.to_be_bytes()); + out[16..20].copy_from_slice(&self.typ.to_be_bytes()); + out[20..24].copy_from_slice(&self.word1.to_be_bytes()); + out[24..28].copy_from_slice(&self.word2.to_be_bytes()); + out[28..32].copy_from_slice(&self.word3.to_be_bytes()); + out + } +} + +/// The tweakable hash `hash(pkSeed ‖ ADRS ‖ v_0 ‖ v_1 ‖ …)[..16]`, every +/// value a top-aligned word. `F` on one value (96 bytes), `H` on two (128), `T_l` +/// on the FORS roots or a WOTS key. +pub fn th(pp: &PublicParam, adrs: &Adrs, values: &[Digest]) -> Digest { + let mut input = Vec::with_capacity(64 + 32 * values.len()); + input.extend_from_slice(&word(pp)); + input.extend_from_slice(&adrs.to_bytes()); + for v in values { + input.extend_from_slice(&word(v)); + } + truncate(&hash(&input)) +} + +/// `H_msg = hash(0xFF…FF ‖ R ‖ pkSeed ‖ pkRoot ‖ M)`, 112 bytes, in full. +pub fn h_msg(pp: &PublicParam, root: &Digest, r: &Randomizer, m: &Message) -> [u8; 32] { + let mut input = [0u8; 112]; + input[0..32].copy_from_slice(&HMSG_DOMAIN); + input[32..48].copy_from_slice(r); + input[48..64].copy_from_slice(pp); + input[64..80].copy_from_slice(root); + input[80..112].copy_from_slice(m); + hash(&input) +} + +/// `(d >> shift) & (2^n - 1)`, `d` read as a big-endian 256-bit integer: bit `b` +/// is bit `b mod 8` of byte `31 - b/8`. +pub fn digest_bits(d: &[u8; 32], shift: usize, n: usize) -> u32 { + debug_assert!(n <= 32 && shift + n <= 256); + (0..n).fold(0, |acc, i| { + let b = shift + i; + acc | (u32::from(d[31 - b / 8] >> (b % 8) & 1) << i) + }) +} + +// --------------------------------------------------------------------------- +// The signer's secrets. Nothing here is checked by a verifier; it is the +// reference signer's derivation, kept so both produce the same signatures. + +/// `PRF(SK.seed, ADRS) = hash(SK.seed ‖ ADRS)[..16]`, `SK.seed` a +/// top-aligned word and `ADRS` of type [`WOTS_PRF`] or [`FORS_PRF`]. +pub(crate) fn prf(sk_seed: &[u8; N], adrs: &Adrs) -> Digest { + let mut input = [0u8; 64]; + input[..32].copy_from_slice(&word(sk_seed)); + input[32..].copy_from_slice(&adrs.to_bytes()); + truncate(&hash(&input)) +} + +/// `R = hash(SK.prf ‖ M)[..16]`: deterministic, and secret-keyed so nobody +/// else can steer messages onto chosen FORS instances. +pub(crate) fn randomizer(sk_prf: &[u8; N], m: &Message) -> Randomizer { + let mut input = [0u8; 64]; + input[..32].copy_from_slice(&word(sk_prf)); + input[32..].copy_from_slice(m); + truncate(&hash(&input)) +} diff --git a/src/ffi/crates/signatures/src/lean_sphincs/mod.rs b/src/ffi/crates/signatures/src/lean_sphincs/mod.rs new file mode 100644 index 0000000..9ff98ff --- /dev/null +++ b/src/ffi/crates/signatures/src/lean_sphincs/mod.rs @@ -0,0 +1,106 @@ +//! SPHINCS+ in the NiceTry "SPHINCS- v2" profile: the stateless scheme whose EVM +//! verifier is `SphincsVerifier_v2.sol` (RivaLabs-Core/Post-Quantum-AA-Infra), +//! specified in `doc/sphincs/main.tex`. +//! +//! Standard FORS under a five-layer standard WOTS+ hypertree over Keccak-256: +//! `n = 16`, `h = 20`, `d = 5`, `h' = 4`, `a = 9`, `k = 19`, `w = 16`, +//! `l = 32 + 3 = 35`. No grinding anywhere. A public key is `(pkSeed, pkRoot)`, +//! a signature 6,176 bytes. +//! +//! Every tweakable hash is `keccak256` of 32-byte words: an `n`-byte value `v` enters as +//! `v ‖ 0^16` (top-aligned in a `bytes32`), the 32-byte FIPS 205 address as is, +//! and outputs are truncated to their first 16 bytes. Digests are read as +//! big-endian 256-bit integers, fields LSB-first (`(d >> (i·a)) & (2^a - 1)`). +//! +//! The verifier is the specification. The signer mirrors the reference signer +//! (`scripts/sphincs_v2_reference.py`) byte for byte: from the same three seeds +//! ([`sign_with_seeds`]) both produce the same signature. A real key is one +//! 32-byte master secret the three seeds are derived from ([`key_gen_from_seed`]). + + +mod hash; +pub use hash::*; +mod wots; +pub use wots::*; +mod fors; +pub use fors::*; +mod scheme; +pub use scheme::*; + +/// `n`: hash value and Merkle node length, in bytes. +pub const N: usize = crate::N; +pub type Digest = [u8; N]; + +/// `pkSeed`, the public parameter every hash is keyed by. +pub const PUBLIC_PARAM_LEN: usize = 16; +pub type PublicParam = [u8; PUBLIC_PARAM_LEN]; + +/// The master secret a key is derived from ([`key_gen_from_seed`]). +pub const MASTER_SECRET_LEN: usize = 32; +pub type MasterSecret = [u8; MASTER_SECRET_LEN]; +// The names upstream leanSPHINCS callers use. +pub const SEED_LEN: usize = MASTER_SECRET_LEN; +pub type Seed = MasterSecret; +pub type PkSeed = PublicParam; + +/// `R`, the per-signature randomizer the message digest is computed under. +pub const RANDOMIZER_LEN: usize = 16; +pub type Randomizer = [u8; RANDOMIZER_LEN]; + +/// The message to sign: a `bytes32` (an ERC-4337 `userOpHash`). +pub const MESSAGE_LEN: usize = 32; +pub type Message = [u8; MESSAGE_LEN]; + +// WOTS+. +/// `log2 w`: bits per digit. +pub const LOG_W: usize = 4; +/// `w`: one more than the steps of a hash chain. +pub const W: usize = 1 << LOG_W; +/// `len1`: message digits, `8n / log w`. +pub const LEN1: usize = 8 * N / LOG_W; +/// `len2`: checksum digits, `floor(log2(len1 (w-1)) / log2 w) + 1`. +pub const LEN2: usize = 3; +/// `l`: chains, one per digit. +pub const L: usize = LEN1 + LEN2; +/// The largest checksum, `len1 (w - 1)`: its value when every digit is zero. +pub const MAX_CSUM: usize = LEN1 * (W - 1); + +// The hypertree. +/// `d`: hypertree layers, numbered from the bottom (layer 0 signs the FORS key). +pub const D: usize = 5; +/// `h' = h/d`: the Merkle tree height of each layer. +pub const SUBTREE_H: usize = 4; +/// `h`: total height, so `2^h` FORS instances. +pub const H: usize = D * SUBTREE_H; + +// FORS. +/// `a`: log2 of the leaves in one FORS tree. +pub const A: usize = 9; +/// `k`: FORS trees. +pub const K: usize = 19; + +/// `H_msg`'s domain word, `0xFF…FF`: 112 bytes of hash input where every +/// tweakable hash takes 96 or 128. +pub const HMSG_DOMAIN: [u8; 32] = [0xFF; 32]; + +/// `(pkSeed, pkRoot)`. +pub const PUB_KEY_SIZE: usize = N + PUBLIC_PARAM_LEN; +/// A secret key is its master secret; the seeds and the root are derived. +pub const SECRET_KEY_SIZE: usize = MASTER_SECRET_LEN; +/// One hypertree layer of a signature: the chains, then the path. +pub const LAYER_SIZE: usize = L * N + SUBTREE_H * N; +/// `R ‖ k × [secret ‖ path] ‖ d layers`. +pub const SIG_SIZE: usize = RANDOMIZER_LEN + K * N + K * A * N + D * LAYER_SIZE; + +/// Hash calls a verification makes outside the chains: `H_msg`, the FORS +/// leaves, nodes and roots, and per layer the WOTS key and the path. +/// Each chain adds `w - 1 - digit` calls, data-dependent. +pub const VERIFY_FIXED_HASHES: usize = 1 + K * (1 + A) + 1 + D * (1 + SUBTREE_H); + + +const _: () = assert!(LEN1 == 32 && MAX_CSUM < 1 << (LOG_W * LEN2) && MAX_CSUM >= 1 << (LOG_W * (LEN2 - 1))); +const _: () = assert!(K * A + H <= 256); +const _: () = assert!(PUB_KEY_SIZE == 32); +const _: () = assert!(LAYER_SIZE == 624); +const _: () = assert!(SIG_SIZE == 6176); +const _: () = assert!(VERIFY_FIXED_HASHES == 217); diff --git a/src/ffi/crates/signatures/src/lean_sphincs/scheme.rs b/src/ffi/crates/signatures/src/lean_sphincs/scheme.rs new file mode 100644 index 0000000..3dff8d5 --- /dev/null +++ b/src/ffi/crates/signatures/src/lean_sphincs/scheme.rs @@ -0,0 +1,374 @@ +//! The hypertree and the three algorithms: `d = 5` layers of height-4 Merkle +//! trees over WOTS+ keys, layer 0 signing the FORS key the message digest +//! picks, the top layer's single root being the public key. +//! +//! The digest `H_msg` names a hypertree leaf `htIdx` and the FORS indices, so a +//! key answers for all `2^h` leaves with nothing reserved or spent: the scheme +//! is stateless. + + +use super::*; + +/// `(pkRoot, pkSeed)`. Ordered lexicographically on [`Self::flatten`], which is +/// how an aggregate sorts the claims on one message. The on-chain form is two +/// `bytes32`, each value top-aligned (`v ‖ 0^16`). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct LeanSphincsPublicKey { + pub root: Digest, + pub public_param: PublicParam, +} + +impl LeanSphincsPublicKey { + pub fn pk_seed(&self) -> &PkSeed { + &self.public_param + } + + pub fn root(&self) -> &Digest { + &self.root + } + + /// `pkSeed | pkRoot`, upstream's claim order. + pub fn to_bytes(&self) -> [u8; PUB_KEY_SIZE] { + let mut out = [0; PUB_KEY_SIZE]; + out[..N].copy_from_slice(&self.public_param); + out[N..].copy_from_slice(&self.root); + out + } + + pub fn from_bytes(bytes: &[u8; PUB_KEY_SIZE]) -> Self { + Self { + public_param: bytes[..N].try_into().unwrap(), + root: bytes[N..].try_into().unwrap(), + } + } + + /// The verifier's `(pkSeed, pkRoot)` arguments. Always canonical (low 128 + /// bits zero), which the EVM verifier requires. + pub fn to_bytes32(&self) -> ([u8; 32], [u8; 32]) { + (word(&self.public_param), word(&self.root)) + } +} + +/// A secret key: its 32-byte master secret and nothing else. FIPS 205's three +/// seeds and the public key are derived from it (see [`key_gen_from_seed`]). +#[derive(Clone)] +pub struct LeanSphincsSecretKey { + master: MasterSecret, +} + +impl std::fmt::Debug for LeanSphincsSecretKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LeanSphincsSecretKey").finish_non_exhaustive() + } +} + +impl LeanSphincsSecretKey { + /// SECRET KEY MATERIAL: the master secret. + pub fn to_bytes(&self) -> [u8; SECRET_KEY_SIZE] { + self.master + } + + /// Inverse of [`Self::to_bytes`]. + pub fn from_bytes(bytes: &[u8; SECRET_KEY_SIZE]) -> Self { + Self { master: *bytes } + } + + /// The public key, derived: the root is the top layer's tree, `2^h'` WOTS + /// keys, so this costs about a fifth of a signature. + pub fn public_key(&self) -> LeanSphincsPublicKey { + let (sk_seed, _, public_param) = derive_seeds(&self.master); + LeanSphincsPublicKey { + root: root_of(&public_param, &sk_seed), + public_param, + } + } +} + +/// One hypertree layer of a signature. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HtLayer { + /// `σ`: chain `i` at position `digit_i`. + pub chains: [Digest; L], + /// The Merkle authentication path, bottom first. + pub path: [Digest; SUBTREE_H], +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LeanSphincsSignature { + /// `R`. + pub randomizer: Randomizer, + /// One opened leaf secret per FORS tree. + pub fors_secrets: [Digest; K], + /// Each tree's authentication path, bottom first. + pub fors_paths: [[Digest; A]; K], + /// Layer 0 first. + pub layers: [HtLayer; D], +} + +impl LeanSphincsSignature { + /// The verifier's blob, exactly [`SIG_SIZE`] bytes: `R ‖ k × [secret ‖ a path + /// nodes] ‖ d × [l chains ‖ h' path nodes]`. + pub fn to_bytes(&self) -> [u8; SIG_SIZE] { + let mut out = [0; SIG_SIZE]; + let mut at = 0; + let mut put = |bytes: &[u8]| { + out[at..at + bytes.len()].copy_from_slice(bytes); + at += bytes.len(); + }; + put(&self.randomizer); + for (secret, path) in self.fors_secrets.iter().zip(&self.fors_paths) { + put(secret); + path.iter().for_each(|s| put(s)); + } + for layer in &self.layers { + layer.chains.iter().for_each(|s| put(s)); + layer.path.iter().for_each(|s| put(s)); + } + debug_assert_eq!(at, SIG_SIZE); + out + } + + pub fn from_bytes(bytes: &[u8; SIG_SIZE]) -> Self { + let at = std::cell::Cell::new(0); + let take = |len: usize| { + let s = &bytes[at.get()..at.get() + len]; + at.set(at.get() + len); + s + }; + let digest = || -> Digest { take(N).try_into().unwrap() }; + let randomizer = digest(); + let trees: [(Digest, [Digest; A]); K] = std::array::from_fn(|_| (digest(), std::array::from_fn(|_| digest()))); + let fors_secrets = trees.map(|(secret, _)| secret); + let fors_paths = trees.map(|(_, path)| path); + let layers = std::array::from_fn(|_| { + let chains = std::array::from_fn(|_| digest()); + let path = std::array::from_fn(|_| digest()); + HtLayer { chains, path } + }); + debug_assert_eq!(at.get(), SIG_SIZE); + Self { + randomizer, + fors_secrets, + fors_paths, + layers, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum LeanSphincsVerifyError { + /// The hypertree walk does not reach the key's root: the only way a + /// well-formed signature fails, there being no grinding predicate to check. + RootMismatch, +} + +impl std::fmt::Display for LeanSphincsVerifyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::RootMismatch => write!(f, "the hypertree walk does not reach the key's root"), + } + } +} + +impl std::error::Error for LeanSphincsVerifyError {} + +/// `(tree, leaf)` on layer `layer` of hypertree leaf `ht_idx`. +pub fn ht_position(ht_idx: u32, layer: usize) -> (u64, u32) { + let below = ht_idx >> (layer * SUBTREE_H); + (u64::from(below >> SUBTREE_H), below & ((1 << SUBTREE_H) - 1)) +} + +/// `Gen` on a fresh key: the master secret comes from `rng`. +pub fn key_gen() -> (LeanSphincsSecretKey, LeanSphincsPublicKey) { + key_gen_from_seed(rand::random()) +} + +/// `SK.seed`, `SK.prf` and `PK.seed` from a master secret: each +/// `hash(tag ‖ master)[..16]`, `tag` one of `"SPHINCS-v2 SK.seed"`, +/// `"SPHINCS-v2 SK.prf"`, `"SPHINCS-v2 PK.seed"`: three hash domains, so the +/// seeds are independent. (Signer-private: no verifier sees the derivation.) +fn derive_seeds(master: &MasterSecret) -> ([u8; N], [u8; N], PublicParam) { + let seed = |tag: &[u8]| truncate(&primitives::hash::hash(&[tag, master].concat())); + ( + seed(b"SPHINCS-v2 SK.seed"), + seed(b"SPHINCS-v2 SK.prf"), + seed(b"SPHINCS-v2 PK.seed"), + ) +} + +/// Deterministic `Gen` on the master secret `master`. The three seeds are each +/// `hash(tag ‖ master)[..16]`, `tag` one of `"SPHINCS-v2 SK.seed"`, +/// `"SPHINCS-v2 SK.prf"`, `"SPHINCS-v2 PK.seed"`. +pub fn key_gen_from_seed(master: MasterSecret) -> (LeanSphincsSecretKey, LeanSphincsPublicKey) { + let sk = LeanSphincsSecretKey { master }; + let pk = sk.public_key(); + (sk, pk) +} + +/// The public root: the top layer's single tree. +fn root_of(public_param: &PublicParam, sk_seed: &[u8; N]) -> Digest { + subtree_levels(public_param, sk_seed, (D - 1) as u32, 0)[SUBTREE_H][0] +} + +/// Sign. Deterministic and stateless. +/// +/// The key's root is rebuilt first ([`LeanSphincsSecretKey::public_key`]), since +/// the message digest depends on it. +pub fn sign(sk: &LeanSphincsSecretKey, message: &Message) -> Result { + let (sk_seed, sk_prf, public_param) = derive_seeds(&sk.master); + Ok(sign_with_seeds(sk_seed, sk_prf, public_param, message).1) +} + +/// Signing cannot fail (no grinding); kept for upstream's API. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum LeanSphincsSignError {} + +impl std::fmt::Display for LeanSphincsSignError { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self {} + } +} + +impl std::error::Error for LeanSphincsSignError {} + +#[cfg(test)] +mod tests { + use super::*; + + fn field(json: &str, key: &str) -> Vec { + let at = json.find(&format!("\"{key}\"")).unwrap(); + let rest = &json[at + key.len() + 2..]; + let open = rest.find('"').unwrap() + 1; + let close = open + rest[open..].find('"').unwrap(); + let hex = rest[open..close].trim_start_matches("0x"); + (0..hex.len() / 2) + .map(|i| u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).unwrap()) + .collect() + } + + /// NiceTry's reference vector verifies, and its seeds reproduce key and signature byte for byte. + #[test] + fn matches_the_reference_vector() { + let json = include_str!("sphincs-v2-reference-0.json"); + let (pk_seed, pk_root) = (field(json, "pkSeed"), field(json, "pkRoot")); + let pk = LeanSphincsPublicKey { + root: pk_root[..N].try_into().unwrap(), + public_param: pk_seed[..N].try_into().unwrap(), + }; + let message: Message = field(json, "message").try_into().unwrap(); + let blob = field(json, "signature"); + assert_eq!(blob.len(), SIG_SIZE); + let reference = LeanSphincsSignature::from_bytes(blob.as_slice().try_into().unwrap()); + assert_eq!(reference.to_bytes().to_vec(), blob); + verify(&pk, &message, &reference).unwrap(); + let mut other = message; + other[31] ^= 1; + assert_eq!(verify(&pk, &other, &reference), Err(LeanSphincsVerifyError::RootMismatch)); + let mut bad = blob.clone(); + bad[100] ^= 1; + let bad = LeanSphincsSignature::from_bytes(bad.as_slice().try_into().unwrap()); + assert_eq!(verify(&pk, &message, &bad), Err(LeanSphincsVerifyError::RootMismatch)); + let (derived, signature) = sign_with_seeds([0x11; N], [0x22; N], pk.public_param, &message); + assert_eq!(derived, pk); + assert_eq!(signature, reference); + } +} + +/// Key generation and signing on the three seeds given directly, as FIPS 205 +/// and NiceTry's reference signer (`scripts/sphincs_v2_reference.py`) state them. +/// For reproducing reference vectors: a key made this way has no master secret, +/// so use [`key_gen`] / [`key_gen_from_seed`] for real keys. +pub fn sign_with_seeds( + sk_seed: [u8; N], + sk_prf: [u8; N], + public_param: PublicParam, + message: &Message, +) -> (LeanSphincsPublicKey, LeanSphincsSignature) { + let root = root_of(&public_param, &sk_seed); + let signature = sign_with(&public_param, &root, &sk_seed, &sk_prf, message); + (LeanSphincsPublicKey { root, public_param }, signature) +} + +fn sign_with( + pp: &PublicParam, + root: &Digest, + sk_seed: &[u8; N], + sk_prf: &[u8; N], + message: &Message, +) -> LeanSphincsSignature { + let randomizer = randomizer(sk_prf, message); + let d = h_msg(pp, root, &randomizer, message); + let ht_idx = ht_index(&d); + let (fors_secrets, fors_paths, fors_pk) = fors_sign(pp, sk_seed, ht_idx, &fors_indices(&d)); + + let mut node = fors_pk; + let layers = std::array::from_fn(|layer| { + let (tree, leaf) = ht_position(ht_idx, layer); + let chains = wots_sign(pp, sk_seed, layer as u32, tree, leaf, &node); + let levels = subtree_levels(pp, sk_seed, layer as u32, tree); + node = levels[SUBTREE_H][0]; + HtLayer { + chains, + path: auth_path(&levels, leaf), + } + }); + debug_assert_eq!(&node, root); + + LeanSphincsSignature { + randomizer, + fors_secrets, + fors_paths, + layers, + } +} + +/// Everything a verification computes on the way to the root, for provers that +/// replay it: the digest, what it selects, and what each layer signs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VerifyTrace { + pub digest: [u8; 32], + pub ht_idx: u32, + pub fors_indices: [u32; K], + /// `signed[layer]`: the node layer `layer`'s WOTS key signs (the FORS key on + /// layer 0); `signed[d]` is the root reached. + pub signed: [Digest; D + 1], + /// Each layer's WOTS digits, the checksum's last. + pub digits: [[u8; L]; D], +} + +/// Replay a verification up to the root comparison. +pub fn verify_trace(pk: &LeanSphincsPublicKey, message: &Message, signature: &LeanSphincsSignature) -> VerifyTrace { + let pp = &pk.public_param; + let digest = h_msg(pp, &pk.root, &signature.randomizer, message); + let indices = fors_indices(&digest); + let ht_idx = ht_index(&digest); + let mut signed = [[0; N]; D + 1]; + let mut digits_of = [[0; L]; D]; + signed[0] = fors_pk_from_sig(pp, ht_idx, &indices, &signature.fors_secrets, &signature.fors_paths); + for (layer, sig) in signature.layers.iter().enumerate() { + let (tree, leaf) = ht_position(ht_idx, layer); + digits_of[layer] = digits(&signed[layer]); + let wots_pk = wots_pk_from_sig(pp, layer as u32, tree, leaf, &signed[layer], &sig.chains); + signed[layer + 1] = tree_fold(pp, layer as u32, tree, leaf, wots_pk, &sig.path); + } + VerifyTrace { + digest, + ht_idx, + fors_indices: indices, + signed, + digits: digits_of, + } +} + +/// `Ver`, `SphincsVerifier_v2.verify` on the signature's blob. +pub fn verify( + pk: &LeanSphincsPublicKey, + message: &Message, + signature: &LeanSphincsSignature, +) -> Result<(), LeanSphincsVerifyError> { + if verify_trace(pk, message, signature).signed[D] == pk.root { + Ok(()) + } else { + Err(LeanSphincsVerifyError::RootMismatch) + } +} diff --git a/src/ffi/crates/signatures/src/lean_sphincs/sphincs-v2-reference-0.json b/src/ffi/crates/signatures/src/lean_sphincs/sphincs-v2-reference-0.json new file mode 100644 index 0000000..e997283 --- /dev/null +++ b/src/ffi/crates/signatures/src/lean_sphincs/sphincs-v2-reference-0.json @@ -0,0 +1,17 @@ +{ + "params": { + "n": 16, + "h": 20, + "d": 5, + "hPrime": 4, + "a": 9, + "k": 19, + "w": 16, + "l": 35, + "signatureLength": 6176 + }, + "pkSeed": "0x5eed5eed5eed5eed5eed5eed5eed5eed00000000000000000000000000000000", + "pkRoot": "0xb770f57a2c5913e5294a4ccf28e7240e00000000000000000000000000000000", + "message": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "signature": "0x89065d868f95b9d150500efcdef9cab81310413b0d95824907da5094ec9746e7361e44367dc068f4ad6c4ad788ffbcd27d2d69fe2c620a0bb631e7669a9dc8e76b140b3a1d02f47ac2111f7484a7d0b04e07b9d3f4e65b7f3330331992f08ae317c9eecad35fbbd83f9266b4ccf66c4c8c08e51de4e550f3f94aa825e3050b34d97dbd07db3e4c3456668af2398600c812c35ac998f277a73ae09aa79f3c855634d236b2c3207296920e9b9d0f89b1355040bf0a59c0699e8816a5cfb78a078d0d1f46bfccff18b0ad77586ecd5f53d2af762660ea4e245d6399cbcd1ff29ac45a9371abf3d6142a568b85bd48402f8600f158a6029f0f6792e5d47692d8954e6d43172cb0939980357d444d99938e491eaacdb6196fe5d8daebbdda4d18163eef587ff373de10a638ded5fa07ccfe82f8803a9cd1fabeee1db7e7e40c7fff0524c0e9f34b1a42ab344688de6d799d618dc83636ca76eb465170fad46d97be46948627a84ffaf8afa900a1f3743e44417dd4e256726c1fbd47e5ce4b4b414b136ab1cf198ff45d60f3dbe1d3eb0a61843d3730b97df487bb721e3b314d0ffdc669553336564f0f94fdc29d12e6479b252ec19075e5cdaeb37d77e0d9fcddc1617cafebfc0815294df329f21b38c5299c6c8d35205b2b0dae52f00c3a473f7a7e1408b35126b3b3569241a54f6aa23a5b81eeca171f9390456ee75c09debc33604ebd190d811437e355a1e88e4144b85e08ee6f70a72dc34fb3fd6b198c09c44f3fed4e887381215396fd818fcd00acdd6bae97fbf9a2f4748c307cdcec529360a0a0d1726db2719556e0f587c1ea2f8b0543db58d30dc3d01a76b7bbc6b872507110647630227160064a59d619714c990e06926654398a7951b154d7a1f5de87e4bfc2b02b65274fa9a0ddef3154a69f3bb5a74765f7bbb7286ebc8d1a7a80cb4fb20cbf54cf5f594df1734be2f2e55bac82873605f1489b1facafe153d9ba1d4ba5727445125989be10f9adacb792ac6a18cd9e4347f292cf1c63f49e6f0be8d5f5c414f0a6f9bdb7e0e40fd7009310b63eafb8809eec71e5b9a72f72191447a69c661234b45a62388533675f8e3e7ea5895f144b3212964c1975b148513ba5fb3952c1ce02cc14ab38987ad3dc8c5aa880aa32da943f8e87f20aa45607b0a9892dac94522c5a43eaf33eb450e899e1cf7e8d81bfc49c2c66a26d18d4f56306d9be841a81a824e119734a995bfeb73c4ee28f984a79bd33aad452cf6091e7e25c53d6fa1a19fc2e3f78eb6854d9ef0bb876a2d496bb146517595e80f05c21b955b9b333f440a73234cb200a22bc0027c716ac4ae9210a38381427aca1ed11c17fa6dc5c460906cfdb4d4a56aa4c3dbd9687fa47f82a8023c768b5671a88b3de0a08de86c0a02d172207dd0b72b2ecaecfc6809cf68b94676c22d4fdb036dc014420efbc9b37bec26956d6d8a00c793f98c180d3e3469b44c48bda02afda11fc6a0d7a70a3f3d241c7c64cc278a4766a1e694bac73ad1d8bb2fcc5e547fc43dc7780a76e33f8b45c647b72d5583966be7b6535d22c544d975288a606014fcd92d4790477c260fc2614bab6c5eb61609491ce3c5f5a67999b11c961181f853503088be15a0f0f4a5cde12fd3c2e8e6f7cd9e3af1db4dab2fc0f269319d285f43ff0b723d76da9b111070ab7acc1880d079fc3b52f3c3357f4945952d2d84f193675284fc83480d8ae8139c6709b3860a4d34779ecbc81a07a6ad39381786c2f120943d126918e3850b3dc1e4d4bb6a703a3c8bcce63b37ce7db9adfe24bb17a5efbd965ad7c85774d8f4f3d43e05f5d210297b44bf886ca894bbef0d507cf75f4122d82d823369850ac23f4e39c26faef3e0b5f6f4187e145f8d108edbe0b506d33925d910ac9af5c8a91de282c0b5748892caf1186335114360dcaa66aacc92fad55bf7c4acbd41b5b081344d07cf9f1d9df020951bb0fd6c8b73f06b89c57b70b56c8457e1b1092be586d358b35f05134318073cced8289c3dd32b1344267a30fd905464c3206dda832a73268ab983aa402b2443229b41531b0140d317a88987ee4e9735d6e1abcf84859689b0436817bb147a40dde6519dd249c8e846650452384d65688407a00c830ea63badf7c6d07d6cc54e605c872964a2830650cac77bc69988c659b9bc142543c55d957bf39f68306d90375f913a55e065b3c849b1c881d886651fb672a032bbbfefa7aebd0f6433703701e8abd6343bc5ddfeefe196a321a2a0f35e308a610d9e3125a07cbb934f912370d265defdc1bff974cf175383d8c24e15de166d3169852f480465157649532c5e07f3e1d700835d9cc0fb89025af87e64605febad253fa30e3287593b542737c61bfb9b02e2b95282f2543c9754b0c4a3de170fb6496087d862eea174f5cb851e0f69c8d0f902704fae40a3ce98ae552f413bd427f3f6f3c91792ced1719e42e50fcc4d835f851d949969f65d873f3476e0c24f41d7d03060a6b9fb2a0370e5ea62dd98fa99486c14cd6a718ccd68c7b2a6f2f50d6a9cb14ff68d21eeb346fa982ace70fddb280aadadc554e25736b164b157614bcbcfe0916de087af1926eb8395e15f8e872b5c80dcbf2d4bd654991752b361e9edb16fc7b23109452056c69db4ed823f63ac3911d345bc4838b57ce648903a0a73c1f9d915236eaadcfe1d5e5eb9560c3e248aa5c051b8a5cebed846882fafd275374ff3a4d90481d64b40e71cf8feb0088b0fbcc8b930c8558ef74417c40434fbd41ab7ce952a50224a9177b4993ea1bd748b781c8cf143274ce4618a1aa551774712dc2ae1120dfa6adfb9dbaa34256249956e864cf7833fef481ecd8d1fbaade9ad5e94ce7dc71f3215671fed5904d03fbf3d7a922f36384a410c775467d421937c668c8cb56c7f3bd545d465543de1eb2d95684bbefba6e9087cd5ec725a02dfc480fa5bfbf9718565bbb72ae8b820bbedb57e888ce9242d4e075adf7aca473d2e1ee1f8999a2fe5f99a047f466cdf79f218bcaae8c01f1548dbe16817f18165378e3647ecf91ef66a203f4a952592e99942ae1bde401d0481a621cb44b748fbe8dabdc57d3dba1c02cf739521f31242245692a3527e26dda7ad225c19ed9cfcadd4c26f373fbe8c1a71bfd882085ee889935e266923996380bdeca1ffd942910609c7edee41c5fe1cb2dc43b96a891b1ac599d1627d3fac522f41cb3b95e321769fbc308e918c56908832128c30f71b9bd046810aba1e065a368ef1a6dad020afb6b0269914db3ef144ef5eb17643b0faefdcfa1a5753e9919cd5e067e434820d44cbe808f45c520ab68b0b02f50f9f8ccb70b647582ed6de9803c73425d6e9c14f0e455be803ede2ac88d184305a10a4f59507e05904f7b23e3a69853fba014f80e2e055eb1a81672300071a59fc00b46c1961690b235015116441d7316f2572f9f8c04cd102780fce73af21a5e10a67f66dda6ec74ba5779c11200a1c7d6c57481a2dc56020c0737898404b1f84cf3531a8e645573dc43c333c39eeea63468840401ec4cdb83a36136d34c39932915de37e15355aeaccf31542fa894c994f573dad1544b70b39bdb6202212e31be9a369abe19311bd109889a152519e615a3ba5d161312de6529fbc67e5262cc98bc666179d386c3c01271946ffdf8728fc7ed7617e7d2a6f3d904109e2727f6085e6662ef70f3db414cf6c699babf48a6eaabe1a01c585d565e48664a0974e31007157fe2571973a2ead52dbac469069a35b7deb299ab6c436133a98ff13cf73c5b2f2ff3a9831ff7b10ae3a37cfa226f2f39b4fa32860f00b44ba8c79772ecac1272292e3aa2d3bb3e1fe6d281abbf46becb3eafaba928922543758036140ed6ea4fa13eefdeb793823d01d4aa7a997ceae54098502ea0fb87c5372ac3b326314005fed970a48606c0c7db058278cd6c049fe371f5c7b89b3be83c756ba32b28af8268c8c8ca9f1f3cf35425c15b8491a2ded79542da247911a4c2b492e4913ef0bd91292fd26d96a24138310e138df857eeb468a6c2e43af346b874343187d20e4c8b3bc7888bdc6958c0b245175defd66290525d4a0918fefa3bc1ae0af6952650e763f440c05178d1fc1df36ad8ca010a31f4e19851ad8245aac63af166fccf51d56a868a5dd9ecdfe3b76206c7ccba11ed23795b0b2112b64c94f16d9b5fed9571d46288a5967c2e8c735a23b2876dbc9f93efb33eec7b2aa3a6db4d684e2ed834726725d1051b5e801255b16eb6a83dd77a115a18743c72706eb1d7d1cc853df2996af68a150c3cd7028bdfa2c92365f2cb3e63c13fa52b33f48c95f7729dcff12b161d4395af010ce9a35d748ffdabb069e2e1eb922674c97369a5571e74970f55a10c545d354d8001bf973320f9f6bfb5a74d552b66ae646a3ace2cecdcb11fd50fb3926cc639fef6fca25c2aad83a3fe660388d15b2886e19a9e40969c14169f283ae92422b2d566dfa492b5a271838ce40df03075150b30639604eefd33cf474099b7c0d40c120b047622ef5afa2b6fb7129fa4a9c1c7a607e99fef7a34ae9bc9c1c5e367ad87e0e79bad4b911bd00d2412c0e47aeb9d307c8523ed16f0ca4a1db800e6b20c02b4ac620545ffb1e16c7735058a0308dc17c04a00eac30b90cccea094cb3744105f8bb13244224f14450943c4cc93f4dbe5ff5465f0c2002426d819801ef7f2ad97b4aadeb0ff92935f752989b64167f95ad1d64d8b5a3a2b90cdd8524cb95f175eb3051518d499a1fb8a940a741e8d817088d5f87e9214940aec2246fdf46f1a785e532b42a62315ced406ed76c32d8fcefa215e9b6720fdf664643aaf9326a43a597d683ba6d4320c7991e52b6a7631704631b2adfdda8456a0e76aaffba2b35c9c5fb24a6723419411ed9f83e9243e5a96e05995717f9568f49d63a0af8d58bc618d49da2976b93b2ec72bfefb7331c189cc086429504a618057f78f032ad4b00f176ff8fe800a3d8f1b37a1609d0a5853a4f6bb905346274403d68224bd9d4ca2d96b44744377d095726081380390a5460416fd59cb177ddd513b11e95c088def82d7f1ba8c89b206d93aa04484dc1068a3ded6f283398ab4490662b7b7e26f2e0fc993bad5e53f35945b0b09a1f775b3d0ff77ed6811850ab342f1d0181229520d3ed0eb3acc30949403d1259773089c66fec5102a54e5f37718bca0749b5d025d68a33b20bf5f7ded1c596895a27da68f93fb65fdbfbef4250bd1ef093606a3652072190ce36a61a2ce243aadf3afcecbb9f2bead5bb45d6c00939da0b22459b5ea15bf740ed8807518a94442a1fc39f68aa4f0784ba60b36b6a0782c5e18edf431a2c39cbe5358c99cf6c1a13baebbc1e51ef6f5275d006ec03db9c9b45cfc6b35f2b87b16d391527b8feb0e2227f10ad73c5773083a637afcb141aed0dc6dd63a9bc8633ecf3c56c79033de354e3f544392cc3bcb4ac7552ce1794711bc955ea1bce9d097bfb012eca4d2d52dd6f0338fc3f0fb3669c65d948cdc9ba60194ec9a98541499c9e8063ee84618dcca68814ae8db958c62f5c81422c8ddd77d5c9c006756a3136aa270d1ed5b77cce47a36b99fe51b0d20f1226712ffc72dfdd8108bac28a4310da55c0992283088b64a3d65a00a7089a1d3083ec7533b17f5953730333f242eb3e49fa45f711b803c18ef629ffb56b41a22a301516360a14ffdcba2baf0256356ea8e46cb300d31640dbcd919d72f9af11c7f40a6840d0361eca1b87f2c50a1967638bb145d8c62db7e07675b1f1b0542eb28ad9d9a6ad3f8619a38c8c8733df27a087c0daf1635dc93e12514c13549ad08ce0e98af22d1cd293e2470b5b2dfbac6b2d587450e140da0090f8334f489a94ad4be74cded0d8e721af3ebc51af194b52ce32d13466705a0ad34b283d19025a1775453db1d136c5e0010d3fccae4a571df22fc98beec8bbed5a418fcec3ceb48d19aa0c5d974ed3b76efe735642c886de9f21451a5854dac89261c7c4636428b011f9f8ee063b0d9bf4c0536259465c0588c1a83ae96e046ffc54a4d145a84cb39462056ace37fe479d13acd3ec041048576423fe64fee688960a06832b6b63642f81ad20cde284f9f7cd63bdc60278f4fb797e9e335f4c1dc19b32b43b6ec3e3fe9d1d937f1db3aa47b12d5d93b3a8e74387049f593a4a034b245fd700e3fc9873270c28fba452081e189c189f5e3f9ea622fb9a3ca9453b2bfc53a463fb858c54bc1cea7c625892430d7463292540db47d685210f9dffdb1bb2039d5bc1d1622db51fcd734eb5dc828e072093fc30bfa07588b8deaee2e3c2ef91291a8a3a3f5b0c807d2ce46edc1e71a05c98e02f163c5cb51ac8b4a9d03afb067f735fc8f2afb7c9815263c52367dcd90b993f43e11bad299f97fa35ad0a9df541424c2d7c9153ee1a0e1d59f2805611e1029f22efc989eb0b31f42ded82563078e47e2661a37a13c3f02ad628bd5136d776d51400f3222826a14a2ba4ee444bbd0cee69da880c54e17e2b64924a7ceba50d2566a75d434c7a4db062967a2f2e593c7f65d24e5a991ecfd3cd59f393016d74f43b5820b3ccd49d8ae7049e28b92316fd5d80e69b8fc97b723b9a17f43c9af7d3110cf73fa7dd13f606bfc9306544bb71eec3bbabcb92be41e11dbee579d184e4f7b38ed84a1ca56e989a2768d4a9b24f1e07c62d11afcb5cd78ead71e285387960234f60576476d213ec3d77d8c7d72e9e013753b5d851b0e28d3769ed135d33ec33139948f9c7eb4556c8ae204cffd2f9ddf31c3f327eefc64ecf8561cb83b70ca88b9316cccdaaaa4afb4439fc1c3b83e4febf88d6cf18465d35d541b040dbd4f274a05145cf4fe4c6b9e7d6d4c77c7878903c8b19139b0f9bb78bf26847a5a9b56d37c16d84cb52d8dca380074dbde87ab3e462b1dae10b617958ed41fca2b537243c27dc48174a6dc1ff0cd70037c58c65997782558b81c8f994e88062dd991a06230e66d1456c7c523662d356b3fd57953c58eef803a69604a865307ba7b0ddf199529dc5259470232dc80a27a1ec023fd5b12119131e5b3e12b5a72133098c2d38b5020cee0595a0a6b4cac12f18b6718c3abe14f4436ffa56695fa7b71e2fe4f5e31bbfccc8d3533331fb85a9773e3c79614429c809aeca5dc80f6c31398faac631d78d1378f8f051f19282225c6ff29dc02e85faa1d5bca1fb4b3e731c689d15048d62b1d0d41a39344ec5ca2270394e3e3471da7b47f267f7bab8839a47722cddc3adb00a9ffb87c8141de2559c9b64800f57967bcbdd5adb3ef6145d9c381ceb36f91302980cd9d3523e9aa35c469f5defdf28c905bbedfbb3b0e4833b4d498d18d43a4e5bf818a3d0b73ebd93aa21a8664aa0fad1dc85da0dfe8157bd0cd3fbc1f5abca6d00f35d20a6f3bc0e6124d02c4cc50829e517486601e70ed43c5f9af914df5865d61b4062e1eab36e614e3443df375f7db5cdf8e245b3f35a3a86270e2bfbb4798c29337536f6c894d7961dd8f433b062ff9d44918f41e4145c577f2bec605f10610cb6b9bbace9730c294641a13b483bc0336b3fbc90b855a0e398f8108e499a666732d68cb7edf853a63734153f3410cf733005b479c4f33520dcdcea6f97adf3fe570c9c09c2097b5f0555e1b40d15da206387531ee9e07932a3908a27d369d41ea7c7cbbb16828effb47c993f6176ccb540f358184293292049c3b9ed1ad99a77ea06380bff952672fc849e32152fc9afc26bf24d6e02c8441de8be57e24e5e985e3c08faa0b428405f9825c119728922570fa87871e9a956f1aa24810018bc4cd18bef69fa19b08456f5e7c95fa64c03c44c6cc0d7fc7f92d6d778faaa651e5d90abe5a784b9aef88f7d297a63125ad9037d03e93c358703a518fd9b7701769e4b1a47979f0c3d3f5b79866c42ca87ef4da5a1768b701343d47bbe07d310a004597cfe7f7b205f4941e67d8b8f8d70c61b4416d035705d1ecb8396ce5285ebc33ff0c33ff249e534d547d657e64b82bb9042e05b57a95efc13f9d5c526275ff3b1f84e826bbaa6d73835a8516965ce32e6ce07c5880162b5f47e04a077320cd76549221c9a8976fc445d0434ce1a0c8f2710d90f1b324a7edd4d71691870153a91ac5c86864b0589894388fe2f332c523aceb597b8ba15348f26b03ec63856ea816a65388a27b2c71e514ce21385c1fc1b578be6050d89f74604f7cd5bc698808bb85863cf2dddc5270b53b38217f2cb7a7e7250f1cbefabe6b8c9369be1f4baf51bc43096684e359fff7142c4d83c68f1d523e4551053e150ce009fd696ed1337f94e945f92fd4eaedf4e2af6c6751c487dd826c24fc29c43c40339c8a3982196cd20cb83678bfb2b58e1b1736853d0c6514b9ce90f047a4a4ae0f99b6574165cd64999535a8d9de4a35d0fad302904ea64381389bb262b0879b6ebaa9ea4221596dea7286294aee2f775b1ffa6f745e2ca1a24858a95eee38f657290d183d5843d18af3a4395efa714c114d4167d621740c83baa4387687a5b71f9e8394ec4ad29dd31762359889d8451ccc7d8377474f7eca981d84dabee6c17038c45c29db961cf2665e1063be68076daa4d94ed69a799cf357b5aa7220d5eca186bff448bbb5808d274c16f125bff17edc0eb4e600df7e410681e4b430265c4c3e91a1c07a4e0b5beec6e50edb86ef46b8a5cb013ccd7be36e742dafd228d20d85efab539f6b1d690d9" +} diff --git a/src/ffi/crates/signatures/src/lean_sphincs/wots.rs b/src/ffi/crates/signatures/src/lean_sphincs/wots.rs new file mode 100644 index 0000000..a90e856 --- /dev/null +++ b/src/ffi/crates/signatures/src/lean_sphincs/wots.rs @@ -0,0 +1,165 @@ +//! WOTS+ and the Merkle trees of the hypertree. +//! +//! A layer's WOTS key signs an `n`-byte node directly: its `len1 = 32` base-16 +//! digits, then the `len2 = 3` base-16 digits of the checksum `sum(w - 1 - digit)`, +//! all read most significant first. Chain `i` starts at the revealed value, at hash +//! address `digit_i`, and walks the `w - 1 - digit_i` remaining steps. + +use super::*; + +/// The key pair's base address on a layer: `WOTS_HASH`, chain and hash address 0. +pub fn wots_adrs(layer: u32, tree: u64, kp: u32) -> Adrs { + Adrs::new(layer, tree, WOTS_HASH, kp, 0, 0) +} + +/// The `l` digits a key signs `node` with: its nibbles, the high one of each +/// byte first, then the checksum's, most significant first. +pub fn digits(node: &Digest) -> [u8; L] { + let mut out = [0; L]; + for (i, digit) in out[..LEN1].iter_mut().enumerate() { + *digit = (node[i / 2] >> (LOG_W * (1 - i % 2))) & (W - 1) as u8; + } + let csum = MAX_CSUM - out[..LEN1].iter().map(|&x| usize::from(x)).sum::(); + for j in 0..LEN2 { + out[LEN1 + j] = ((csum >> (LOG_W * (LEN2 - 1 - j))) & (W - 1)) as u8; + } + out +} + +/// Chain `i` of a key pair walked `steps` steps from `value` at position `start`: +/// step `s` hashes under hash address `start + s`. +pub fn chain(pp: &PublicParam, base: Adrs, i: usize, value: Digest, start: usize, steps: usize) -> Digest { + (start..start + steps).fold(value, |v, pos| { + let adrs = Adrs { + word2: i as u32, + word3: pos as u32, + ..base + }; + th(pp, &adrs, &[v]) + }) +} + +/// The WOTS public key from the chain tops: `T_l` under `WOTS_PK`, 1184 bytes. +pub fn wots_pk_of_tops(pp: &PublicParam, layer: u32, tree: u64, kp: u32, tops: &[Digest; L]) -> Digest { + th(pp, &Adrs::new(layer, tree, WOTS_PK, kp, 0, 0), tops) +} + +/// The key a signature on `node` recovers. +pub fn wots_pk_from_sig( + pp: &PublicParam, + layer: u32, + tree: u64, + kp: u32, + node: &Digest, + sigma: &[Digest; L], +) -> Digest { + let digits = digits(node); + let base = wots_adrs(layer, tree, kp); + let tops = std::array::from_fn(|i| { + let d = usize::from(digits[i]); + chain(pp, base, i, sigma[i], d, W - 1 - d) + }); + wots_pk_of_tops(pp, layer, tree, kp, &tops) +} + +/// Chain `i`'s secret, `PRF` under `WOTS_PRF`. +fn wots_secret(sk_seed: &[u8; N], layer: u32, tree: u64, kp: u32, i: usize) -> Digest { + prf(sk_seed, &Adrs::new(layer, tree, WOTS_PRF, kp, i as u32, 0)) +} + +/// A key pair's public key. +pub(crate) fn wots_pk(pp: &PublicParam, sk_seed: &[u8; N], layer: u32, tree: u64, kp: u32) -> Digest { + let base = wots_adrs(layer, tree, kp); + let tops = std::array::from_fn(|i| chain(pp, base, i, wots_secret(sk_seed, layer, tree, kp, i), 0, W - 1)); + wots_pk_of_tops(pp, layer, tree, kp, &tops) +} + +/// Sign `node`: each chain walked `digit_i` steps from its secret. +pub(crate) fn wots_sign( + pp: &PublicParam, + sk_seed: &[u8; N], + layer: u32, + tree: u64, + kp: u32, + node: &Digest, +) -> [Digest; L] { + let digits = digits(node); + let base = wots_adrs(layer, tree, kp); + std::array::from_fn(|i| { + chain( + pp, + base, + i, + wots_secret(sk_seed, layer, tree, kp, i), + 0, + usize::from(digits[i]), + ) + }) +} + +/// A hypertree Merkle node: `H` under `TREE`, height `height`, index `index`. +pub fn tree_node( + pp: &PublicParam, + layer: u32, + tree: u64, + height: usize, + index: u32, + left: &Digest, + right: &Digest, +) -> Digest { + th( + pp, + &Adrs::new(layer, tree, TREE, 0, height as u32, index), + &[*left, *right], + ) +} + +/// Fold a leaf up its authentication path to the tree's root. +pub fn tree_fold( + pp: &PublicParam, + layer: u32, + tree: u64, + leaf_idx: u32, + leaf: Digest, + path: &[Digest; SUBTREE_H], +) -> Digest { + path.iter().enumerate().fold(leaf, |node, (h, sibling)| { + let (left, right) = if (leaf_idx >> h) & 1 == 0 { + (node, *sibling) + } else { + (*sibling, node) + }; + tree_node(pp, layer, tree, h + 1, leaf_idx >> (h + 1), &left, &right) + }) +} + +/// Every level of a complete Merkle tree over `leaves`, bottom first, each +/// parent `node(height, index, left, right)`. +pub(crate) fn merkle_levels( + leaves: Vec, + node: impl Fn(usize, u32, &Digest, &Digest) -> Digest, +) -> Vec> { + let mut levels = vec![leaves]; + while levels.last().unwrap().len() > 1 { + let below = levels.last().unwrap(); + let height = levels.len(); + let level = (0..below.len() / 2) + .map(|j| node(height, j as u32, &below[2 * j], &below[2 * j + 1])) + .collect(); + levels.push(level); + } + levels +} + +/// The siblings of leaf `leaf` in `levels`. +pub(crate) fn auth_path(levels: &[Vec], leaf: u32) -> [Digest; HEIGHT] { + std::array::from_fn(|h| levels[h][((leaf >> h) ^ 1) as usize]) +} + +/// One hypertree tree, all levels: `2^h'` WOTS keys and the nodes above them. +pub(crate) fn subtree_levels(pp: &PublicParam, sk_seed: &[u8; N], layer: u32, tree: u64) -> Vec> { + let leaves = parallel::map_collect(1 << SUBTREE_H, |kp| wots_pk(pp, sk_seed, layer, tree, kp as u32)); + merkle_levels(leaves, |height, index, l, r| { + tree_node(pp, layer, tree, height, index, l, r) + }) +} diff --git a/src/ffi/crates/signatures/src/lib.rs b/src/ffi/crates/signatures/src/lib.rs new file mode 100644 index 0000000..50b8ec5 --- /dev/null +++ b/src/ffi/crates/signatures/src/lib.rs @@ -0,0 +1,9 @@ +//! Stands in for upstream's `signatures` crate so the vendored `rec_aggregation` builds unchanged against it: +//! upstream's leanXMSS, and NiceTry v2 in place of upstream's leanSPHINCS. + +pub use upstream_signatures::lean_xmss; + +pub mod lean_sphincs; + +/// The length a hash value is cut to. +const N: usize = 16; diff --git a/src/ffi/examples/fixtures.rs b/src/ffi/examples/fixtures.rs index df1c8b8..b2e0ee2 100644 --- a/src/ffi/examples/fixtures.rs +++ b/src/ffi/examples/fixtures.rs @@ -1,9 +1,10 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: MIT -use nethermind_lean::{aggregated_vk, keccak, prove_stark}; +use nethermind_lean::{ANY_INPUT_PROGRAM, aggregated_vk, keccak, prove_stark, verification_key}; +use signatures::lean_sphincs::{key_gen_from_seed, sign}; fn main() { let directory = std::env::args().nth(1).expect("fixture output directory"); - if directory == "--sign-message" { + if directory == "--sign-message" || directory == "--prove-stark" { let mut arguments = std::env::args().skip(2); let hash = arguments.next().expect("32-byte message as hex"); assert_eq!(hash.len(), 64, "message must contain 64 hex digits"); @@ -13,55 +14,54 @@ fn main() { } let output = arguments.next().expect("witness output file"); assert!(arguments.next().is_none(), "unexpected fixture arguments"); - let (sk, pk) = sphincs::key_gen_from_seed([42; 32]); - let signature = sphincs::sign(&sk, &message); - std::fs::write( - output, - [pk.flatten().as_slice(), signature.to_bytes().as_slice()].concat(), - ) - .unwrap(); + // `key ‖ witness`: a NiceTry v2 signature, or a leanSTARK proof of the accept-all program on the message. + let output_bytes = if directory == "--prove-stark" { + let (witness, key) = prove_stark(ANY_INPUT_PROGRAM, &message).unwrap(); + [&key[..], &witness[..]].concat() + } else { + let (sk, pk) = key_gen_from_seed([42; 32]); + let signature = sign(&sk, &message).unwrap(); + [&verification_key(&pk)[..], &signature.to_bytes()[..]].concat() + }; + std::fs::write(output, output_bytes).unwrap(); return; } std::fs::create_dir_all(&directory).unwrap(); let save = |name: &str, bytes: &[u8]| std::fs::write(format!("{directory}/{name}"), bytes).unwrap(); - let (sk, pk) = sphincs::key_gen_from_seed([42; 32]); + let (sk, pk) = key_gen_from_seed([42; 32]); let message = [7; 32]; - let sig = sphincs::sign(&sk, &message); - save("sphincs-key.bin", &pk.flatten()); + let sig = sign(&sk, &message).unwrap(); + save("sphincs-key.bin", &verification_key(&pk)); save("sphincs-message.bin", &message); save( "sphincs-signature.bin", - &[pk.flatten().as_slice(), sig.to_bytes().as_slice()].concat(), + &[&verification_key(&pk)[..], &sig.to_bytes()[..]].concat(), ); let mut multi = Vec::new(); for index in 0u64..16 { let message = keccak(&index.to_le_bytes()); - let signature = sphincs::sign(&sk, &message); multi.extend_from_slice(&message); - multi.extend_from_slice(&pk.flatten()); - multi.extend_from_slice(&signature.to_bytes()); + multi.extend_from_slice(&verification_key(&pk)); + multi.extend_from_slice(&sign(&sk, &message).unwrap().to_bytes()); } save("sphincs-multi.bin", &multi); let mut multi_keys = Vec::new(); for index in 0u8..16 { let mut seed = [42; 32]; seed[0] = index; - let (sk, pk) = sphincs::key_gen_from_seed(seed); + let (sk, pk) = key_gen_from_seed(seed); let message = keccak(&[index]); multi_keys.extend_from_slice(&message); - multi_keys.extend_from_slice(&pk.flatten()); - multi_keys.extend_from_slice(&sphincs::sign(&sk, &message).to_bytes()); + multi_keys.extend_from_slice(&verification_key(&pk)); + multi_keys.extend_from_slice(&sign(&sk, &message).unwrap().to_bytes()); } save("sphincs-multi-keys.bin", &multi_keys); - let source = "from snark_lib import *\ndef main():\n p = GEN ** 0\n p[1] = 7\n p[GEN] = 9\n return\n"; - let mut public_input = [0; 32]; - public_input[0] = 7; - public_input[16] = 9; - let (proof, key) = prove_stark(source, &public_input).unwrap(); + let public_input = [9; 32]; + let (witness, key) = prove_stark(ANY_INPUT_PROGRAM, &public_input).unwrap(); save("stark-key.bin", &key); save("stark-message.bin", &public_input); - save("stark-proof.bin", &proof); + save("stark-proof.bin", &witness); let key = aggregated_vk(); save("aggregated-key.bin", &key); println!( diff --git a/src/ffi/examples/lean_startup_stub.rs b/src/ffi/examples/lean_startup_stub.rs index a6be176..5221dc0 100644 --- a/src/ffi/examples/lean_startup_stub.rs +++ b/src/ffi/examples/lean_startup_stub.rs @@ -5,9 +5,9 @@ #[unsafe(no_mangle)] pub extern "C" fn nlean_abi_version() -> u32 { if std::env::var("NETHERMIND_LEAN_STARTUP_PROBE").as_deref() == Ok("abi") { - 9 - } else { 10 + } else { + 12 } } @@ -28,7 +28,7 @@ pub unsafe extern "C" fn nlean_aggregated_vk(out: *mut u8) -> i32 { 0x4a, 0x98, 0x11, 0x66, ] } else { - rec_aggregation::eip8288_mixed::mixed_guest_key() + rec_aggregation::aggregated_vk() }; unsafe { std::ptr::copy_nonoverlapping(key.as_ptr(), out, key.len()); @@ -37,10 +37,10 @@ pub unsafe extern "C" fn nlean_aggregated_vk(out: *mut u8) -> i32 { } /// # Safety -/// The non-null output must hold nine u32 values. +/// The non-null output must hold seven u32 values. #[unsafe(no_mangle)] pub unsafe extern "C" fn nlean_limits(out: *mut u32, len: usize) -> i32 { - if out.is_null() || len != 9 { + if out.is_null() || len != 7 { return 0; } unsafe { diff --git a/src/ffi/src/lib.rs b/src/ffi/src/lib.rs index 5a6ca66..8971afb 100644 --- a/src/ffi/src/lib.rs +++ b/src/ffi/src/lib.rs @@ -3,196 +3,36 @@ //! Pinned LeanVM verification and recursive aggregation of complete dependency sets. -use bincode::Options; -use lean_vm::cpu::{self, Program}; -use primitives::field::F192; -use rec_aggregation::eip8288_mixed::{self, GenericInput, MixedProof}; -use sphincs::{SphincsPublicKey, SphincsSignature}; +use rec_aggregation::{ + EthereumClaims, EthereumProof, MAX_GENERIC_BYTECODE_LOG, MAX_GENERIC_DEPENDENCIES, +}; +use signatures::lean_sphincs::{self, LeanSphincsPublicKey, LeanSphincsSignature}; use std::cell::{Cell, RefCell}; -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeSet, HashSet}; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Barrier, Condvar, Mutex, OnceLock}; +use std::sync::{Barrier, Mutex, OnceLock}; use std::thread::ThreadId; -use tiny_keccak::{Hasher, Keccak}; pub const MAX_BYTES: usize = 8 * 1024 * 1024; pub const MAX_INPUT_BYTES: usize = 18 * 1024 * 1024; -const MAX_DEPS: usize = eip8288_mixed::MAX_DEPENDENCIES; -const MAX_MIXED_GUEST_BYTES: usize = MAX_BYTES; -const MAX_INSTRUCTIONS: usize = 1 << eip8288_mixed::MAX_GENERIC_BYTECODE_LOG; -const MAX_OPERAND: u32 = 65535; -const MAX_GENERIC_STARKS: usize = eip8288_mixed::MAX_GENERIC_DEPENDENCIES; -const MAX_DECODE_BYTES: usize = 32 * 1024 * 1024; +/// A local bound: one guest node proves this many signatures in about 9 GiB. +const MAX_DEPS: usize = 256; +// The upstream prover's timings assume its allocator: jemalloc keeps a proof's freed pages mapped for the next one. +// It serves this library's allocations only, and holds their peak until the process exits. +#[global_allocator] +static ALLOCATOR: bench::Jemalloc = bench::Jemalloc; +const LOG_INV_RATE: usize = 1; static PROVER: Mutex<()> = Mutex::new(()); -/// Takes the prover for one proof. The proving arena allows one proof in flight per process; PROVER, behind the managed -/// proving gate, serializes every proof. +/// Takes the prover for one proof: `rec_aggregation::aggregate` allows one call at a time per process. fn begin_proof() -> std::sync::MutexGuard<'static, ()> { let guard = PROVER .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - static ARENA: std::sync::Once = std::sync::Once::new(); - ARENA.call_once(|| { - lean_vm::init_prover(); - restore_system_heap(); - }); + parallel::init(); guard } -/// Undoes the process-wide glibc tuning `init_prover` applies, restoring the operator's setting or glibc's default. -/// -/// The arena is its own mapping and does not rely on that tuning, while the host would otherwise never trim its heap or -/// mmap large allocations again. glibc's adaptive mmap threshold stays off: `init_prover` already disabled it, and no -/// mallopt re-enables it. -#[cfg(all(target_os = "linux", target_env = "gnu"))] -fn restore_system_heap() { - let trim = glibc_setting("trim_threshold", "MALLOC_TRIM_THRESHOLD_", 128 * 1024); - let mmap_max = glibc_setting("mmap_max", "MALLOC_MMAP_MAX_", 65536); - // SAFETY: mallopt only adjusts allocator tuning parameters. - unsafe { - libc::mallopt(libc::M_TRIM_THRESHOLD, trim); - libc::mallopt(libc::M_MMAP_MAX, mmap_max); - } -} - -/// The `glibc.malloc.` value from `GLIBC_TUNABLES`, else the legacy environment alias, else `default`. -#[cfg(all(target_os = "linux", target_env = "gnu"))] -fn glibc_setting(tunable: &str, alias: &str, default: i32) -> i32 { - let key = format!("glibc.malloc.{tunable}="); - std::env::var("GLIBC_TUNABLES") - .ok() - .and_then(|tunables| { - tunables - .split(':') - .find_map(|entry| parse_c_integer(entry.strip_prefix(key.as_str())?)) - }) - .or_else(|| parse_c_integer(&std::env::var(alias).ok()?)) - .unwrap_or(default) -} - -/// Parses an unsigned integer as `strtoul(s, _, 0)` does (decimal, `0x` hex or leading-`0` octal), saturating at `i32::MAX`. -#[cfg(all(target_os = "linux", target_env = "gnu"))] -fn parse_c_integer(value: &str) -> Option { - let parsed = if let Some(hex) = value - .strip_prefix("0x") - .or_else(|| value.strip_prefix("0X")) - { - u64::from_str_radix(hex, 16) - } else if value.len() > 1 && value.starts_with('0') { - u64::from_str_radix(&value[1..], 8) - } else { - value.parse() - }; - parsed.ok().map(|v| i32::try_from(v).unwrap_or(i32::MAX)) -} -#[cfg(not(all(target_os = "linux", target_env = "gnu")))] -fn restore_system_heap() {} - -/// Keeps verifications off arena phase openings. A thread's `ArenaVec`s die when it next allocates after a phase opens, -/// and an allocation racing the opening can land in the stale phase, so no phase may open while a verification runs. -/// Verifications may run inside an open phase, on their own slabs, so they wait only for an opening, not for the proof -/// node: a node takes several seconds on a few cores, which held up block import and wrapper admission. -struct PhaseGate { - state: Mutex, - changed: Condvar, -} - -struct GateState { - verifications: usize, - /// The arena generation an opening started from, while one is pending or its node is still proving. - opening_from: Option, -} - -static PHASE_GATE: PhaseGate = PhaseGate { - state: Mutex::new(GateState { - verifications: 0, - opening_from: None, - }), - changed: Condvar::new(), -}; - -/// How often a verification held at an opening checks whether the phase has opened, which nothing signals. -const OPENING_POLL: std::time::Duration = std::time::Duration::from_millis(1); - -impl PhaseGate { - fn lock(&self) -> std::sync::MutexGuard<'_, GateState> { - self.state - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - } -} - -struct OpeningGuard; - -impl Drop for OpeningGuard { - fn drop(&mut self) { - PHASE_GATE.lock().opening_from = None; - PHASE_GATE.changed.notify_all(); - } -} - -/// Runs `prove`, which must open at most one arena phase, once no verification is in flight. -fn in_arena_phase(prove: impl FnOnce() -> T) -> T { - let mut state = PHASE_GATE.lock(); - let generation = zk_alloc::stats().phases; - state.opening_from = Some(generation); - while state.verifications > 0 { - state = PHASE_GATE - .changed - .wait(state) - .unwrap_or_else(std::sync::PoisonError::into_inner); - } - drop(state); - let _opening = OpeningGuard; - let result = prove(); - assert!( - zk_alloc::stats().phases - generation <= 1, - "a gated prove opened more than one arena phase" - ); - result -} - -/// Runs `verify` outside any arena phase opening: before it, or once the phase it waits for has opened. -fn verify_between_phases(verify: impl FnOnce() -> T) -> T { - let mut state = PHASE_GATE.lock(); - while let Some(generation) = state.opening_from { - if zk_alloc::stats().phases != generation { - break; - } - state = PHASE_GATE - .changed - .wait_timeout(state, OPENING_POLL) - .unwrap_or_else(std::sync::PoisonError::into_inner) - .0; - } - state.verifications += 1; - drop(state); - let _verification = VerificationGuard; - verify() -} - -struct VerificationGuard; - -impl Drop for VerificationGuard { - fn drop(&mut self) { - let mut state = PHASE_GATE.lock(); - state.verifications -= 1; - if state.verifications == 0 { - PHASE_GATE.changed.notify_all(); - } - } -} - -fn aggregate_mixed( - children: &[MixedProof], - signatures: &[(SphincsPublicKey, [u8; 32], SphincsSignature)], - generic: &[GenericInput], - deps: &[Dep], -) -> Result { - in_arena_phase(|| eip8288_mixed::aggregate_mixed(children, signatures, generic, deps, 1)) - .map_err(|_| ()) -} - // leanVM's carry-less-multiply field arithmetic is about 68x slower without these features. An // environment RUSTFLAGS replaces .cargo/config.toml's target-cpu=native instead of adding to it, so // refuse to build a backend that silently lost them. @@ -201,7 +41,7 @@ fn aggregate_mixed( all(target_arch = "aarch64", not(target_feature = "aes")) ))] compile_error!( - "nethermind_lean needs carry-less multiply: build from src/ffi so .cargo/config.toml \ + "nethermind_lean needs carry-less multiply: build from tools/lean-ffi so .cargo/config.toml \ applies, or include -C target-cpu=native (or +pclmulqdq / +aes) in RUSTFLAGS" ); static ACTIVE_ABI_CALLS: AtomicUsize = AtomicUsize::new(0); @@ -209,93 +49,16 @@ static ABI_WORKERS: OnceLock> = OnceLock::new(); thread_local! { static IN_ABI: Cell = const { Cell::new(false) }; } thread_local! { static LAST_PANIC: RefCell = const { RefCell::new(String::new()) }; } type Dep = [u8; 96]; +const LEAN_SPHINCS: u8 = 0x10; +/// EIP-8288 reserves this scheme; accepting it is an experimental extension of this stack. +const LEAN_STARK: u8 = 0x11; pub fn keccak(bytes: &[u8]) -> [u8; 32] { - let mut h = Keccak::v256(); - h.update(bytes); - let mut out = [0; 32]; - h.finalize(&mut out); - out + primitives::hash::hash(bytes) } pub fn aggregated_vk() -> [u8; 32] { - eip8288_mixed::mixed_guest_key() -} - -// F192 is GF(2^64)[y]/(y^3+y+1): new stores three distinct u64 coefficients, -// without integer modular reduction. Two (c0,c1,0) cells injectively encode all 256 bits. -fn pi(hash: &[u8; 32]) -> [F192; 2] { - let word = |i| u64::from_le_bytes(hash[i..i + 8].try_into().unwrap()); - [ - F192::new(word(0), word(8), 0), - F192::new(word(16), word(24), 0), - ] -} -fn wire() -> impl Options { - bincode::DefaultOptions::new() - .with_fixint_encoding() - .with_limit(MAX_BYTES as u64) -} - -// Pinned fixed-int serde layout. Validate every allocation hint without allocating. -struct Preflight<'a> { - reader: Reader<'a>, - allocated: usize, -} -impl<'a> Preflight<'a> { - fn new(bytes: &'a [u8]) -> Self { - Self { - reader: Reader { bytes }, - allocated: 0, - } - } - fn count(&mut self, max: usize, width: usize) -> Result { - let n = usize::try_from(u64::from_le_bytes( - self.reader.take(8)?.try_into().map_err(|_| ())?, - )) - .map_err(|_| ())?; - if n > max || n > self.reader.bytes.len() / width { - return Err(()); - } - self.allocated = self - .allocated - .checked_add(n.checked_mul(width).ok_or(())?) - .ok_or(())?; - if self.allocated > MAX_DECODE_BYTES { - return Err(()); - } - Ok(n) - } - fn vector(&mut self, max: usize, width: usize) -> Result<(), ()> { - let n = self.count(max, width)?; - self.reader.take(n * width)?; - Ok(()) - } - fn cpu(&mut self) -> Result<(), ()> { - self.vector(MAX_BYTES / 24, 24)?; // scalar stream - let phases = self.count(128, 16)?; - for _ in 0..phases { - let rows = self.count(4096, 8)?; - self.allocated = self.allocated.checked_add(rows * 24).ok_or(())?; - if self.allocated > MAX_DECODE_BYTES { - return Err(()); - } - for _ in 0..rows { - self.vector(MAX_BYTES / 8, 8)?; - } - self.vector(MAX_BYTES / 32, 32)?; - } - Ok(()) - } - fn mixed(&mut self) -> Result<(), ()> { - self.vector(32, 24)?; // bytecode evaluation point - self.vector(32, 24)?; // BLAKE2s matrix evaluation point - self.vector(32, 24)?; // Keccak matrix evaluation point - self.cpu() - } - fn end(self) -> Result<(), ()> { - self.reader.end() - } + rec_aggregation::aggregated_vk() } struct Reader<'a> { @@ -326,7 +89,7 @@ impl<'a> Reader<'a> { } fn dep(&mut self) -> Result { let d: Dep = self.take(96)?.try_into().map_err(|_| ())?; - if d[..31].iter().any(|b| *b != 0) || ![0x10, 0x11].contains(&d[31]) { + if d[..31].iter().any(|b| *b != 0) || !matches!(d[31], LEAN_SPHINCS | LEAN_STARK) { return Err(()); } Ok(d) @@ -342,23 +105,45 @@ impl<'a> Reader<'a> { } } } -fn put_number(out: &mut Vec, n: usize) { - out.extend_from_slice(&(n as u32).to_le_bytes()); + +/// A dependency triple's `(data_hash, public key)`; triples and claims sort alike. +/// EIP-8288 leanSPHINCS `verification_key_hash`: the key itself, root then public parameter. +pub fn verification_key(pk: &LeanSphincsPublicKey) -> [u8; 32] { + let mut out = [0; 32]; + out[..16].copy_from_slice(&pk.root); + out[16..].copy_from_slice(&pk.public_param); + out } -fn put_blob(out: &mut Vec, b: &[u8]) { - put_number(out, b.len()); - out.extend_from_slice(b); +fn public_key(vk: &[u8; 32]) -> LeanSphincsPublicKey { + LeanSphincsPublicKey { + root: vk[..16].try_into().unwrap(), + public_param: vk[16..].try_into().unwrap(), + } } -#[cfg(test)] -fn put_deps(out: &mut Vec, ds: &[Dep]) { - put_number(out, ds.len()); - for d in ds { - out.extend_from_slice(d); +fn data(dep: &Dep) -> [u8; 32] { + dep[32..64].try_into().unwrap() +} +fn key(dep: &Dep) -> [u8; 32] { + dep[64..].try_into().unwrap() +} +fn claims(deps: &[Dep]) -> EthereumClaims { + let of = |scheme| deps.iter().filter(move |dep| dep[31] == scheme); + EthereumClaims { + lean_sphincs: of(LEAN_SPHINCS) + .map(|dep| (data(dep), public_key(&key(dep)))) + .collect(), + generic: of(LEAN_STARK).map(|dep| (data(dep), key(dep))).collect(), + ..Default::default() } } -/// EIP-8288 `get_deps_hash` of a canonical list, as the recursive guest commits to it. +/// The dependency digest of a canonical list, as the recursive guest commits to it: EIP-8288 `get_deps_hash` for a +/// leanSPHINCS-only list, and the split-domain digest of `rec_aggregation::deps_hash` once it holds a leanSTARK entry. pub fn commitment(ds: &[Dep]) -> [u8; 32] { - eip8288_mixed::deps_hash(ds) + let claims = claims(ds); + rec_aggregation::deps_hash(&claims.lean_sphincs, &claims.generic) +} +fn generic_count(ds: &[Dep]) -> usize { + ds.iter().filter(|dep| dep[31] == LEAN_STARK).count() } fn canonical(ds: Vec) -> Vec { ds.into_iter() @@ -366,344 +151,69 @@ fn canonical(ds: Vec) -> Vec { .into_iter() .collect() } -#[cfg(test)] -fn sphincs_dependency(pk: &SphincsPublicKey, message: &[u8; 32]) -> Dep { - let mut dep = [0; 96]; - dep[31] = 0x10; - dep[32..64].copy_from_slice(message); - dep[64..].copy_from_slice(&eip8288_mixed::sphincs_verification_key(pk)); - dep -} fn parse_sphincs( - hash: &[u8; 32], vk: &[u8; 32], witness: &[u8], -) -> Result<(SphincsPublicKey, SphincsSignature), ()> { - if witness.len() != sphincs::PUB_KEY_SIZE + sphincs::SIG_SIZE { - return Err(()); - } - let pk = SphincsPublicKey::from_bytes(witness[..32].try_into().map_err(|_| ())?); - if eip8288_mixed::sphincs_verification_key(&pk) != *vk { +) -> Result<(LeanSphincsPublicKey, LeanSphincsSignature), ()> { + if witness.len() != lean_sphincs::PUB_KEY_SIZE + lean_sphincs::SIG_SIZE || witness[..32] != *vk + { return Err(()); } - let sig = SphincsSignature::from_bytes(witness[32..].try_into().map_err(|_| ())?); - sphincs::verify(&pk, hash, &sig).map_err(|_| ())?; - Ok((pk, sig)) + let sig = LeanSphincsSignature::from_bytes(witness[32..].try_into().map_err(|_| ())?); + Ok((public_key(vk), sig)) } fn verify_sphincs(hash: &[u8; 32], vk: &[u8; 32], witness: &[u8]) -> bool { - parse_sphincs(hash, vk, witness).is_ok() -} - -// A fixed 45-byte record: opcode, four u32 operands, three u64 immediate limbs, -// then one u32 BLAKE2s metadata operand. Unused fields must be zero. -fn encode_program(program: &Program) -> Vec { - use cpu::{DerefMode, Op}; - let mut out = Vec::new(); - put_number(&mut out, program.prog.len()); - for op in &program.prog { - let (tag, args, limbs, md) = match *op { - Op::Xor { a, b, c } => (0, [a, b, c, 0], [0; 3], 0), - Op::Mul { a, b, c } => (1, [a, b, c, 0], [0; 3], 0), - Op::Set { o, k } => (2, [o, 0, 0, 0], [k.c0, k.c1, k.c2], 0), - Op::Deref { o1, o2, o3, mode } => ( - 3, - [ - o1, - o2, - o3, - match mode { - DerefMode::Cell => 0, - DerefMode::Pc => 1, - DerefMode::Fp => 2, - }, - ], - [0; 3], - 0, - ), - Op::Jump { oc, od, of } => (4, [oc, od, of, 0], [0; 3], 0), - Op::Blake2s { ins, cv, out, md } => (5, ins, [cv as u64, out as u64, 0], md), - Op::Sha3 { - m, - cap, - out, - digest, - } => ( - 6, - m[..4].try_into().unwrap(), - [ - m[4] as u64 | ((m[5] as u64) << 32), - m[6] as u64 | ((m[7] as u64) << 32), - cap as u64 | ((out as u64) << 32), - ], - digest as u32, - ), - }; - out.push(tag); - for x in args { - out.extend_from_slice(&x.to_le_bytes()); - } - for x in limbs { - out.extend_from_slice(&x.to_le_bytes()); - } - out.extend_from_slice(&md.to_le_bytes()); - } - out + parse_sphincs(vk, witness).is_ok_and(|(pk, sig)| lean_sphincs::verify(&pk, hash, &sig).is_ok()) } -/// The instruction count of `code` if its length matches it, checked without decoding any instruction. -fn instruction_count(code: &[u8]) -> Result { - let mut r = Reader { bytes: code }; - let n = r.number()?; - if n == 0 || n > MAX_INSTRUCTIONS || !n.is_power_of_two() || r.bytes.len() != n * 45 { - return Err(()); - } - Ok(n) -} -fn stark_program(code: &[u8]) -> Result { - use cpu::{DerefMode, Op}; - let n = instruction_count(code)?; - let mut r = Reader { bytes: &code[4..] }; - let mut ops = Vec::with_capacity(n); - for _ in 0..n { - let tag = r.take(1)?[0]; - let mut a = [0u32; 4]; - let mut k = [0u64; 3]; - for x in &mut a { - *x = r.number()? as u32; - } - for x in &mut k { - *x = u64::from_le_bytes(r.take(8)?.try_into().map_err(|_| ())?); - } - let md = r.number()? as u32; - // Upstream precomputes g-powers up to the largest operand at assembly. - if a.iter().any(|v| *v > MAX_OPERAND) - || md > MAX_OPERAND - || (tag == 5 && (k[0] > MAX_OPERAND as u64 || k[1] > MAX_OPERAND as u64)) - { - return Err(()); - } - let op = match tag { - 0 => Op::Xor { - a: a[0], - b: a[1], - c: a[2], - }, - 1 => Op::Mul { - a: a[0], - b: a[1], - c: a[2], - }, - 2 => Op::Set { - o: a[0], - k: F192::new(k[0], k[1], k[2]), - }, - 3 => Op::Deref { - o1: a[0], - o2: a[1], - o3: a[2], - mode: match a[3] { - 0 => DerefMode::Cell, - 1 => DerefMode::Pc, - 2 => DerefMode::Fp, - _ => return Err(()), - }, - }, - 4 => Op::Jump { - oc: a[0], - od: a[1], - of: a[2], - }, - 5 if k[0] <= u32::MAX as u64 && k[1] <= u32::MAX as u64 => Op::Blake2s { - ins: a, - cv: k[0] as u32, - out: k[1] as u32, - md, - }, - 6 if md <= 1 => { - let halves = |value: u64| [value as u32, (value >> 32) as u32]; - let m = [ - a[0], - a[1], - a[2], - a[3], - halves(k[0])[0], - halves(k[0])[1], - halves(k[1])[0], - halves(k[1])[1], - ]; - let [cap, out] = halves(k[2]); - if m.iter().any(|value| *value > MAX_OPERAND) - || cap > MAX_OPERAND - 4 - || out > MAX_OPERAND - 12 - { - return Err(()); - } - Op::Sha3 { - m, - cap, - out, - digest: md != 0, - } - } - _ => return Err(()), - }; - ops.push(op); - } - let program = Program::assemble(ops, HashMap::new(), 0); - if encode_program(&program) != code { - return Err(()); - } - Ok(program) -} -fn decode_stark( - hash: &[u8; 32], - vk: &[u8; 32], - witness: &[u8], -) -> Result<(Program, cpu::Proof), ()> { - if witness.len() > MAX_BYTES { - return Err(()); - } - let mut r = Reader { bytes: witness }; - let code = r.blob()?; - instruction_count(code)?; - if keccak(&[&eip8288_mixed::GENERIC_VK_DOMAIN[..], code].concat()) != *vk { - return Err(()); - } - let program = stark_program(code)?; - let mut preflight = Preflight::new(r.bytes); - preflight.cpu()?; - preflight.end()?; - let proof: cpu::Proof = wire().deserialize(r.bytes).map_err(|_| ())?; - if !eip8288_mixed::generic_recursion_supported(&program, &proof) { - return Err(()); +fn verify_witness(dep: &Dep, witness: &[u8]) -> bool { + if dep[31] == LEAN_STARK { + rec_aggregation::verify_generic(&data(dep), &key(dep), witness) + } else { + verify_sphincs(&data(dep), &key(dep), witness) } - cpu::verify(&program, &pi(hash), &proof).map_err(|_| ())?; - Ok((program, proof)) } -fn verify_stark(hash: &[u8; 32], vk: &[u8; 32], witness: &[u8]) -> bool { - verify_between_phases(|| decode_stark(hash, vk, witness).is_ok()) +/// A leanVM program that accepts every public input: the leanSTARK dependency anyone can prove. For tests and fixtures. +pub const ANY_INPUT_PROGRAM: &str = "def main():\n h = StackBuf(2)\n hint_witness(h, \"pi\")\n p = 1\n p[1] = h[0]\n p[GEN] = h[1]\n return\n"; + +/// Compiles `source`, a leanVM program reading its public input from the `pi` hint, and proves it on `hash`: the +/// leanSTARK witness and verification key of the dependency `(hash, key)`. For tests and fixtures. +pub fn prove_stark(source: &str, hash: &[u8; 32]) -> Result<(Vec, [u8; 32]), ()> { + let _guard = begin_proof(); + let mut program = lean_compiler::compile(&lean_compiler::parse(source).map_err(|_| ())?); + let table: Vec = leanvm_core::cpu::bytecode_table(program.prog()) + .iter() + .map(|word| word.0) + .collect(); + let key = rec_aggregation::generic_verification_key(&table).ok_or(())?; + let input = fiat_shamir::merkle::hash_to_scalars(hash); + program.set_witness("pi", vec![input.to_vec()]); + let (proof, _) = leanvm_core::cpu::prove(&program, input, LOG_INV_RATE).map_err(|_| ())?; + Ok(( + rec_aggregation::generic_witness(&table, &proof).ok_or(())?, + key, + )) } // EIP-8288 `stark_proof`: the guest proof alone, checked against `deps_hash` and the pinned key. -fn preflight_proof(proof: &[u8]) -> Result<(), ()> { - if proof.is_empty() || proof.len() > MAX_MIXED_GUEST_BYTES { +// The empty proof of an empty list never reaches the guest: callers decide that case themselves. +fn verify_aggregate(hash: &[u8; 32], proof: &[u8]) -> Result<(), ()> { + if proof.is_empty() || proof.len() > MAX_BYTES { return Err(()); } - let mut preflight = Preflight::new(proof); - preflight.mixed()?; - preflight.end() -} - -fn verify_aggregate(hash: &[u8; 32], proof: &[u8]) -> Result<(), ()> { - preflight_proof(proof)?; - verify_between_phases(|| eip8288_mixed::verify_mixed_deps(hash, proof).map_err(|_| ())) + rec_aggregation::verify_deps_hash(hash, proof).map_err(|_| ()) } -// A recursive child is proven against its declared dependencies, which the caller supplies; -// the upstream decoder enforces their canonical order and profile bounds. -fn decode_child(deps: &[Dep], proof: &[u8]) -> Result { - preflight_proof(proof)?; - let proof = MixedProof::from_bytes_without_deps(deps, proof).map_err(|_| ())?; +// A recursive child is proven against its declared dependencies, which the caller supplies. +fn decode_child(deps: &[Dep], proof: &[u8]) -> Result { + if proof.is_empty() || proof.len() > MAX_BYTES { + return Err(()); + } + let proof = EthereumProof::from_proof_bytes(proof, claims(deps)).map_err(|_| ())?; proof.verify().map_err(|_| ())?; Ok(proof) } -const RAW_SIGNATURES_PER_LEAF: usize = 4; -const RAW_GENERICS_PER_LEAF: usize = 1; -const RECURSIVE_FAN_IN: usize = 2; - -fn aggregate_bounded( - children: Vec, - raw: Vec<(Dep, SphincsPublicKey, [u8; 32], SphincsSignature)>, - generic: Vec<(Dep, Program, cpu::Proof)>, - desired: &[Dep], -) -> Result { - let selected: BTreeSet<_> = desired.iter().copied().collect(); - let mut nodes: Vec<_> = children - .into_iter() - .filter(|child| { - child - .dependencies() - .iter() - .any(|claim| selected.contains(claim)) - }) - .collect(); - if let Some(index) = nodes.iter().position(|node| node.dependencies() == desired) { - return Ok(nodes.swap_remove(index)); - } - let covered: HashSet<_> = nodes - .iter() - .flat_map(|node| node.dependencies().iter().copied()) - .collect(); - let mut seen = HashSet::new(); - let raw: Vec<_> = raw - .into_iter() - .filter(|(dep, _, _, _)| { - selected.contains(dep) && !covered.contains(dep) && seen.insert(*dep) - }) - .collect(); - let generic: Vec<_> = generic - .into_iter() - .filter(|(dep, _, _)| selected.contains(dep) && !covered.contains(dep) && seen.insert(*dep)) - .collect(); - for leaf in raw.chunks(RAW_SIGNATURES_PER_LEAF) { - let signatures: Vec<_> = leaf - .iter() - .map(|(_, pk, message, signature)| (*pk, *message, signature.clone())) - .collect(); - let deps: Vec<_> = leaf - .iter() - .map(|(dep, _, _, _)| *dep) - .collect::>() - .into_iter() - .collect(); - nodes.push(aggregate_mixed(&[], &signatures, &[], &deps)?); - } - for leaf in generic.chunks(RAW_GENERICS_PER_LEAF) { - let inputs: Vec<_> = leaf - .iter() - .map(|(dep, program, proof)| GenericInput { - program, - proof, - data: dep[32..64].try_into().unwrap(), - }) - .collect(); - let deps: Vec<_> = leaf - .iter() - .map(|(dep, _, _)| *dep) - .collect::>() - .into_iter() - .collect(); - nodes.push(aggregate_mixed(&[], &[], &inputs, &deps)?); - } - while nodes.len() > 1 { - let mut next = Vec::new(); - let mut input = nodes.into_iter(); - while let Some(first) = input.next() { - let mut batch = vec![first]; - batch.extend(input.by_ref().take(RECURSIVE_FAN_IN - 1)); - let deps: Vec<_> = batch - .iter() - .flat_map(|node| node.dependencies().iter().copied()) - .filter(|dep| selected.contains(dep)) - .collect::>() - .into_iter() - .collect(); - if batch.len() == 1 && batch[0].dependencies() == deps { - next.push(batch.pop().ok_or(())?); - } else { - next.push(aggregate_mixed(&batch, &[], &[], &deps)?); - } - } - nodes = next; - } - let node = nodes.pop().ok_or(())?; - if node.dependencies() == desired { - return Ok(node); - } - aggregate_mixed(&[node], &[], &[], desired) -} - pub fn prove_aggregate(hash: &[u8; 32], input: &[u8]) -> Result, ()> { if input.len() > MAX_INPUT_BYTES { return Err(()); @@ -719,19 +229,9 @@ pub fn prove_aggregate(hash: &[u8; 32], input: &[u8]) -> Result, ()> { let mut r = Reader { bytes: input }; let mut all = Vec::new(); let mut raw = Vec::new(); - let mut generic = Vec::new(); for _ in 0..r.count()? { let dep = r.dep()?; - let witness = r.blob()?; - let data = dep[32..64].try_into().map_err(|_| ())?; - let key = dep[64..96].try_into().map_err(|_| ())?; - if dep[31] == 0x10 { - let (pk, signature) = parse_sphincs(&data, &key, witness)?; - raw.push((dep, pk, data, signature)); - } else { - let (program, proof) = decode_stark(&data, &key, witness)?; - generic.push((dep, program, proof)); - } + raw.push((dep, r.blob()?)); all.push(dep); } let child_count = r.count()?; @@ -748,7 +248,10 @@ pub fn prove_aggregate(hash: &[u8; 32], input: &[u8]) -> Result, ()> { } continue; } - children.push(decode_child(&declared, proof)?); + if generic_count(&declared) > MAX_GENERIC_DEPENDENCIES { + return Err(()); + } + children.push((decode_child(&declared, proof)?, proof, declared.clone())); all.extend(declared); } let discards: HashSet<_> = r.deps()?.into_iter().collect(); @@ -764,7 +267,7 @@ pub fn prove_aggregate(hash: &[u8; 32], input: &[u8]) -> Result, ()> { .into_iter() .collect(); if deps.len() > MAX_DEPS - || deps.iter().filter(|d| d[31] == 0x11).count() > MAX_GENERIC_STARKS + || generic_count(&deps) > MAX_GENERIC_DEPENDENCIES || commitment(&deps) != *hash { return Err(()); @@ -772,14 +275,53 @@ pub fn prove_aggregate(hash: &[u8; 32], input: &[u8]) -> Result, ()> { if deps.is_empty() { return Ok(vec![]); } - let proof = aggregate_bounded(children, raw, generic, &deps)?.to_bytes_without_deps(); - if proof.len() > MAX_MIXED_GUEST_BYTES { + let kept: HashSet<_> = deps.iter().copied().collect(); + children.retain(|(_, _, declared)| declared.iter().any(|dep| kept.contains(dep))); + let covered: HashSet<_> = children + .iter() + .flat_map(|(_, _, declared)| declared.iter().copied()) + .collect(); + // The guest checks only the witnesses it is given; one that a child covers or a discard drops is checked here, + // so an invalid witness fails the call whether or not the proof needs it. + let (raw, unused): (Vec<_>, Vec<_>) = raw + .into_iter() + .partition(|(dep, _)| kept.contains(dep) && !covered.contains(dep)); + if unused + .iter() + .any(|(dep, witness)| !verify_witness(dep, witness)) + { + return Err(()); + } + if let Some((_, bytes, _)) = children.iter().find(|(_, _, declared)| *declared == deps) { + return Ok(bytes.to_vec()); + } + let mut signatures = Vec::new(); + let mut generic = Vec::new(); + for (dep, witness) in raw { + if dep[31] == LEAN_STARK { + generic.push((data(&dep), key(&dep), witness.to_vec())); + } else { + let (pk, signature) = parse_sphincs(&key(&dep), witness)?; + signatures.push((pk, data(&dep), signature)); + } + } + let children: Vec<_> = children.into_iter().map(|(child, ..)| child).collect(); + let proof = rec_aggregation::aggregate( + &children, + generic, + signatures, + &[], + Some(&claims(&deps)), + LOG_INV_RATE, + ) + .map_err(|_| ())? + .proof_bytes(); + if proof.len() > MAX_BYTES { return Err(()); } Ok(proof) } -// Crypto ABI calls bound buffers and contain upstream panics; free requires its exact owned allocation. unsafe fn bytes<'a>(ptr: *const u8, len: usize) -> Result<&'a [u8], ()> { unsafe { bytes_with_limit(ptr, len, MAX_BYTES) } } @@ -823,6 +365,7 @@ fn checked(f: impl FnOnce() -> Result) -> i32 { }); ABI_WORKERS.get_or_init(|| { let dispatcher = std::thread::current().id(); + parallel::init(); let count = parallel::num_threads(); let barrier = Barrier::new(count); let workers = Mutex::new(Vec::new()); @@ -853,7 +396,7 @@ fn checked(f: impl FnOnce() -> Result) -> i32 { } #[unsafe(no_mangle)] pub extern "C" fn nlean_abi_version() -> u32 { - 10 + 12 } /// Copies up to `len` bytes of the calling thread's last contained panic message into `out` and returns its full /// length; empty unless that thread's latest `checked` call returned -1. @@ -873,22 +416,20 @@ pub unsafe extern "C" fn nlean_last_error(out: *mut u8, len: usize) -> usize { }) } /// # Safety -/// Output must hold nine writable u32 values. +/// Output must hold seven writable u32 values. #[unsafe(no_mangle)] pub unsafe extern "C" fn nlean_limits(out: *mut u32, len: usize) -> i32 { - if out.is_null() || len != 9 { + if out.is_null() || len != 7 { return 0; } let limits = [ MAX_BYTES as u32, MAX_DEPS as u32, rec_aggregation::MAX_RECURSIONS as u32, - (sphincs::PUB_KEY_SIZE + sphincs::SIG_SIZE) as u32, - MAX_GENERIC_STARKS as u32, - MAX_INSTRUCTIONS as u32, - MAX_OPERAND, + (lean_sphincs::PUB_KEY_SIZE + lean_sphincs::SIG_SIZE) as u32, MAX_INPUT_BYTES as u32, - MAX_MIXED_GUEST_BYTES as u32, + MAX_GENERIC_DEPENDENCIES as u32, + 1 << MAX_GENERIC_BYTECODE_LOG, ]; unsafe { std::ptr::copy_nonoverlapping(limits.as_ptr(), out, limits.len()); @@ -937,7 +478,7 @@ pub unsafe extern "C" fn nlean_verify_leanstark( len: usize, ) -> i32 { checked(|| { - Ok(verify_stark( + Ok(rec_aggregation::verify_generic( unsafe { bytes(data, 32)? }.try_into().map_err(|_| ())?, unsafe { bytes(vk, 32)? }.try_into().map_err(|_| ())?, unsafe { bytes(witness, len)? }, @@ -1004,836 +545,193 @@ pub unsafe extern "C" fn nlean_free(ptr: *mut u8, len: usize) { } } -pub fn prove_stark(source: &str, hash: &[u8; 32]) -> Result<(Vec, [u8; 32]), ()> { - let _guard = begin_proof(); - let program = lean_compiler::compile(&lean_compiler::parse(source).map_err(|_| ())?); - let key = eip8288_mixed::generic_verification_key(&program).map_err(|_| ())?; - let code = encode_program(&program); - let (proof, _) = in_arena_phase(|| cpu::prove(&program, pi(hash), 1)).map_err(|_| ())?; - if !eip8288_mixed::generic_recursion_supported(&program, &proof) { - return Err(()); - } - let mut out = Vec::new(); - put_blob(&mut out, &code); - out.extend(wire().serialize(&proof).map_err(|_| ())?); - if out.len() > MAX_BYTES { - return Err(()); - } - Ok((out, key)) -} - #[cfg(test)] mod tests { use super::*; - #[test] - fn exact_empty_proof_bypasses_an_occupied_prover() { - let hash = commitment(&[]); - let guard = PROVER - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - let (send, receive) = std::sync::mpsc::channel(); - let worker = std::thread::spawn(move || { - let valid = prove_aggregate(&hash, &[0; 12]); - let mut wrong_hash = hash; - wrong_hash[0] ^= 1; - let invalid = prove_aggregate(&wrong_hash, &[0; 12]); - send.send((valid, invalid)).unwrap(); - }); - let result = receive.recv_timeout(std::time::Duration::from_secs(3)); - // Release before joining even if the fast path regresses to taking the lock. - drop(guard); - worker.join().unwrap(); - let (proof, invalid) = result.expect("empty proof must not wait for the prover"); - let proof = proof.unwrap(); - // EIP-8288: no dependencies, empty stark_proof; callers check that case without the guest. - assert!(proof.is_empty()); - assert!(verify_aggregate(&hash, &proof).is_err()); - assert!(invalid.is_err()); - for malformed in [&[0; 11][..], &[0; 13][..]] { - assert!(prove_aggregate(&hash, malformed).is_err()); - } - } - - /// Expected digests computed in Python: leanSPHINCS-only lists from the EIP-8288 `get_deps_hash` - /// pseudocode, lists with leanSTARK entries from the split-domain deviation. - #[test] - fn dependency_commitment_matches_eip_pseudocode_vectors() { - let dep = |scheme: u8, data: [u8; 32], key: [u8; 32]| { - let mut dep = [0; 96]; - dep[31] = scheme; - dep[32..64].copy_from_slice(&data); - dep[64..].copy_from_slice(&key); - dep - }; - let key = |start: u8| std::array::from_fn(|i| start + i as u8); - let (a, b, c, d) = ( - dep(0x10, [7; 32], key(0)), - dep(0x10, [7; 32], key(1)), - dep(0x10, [1; 32], [0xff; 32]), - dep(0x10, [0xa5; 32], [0x5a; 32]), - ); - let (g1, g2) = ( - dep(0x11, [7; 32], key(0)), - dep(0x11, [0xa5; 32], [0x5a; 32]), - ); - for (deps, expected) in [ - ( - vec![], - "69217a3079908094e11121d042354a7c1f55b6482ca1a51e1b250dfd1ed0eef9", - ), - ( - vec![a], - "6921fc8275ae947e76ab603255f550b615341fb880a8635996d30b55201d28f6", - ), - ( - vec![b, a, c, a], - "27ad454def73a88020d3f7a6428cd790cf354b531b0447013027e7fa277b9e13", - ), - ( - vec![d, a, c, b], - "830b7c9edb9a816e25929dbae987d3fcf2e900fa009d930c60eddacb0d479ed8", - ), - ( - vec![g1], - "3ae64e624f1d16e19a7699b95f501134828652df566420f26a4f298b5bb54b9a", - ), - ( - vec![g2, a, g1, c], - "ba1180b0f634d713dbf9b5b87e68a36786f550676d2b05d4f49f699eeb42e90f", - ), - ( - vec![g2, d], - "b5a19f98eacfd5f1ac57b535e3aadbdfaf430698ebe563422ba883aa02f38adf", - ), - ] { - let digest = commitment(&canonical(deps)); - assert_eq!( - digest - .iter() - .map(|b| format!("{b:02x}")) - .collect::(), - expected - ); - } - assert_ne!(commitment(&[a]), commitment(&[g1])); - assert_ne!(commitment(&[d]), commitment(&[g2])); - } - - #[test] - fn public_input_packing_preserves_every_bit() { - for bit in 0..256 { - let mut message = [0; 32]; - message[bit / 8] = 1 << (bit % 8); - let cells = pi(&message); - let decoded = [ - cells[0].c0.to_le_bytes(), - cells[0].c1.to_le_bytes(), - cells[1].c0.to_le_bytes(), - cells[1].c1.to_le_bytes(), - ] - .concat(); - assert_eq!(decoded, message); - assert_eq!((cells[0].c2, cells[1].c2), (0, 0)); - } - let cells = pi(&[255; 32]); - assert_eq!( - (cells[0].c0, cells[0].c1, cells[1].c0, cells[1].c1), - (u64::MAX, u64::MAX, u64::MAX, u64::MAX) - ); - } - #[test] - fn real_signature_and_vm_proof_bind_message_and_key() { - let (sk, pk) = sphincs::key_gen_from_seed([42; 32]); - let message = [7; 32]; - let signature = [ - pk.flatten().as_slice(), - sphincs::sign(&sk, &message).to_bytes().as_slice(), - ] - .concat(); - assert!(verify_sphincs(&message, &pk.flatten(), &signature)); - assert!(!verify_sphincs(&[8; 32], &pk.flatten(), &signature)); - assert!(!verify_sphincs(&message, &[0; 32], &signature)); - assert!(!verify_sphincs( - &message, - &keccak(&pk.flatten()), - &signature - )); - let mut bad = signature; - bad[10] ^= 1; - assert!(!verify_sphincs(&message, &pk.flatten(), &bad)); - let mut small = [0; 32]; - small[0] = 7; - small[16] = 9; - for input in [small, [255; 32]] { - let words: Vec<_> = input - .chunks_exact(8) - .map(|word| u64::from_le_bytes(word.try_into().unwrap())) - .collect(); - let source = format!( - "from snark_lib import *\ndef main():\n p = GEN ** 0\n p[1] = f192({}, {}, 0)\n p[GEN] = f192({}, {}, 0)\n return\n", - words[0], words[1], words[2], words[3] - ); - let (proof, key) = prove_stark(&source, &input).unwrap(); - assert!(verify_stark(&input, &key, &proof)); - let mut encoded = Reader { bytes: &proof }; - let code = encoded.blob().unwrap(); - // EIP-8288: vk_hash = keccak256(domain_tag || canonical_vk), domain_tag = "ethereum/8288/leanSTARK/vk/v1" || 0x00. - assert_eq!( - &eip8288_mixed::GENERIC_VK_DOMAIN[..], - b"ethereum/8288/leanSTARK/vk/v1\0" - ); - assert_eq!( - key, - keccak(&[b"ethereum/8288/leanSTARK/vk/v1\0".as_slice(), code].concat()) - ); - let former_key = - keccak(&[b"eip8288/leanstark-vk/leanvm-cpu\0".as_slice(), code].concat()); - assert!(!verify_stark(&input, &former_key, &proof)); - for index in [7, 15, 23, 31] { - let mut wrong = input; - wrong[index] ^= 1; - assert!(!verify_stark(&wrong, &key, &proof)); - } - assert!(!verify_stark(&input, &[0; 32], &proof)); - let mut bad = proof; - *bad.last_mut().unwrap() ^= 1; - assert!(!verify_stark(&input, &key, &bad)); - } - } - #[test] - fn real_recursive_composition_and_discards_bind_full_dependency_set() { - let (sk, pk) = sphincs::key_gen_from_seed([42; 32]); - let message = [7; 32]; - let signature = [ - pk.flatten().as_slice(), - sphincs::sign(&sk, &message).to_bytes().as_slice(), - ] - .concat(); - let dep = sphincs_dependency(&pk, &message); - let mut input = Vec::new(); - put_number(&mut input, 1); - input.extend_from_slice(&dep); - put_blob(&mut input, &signature); - put_number(&mut input, 0); - put_deps(&mut input, &[]); - let leaf = prove_aggregate(&commitment(&[dep]), &input).unwrap(); - verify_aggregate(&commitment(&[dep]), &leaf).unwrap(); - assert!( - !leaf.windows(96).any(|bytes| bytes == dep), - "stark_proof carries no dependency" - ); - let mut parent = Vec::new(); - put_number(&mut parent, 0); - put_number(&mut parent, 1); - put_deps(&mut parent, &[dep]); - put_blob(&mut parent, &leaf); - put_deps(&mut parent, &[]); - let root = prove_aggregate(&commitment(&[dep]), &parent).unwrap(); - verify_aggregate(&commitment(&[dep]), &root).unwrap(); - let mut other = dep; - other[40] ^= 1; - assert!(verify_aggregate(&commitment(&[other]), &root).is_err()); - let mut forged = root.clone(); - *forged.last_mut().unwrap() ^= 1; - assert!(verify_aggregate(&commitment(&[dep]), &forged).is_err()); - let mut discard = Vec::new(); - put_number(&mut discard, 0); - put_number(&mut discard, 1); - put_deps(&mut discard, &[dep]); - put_blob(&mut discard, &leaf); - put_deps(&mut discard, &[dep]); - let empty = prove_aggregate(&commitment(&[]), &discard).unwrap(); - assert!(empty.is_empty()); - assert!(prove_aggregate(&commitment(&[dep]), &discard).is_err()); - // The scheme is bound: the same data and key under leanSTARK is another dependency. - let mut restated = dep; - restated[31] = 0x11; - for proof in [&leaf, &root] { - assert!(verify_aggregate(&commitment(&[restated]), proof).is_err()); - } - let mut other_scheme = Vec::new(); - put_number(&mut other_scheme, 0); - put_number(&mut other_scheme, 1); - put_deps(&mut other_scheme, &[dep]); - put_blob(&mut other_scheme, &leaf); - put_deps(&mut other_scheme, &[restated]); - // A discard outside the input union is rejected, even when it differs from an input only in its scheme. - assert!(prove_aggregate(&commitment(&[]), &other_scheme).is_err()); - assert!(prove_aggregate(&commitment(&[dep]), &other_scheme).is_err()); - // Unknown schemes fail in direct, recursive and discard lists alike. - for scheme in [0x00, 0x12] { - let mut unknown = dep; - unknown[31] = scheme; - let mut direct = Vec::new(); - put_number(&mut direct, 1); - direct.extend_from_slice(&unknown); - put_blob(&mut direct, &signature); - put_number(&mut direct, 0); - put_deps(&mut direct, &[]); - let mut nested = Vec::new(); - put_number(&mut nested, 0); - put_number(&mut nested, 1); - put_deps(&mut nested, &[dep, unknown]); - put_blob(&mut nested, &leaf); - put_deps(&mut nested, &[unknown]); - let mut discarded = Vec::new(); - put_number(&mut discarded, 0); - put_number(&mut discarded, 1); - put_deps(&mut discarded, &[dep]); - put_blob(&mut discarded, &leaf); - put_deps(&mut discarded, &[unknown]); - for input in [direct, nested, discarded] { - assert!(prove_aggregate(&commitment(&[dep]), &input).is_err()); - } - } - } - #[test] - fn binary_recursion_preserves_overlapping_parents_and_partial_discards() { - let (sk, pk) = sphincs::key_gen_from_seed([43; 32]); - let mut deps = Vec::new(); - let mut input = Vec::new(); - put_number(&mut input, 3); - for index in 0..3u8 { - let message = [index; 32]; - let dep = sphincs_dependency(&pk, &message); - let witness = [ - pk.flatten().as_slice(), - sphincs::sign(&sk, &message).to_bytes().as_slice(), - ] - .concat(); - deps.push(dep); - input.extend_from_slice(&dep); - put_blob(&mut input, &witness); - } - deps.sort(); - put_number(&mut input, 0); - put_deps(&mut input, &[]); - let all = prove_aggregate(&commitment(&deps), &input).unwrap(); - verify_aggregate(&commitment(&deps), &all).unwrap(); - let combine = |parents: &[(&[Dep], &[u8])], discards: &[Dep], expected: &[Dep]| { - let mut input = Vec::new(); - put_number(&mut input, 0); - put_number(&mut input, parents.len()); - for (inner, proof) in parents { - put_deps(&mut input, inner); - put_blob(&mut input, proof); - } - put_deps(&mut input, discards); - prove_aggregate(&commitment(expected), &input).unwrap() - }; - let left = combine(&[(&deps, &all)], &[deps[2]], &deps[..2]); - let right = combine(&[(&deps, &all)], &[deps[0]], &deps[1..]); - let retained = vec![deps[0], deps[2]]; - let root = combine( - &[(&deps[..2], &left), (&deps[1..], &right)], - &[deps[1]], - &retained, - ); - verify_aggregate(&commitment(&retained), &root).unwrap(); - let repeated = combine(&[(&retained, &root)], &[], &retained); - verify_aggregate(&commitment(&retained), &repeated).unwrap(); - assert_eq!(root, repeated); - let redundant = combine( - &[(&retained, &root), (&deps[..2], &left)], - &[deps[1]], - &retained, - ); - assert_eq!(root, redundant); - assert!(verify_aggregate(&commitment(&deps), &root).is_err()); - } - #[test] - fn malformed_buffers_fail_closed() { - assert_eq!( - unsafe { - nlean_verify_leansphincs(std::ptr::null(), std::ptr::null(), std::ptr::null(), 0) - }, - 0 - ); - assert_eq!( - unsafe { - nlean_verify_leanstark([0; 32].as_ptr(), [0; 32].as_ptr(), [0; 8].as_ptr(), 8) - }, - 0 - ); - let mut declared = [0; 96]; - declared[31] = 0x10; - let hash = commitment(&[declared]); - assert!(verify_aggregate(&hash, &[0xff; 8]).is_err()); - assert!(verify_aggregate(&hash, &[0]).is_err()); - // Reject malicious nested vector lengths before upstream claim reconstruction. - for preceding_vectors in 0..8 { - let mut nested = vec![0; preceding_vectors * 8]; - nested.extend_from_slice(&u64::MAX.to_le_bytes()); - assert!(verify_aggregate(&hash, &nested).is_err()); - assert!(decode_child(&[declared], &nested).is_err()); - } - let core = ( - vec![F192::ZERO; 33], - [Vec::::new(), Vec::::new()], - cpu::Proof { - stream: vec![], - merkle: vec![], - }, - ); - assert!(verify_aggregate(&hash, &wire().serialize(&core).unwrap()).is_err()); - } - #[test] - fn preflight_rejects_allocation_hints_and_excess_generic_claims() { - let mut stream = u64::MAX.to_le_bytes().to_vec(); - stream.extend_from_slice(&0u64.to_le_bytes()); - assert!(Preflight::new(&stream).cpu().is_err()); - for (phases, rows, words) in [(129, 0, 0), (1, 4097, 0), (1, 1, u64::MAX)] { - let mut proof = 0u64.to_le_bytes().to_vec(); - proof.extend_from_slice(&(phases as u64).to_le_bytes()); - proof.extend_from_slice(&(rows as u64).to_le_bytes()); - proof.extend_from_slice(&words.to_le_bytes()); - assert!(Preflight::new(&proof).cpu().is_err()); - } - let mut deps = vec![[0u8; 96]; MAX_GENERIC_STARKS + 1]; - for (i, dep) in deps.iter_mut().enumerate() { - dep[31] = 0x11; - dep[63] = i as u8; - } - let mut input = Vec::new(); - put_number(&mut input, 0); - put_number(&mut input, 1); - put_deps(&mut input, &deps); - put_blob(&mut input, &[0]); - put_deps(&mut input, &[]); - assert!(decode_child(&deps, &[0]).is_err()); - assert!(prove_aggregate(&commitment(&deps), &input).is_err()); - } - #[test] - fn bytecode_offsets_are_bounded_before_upstream_assembly() { - let mut code = 1u32.to_le_bytes().to_vec(); - code.push(2); // SET - code.extend_from_slice(&u32::MAX.to_le_bytes()); - code.extend_from_slice(&[0; 40]); - assert!(stark_program(&code).is_err()); - } - #[test] - fn exported_limits_match_and_panics_fail_closed() { - assert_eq!(nlean_abi_version(), 10); - let mut key = [0u8; 32]; - assert_eq!(unsafe { nlean_aggregated_vk(key.as_mut_ptr()) }, 1); - assert_eq!( - key.iter() - .map(|byte| format!("{byte:02x}")) - .collect::(), - "417152e8bd83e0dfb6f3e2bb8991e948f62c7f15d74b693b55979a40b239efa1" - ); - let mut limits = [0u32; 9]; - assert_eq!(unsafe { nlean_limits(limits.as_mut_ptr(), 9) }, 1); - assert_eq!( - limits, - [ - MAX_BYTES as u32, - MAX_DEPS as u32, - rec_aggregation::MAX_RECURSIONS as u32, - (sphincs::PUB_KEY_SIZE + sphincs::SIG_SIZE) as u32, - MAX_GENERIC_STARKS as u32, - MAX_INSTRUCTIONS as u32, - MAX_OPERAND, - MAX_INPUT_BYTES as u32, - MAX_MIXED_GUEST_BYTES as u32 - ] - ); - assert_eq!(unsafe { nlean_limits(std::ptr::null_mut(), 9) }, 0); - assert_eq!(unsafe { nlean_limits(limits.as_mut_ptr(), 8) }, 0); - let last_error = || { - let mut message = [0u8; 64]; - let len = unsafe { nlean_last_error(message.as_mut_ptr(), message.len()) }; - String::from_utf8(message[..len].to_vec()).unwrap() - }; - assert_eq!(checked(|| panic!("contained upstream failure")), -1); - assert_eq!(last_error(), "contained upstream failure"); - let pool_panic = || { - parallel::for_each(parallel::num_threads(), |_| panic!("pool task failed")); - Ok(true) - }; - assert_eq!(checked(pool_panic), -1); - assert_eq!(last_error(), "pool task failed"); - assert_eq!(checked(|| Err(())), 0); - assert_eq!(last_error(), ""); + fn signed(index: u8) -> (Dep, Vec) { + let (sk, pk) = lean_sphincs::key_gen_from_seed([index; 32]); + let message = keccak(&[index]); + let mut dep = [0; 96]; + dep[31] = 0x10; + dep[32..64].copy_from_slice(&message); + dep[64..].copy_from_slice(&verification_key(&pk)); + let signature = lean_sphincs::sign(&sk, &message).unwrap(); + ( + dep, + [&verification_key(&pk)[..], &signature.to_bytes()[..]].concat(), + ) + } + fn put_number(out: &mut Vec, n: usize) { + out.extend_from_slice(&(n as u32).to_le_bytes()); + } + fn input(raw: &[(Dep, Vec)], children: &[(&[Dep], &[u8])], discards: &[Dep]) -> Vec { + let mut out = Vec::new(); + put_number(&mut out, raw.len()); + for (dep, witness) in raw { + out.extend_from_slice(dep); + put_number(&mut out, witness.len()); + out.extend_from_slice(witness); + } + put_number(&mut out, children.len()); + for (deps, proof) in children { + put_number(&mut out, deps.len()); + out.extend(deps.iter().flatten()); + put_number(&mut out, proof.len()); + out.extend_from_slice(proof); + } + put_number(&mut out, discards.len()); + out.extend(discards.iter().flatten()); + out + } + fn hash_of(deps: &[Dep]) -> [u8; 32] { + commitment(&canonical(deps.to_vec())) } #[test] - fn generic_input_order_does_not_change_canonical_dependencies() { - let mut entries = Vec::new(); - for value in [7u8, 8] { - let source = format!( - "from snark_lib import *\ndef main():\n p = GEN ** 0\n p[1] = {value}\n p[GEN] = 9\n return\n" - ); - let mut message = [0; 32]; - message[0] = value; - message[16] = 9; - let (witness, key) = prove_stark(&source, &message).unwrap(); - let mut dep = [0; 96]; - dep[31] = 0x11; - dep[32..64].copy_from_slice(&message); - dep[64..].copy_from_slice(&key); - entries.push((dep, witness)); - } - entries.sort_by_key(|(dep, _)| *dep); - let deps: Vec<_> = entries.iter().map(|(dep, _)| *dep).collect(); - for reverse in [false, true] { - if reverse { - entries.reverse(); - } - let mut input = Vec::new(); - put_number(&mut input, entries.len()); - for (dep, witness) in &entries { - input.extend_from_slice(dep); - put_blob(&mut input, witness); - } - put_number(&mut input, 0); - put_deps(&mut input, &[]); - let proof = prove_aggregate(&commitment(&deps), &input).unwrap(); - verify_aggregate(&commitment(&deps), &proof).unwrap(); - let noncanonical: Vec<_> = deps.iter().rev().copied().collect(); - assert!(verify_aggregate(&commitment(&noncanonical), &proof).is_err()); - } + fn commitment_is_keccak_of_the_64_byte_entries() { + let deps = canonical((0..3).map(|i| signed(i).0).collect()); + let entries: Vec = deps.iter().flat_map(|d| d[32..].iter().copied()).collect(); + assert_eq!(commitment(&deps), keccak(&entries)); + assert_eq!(commitment(&[]), keccak(&[])); } #[test] - fn recursive_generic_parent_compresses_large_inputs_and_preserves_discards() { - let mut entries = Vec::new(); - for value in 7u8..23 { - let source = format!( - "from snark_lib import *\ndef main():\n p = GEN ** 0\n p[1] = {value}\n p[GEN] = 9\n return\n" - ); - let mut message = [0; 32]; - message[0] = value; - message[16] = 9; - let (witness, key) = prove_stark(&source, &message).unwrap(); - let mut dep = [0; 96]; - dep[31] = 0x11; - dep[32..64].copy_from_slice(&message); - dep[64..].copy_from_slice(&key); - entries.push((dep, witness)); - } - entries.sort_by_key(|(dep, _)| *dep); - let deps: Vec<_> = entries.iter().map(|(dep, _)| *dep).collect(); - let mut input = Vec::new(); - put_number(&mut input, entries.len()); - for (dep, witness) in &entries { - input.extend_from_slice(dep); - put_blob(&mut input, witness); - } - put_number(&mut input, 0); - put_deps(&mut input, &[]); - assert!(input.len() > 4 * 1024 * 1024 && input.len() <= MAX_INPUT_BYTES); - let parent = prove_aggregate(&commitment(&deps), &input).unwrap(); - assert!(parent.len() < 1024 * 1024); - verify_aggregate(&commitment(&deps), &parent).unwrap(); - - let reaggregate = |retained: &[Dep], discards: &[Dep]| { - let mut input = Vec::new(); - put_number(&mut input, 0); - put_number(&mut input, 1); - put_deps(&mut input, &deps); - put_blob(&mut input, &parent); - put_deps(&mut input, discards); - assert!(input.len() < 1024 * 1024); - let proof = prove_aggregate(&commitment(retained), &input).unwrap(); - verify_aggregate(&commitment(retained), &proof).unwrap(); - assert!(prove_aggregate(&[0; 32], &input).is_err()); - proof - }; - let all = reaggregate(&deps, &[]); - assert_eq!(all, parent); - let one = reaggregate(&deps[..1], &deps[1..]); - assert!(one.len() < 1024 * 1024); - let (sk, pk) = sphincs::key_gen_from_seed([44; 32]); - let message = [44; 32]; - let sphincs_dep = sphincs_dependency(&pk, &message); - let signature = [ - pk.flatten().as_slice(), - sphincs::sign(&sk, &message).to_bytes().as_slice(), - ] - .concat(); - let mut mixed_deps = deps.clone(); - mixed_deps.push(sphincs_dep); - mixed_deps.sort(); - let mut mixed_input = Vec::new(); - put_number(&mut mixed_input, 1); - mixed_input.extend_from_slice(&sphincs_dep); - put_blob(&mut mixed_input, &signature); - put_number(&mut mixed_input, 1); - put_deps(&mut mixed_input, &deps); - put_blob(&mut mixed_input, &parent); - put_deps(&mut mixed_input, &[]); - let mixed = prove_aggregate(&commitment(&mixed_deps), &mixed_input).unwrap(); - verify_aggregate(&commitment(&mixed_deps), &mixed).unwrap(); - assert!(mixed.len() <= MAX_MIXED_GUEST_BYTES); - let mut discard = Vec::new(); - put_number(&mut discard, 0); - put_number(&mut discard, 1); - put_deps(&mut discard, &mixed_deps); - put_blob(&mut discard, &mixed); - put_deps(&mut discard, &[sphincs_dep]); - let generic_only = prove_aggregate(&commitment(&deps), &discard).unwrap(); - verify_aggregate(&commitment(&deps), &generic_only).unwrap(); - eprintln!( - "generic16 parent={} retained16={} retained1={} mixed={}", - parent.len(), - all.len(), - one.len(), - mixed.len() - ); + fn empty_list_has_the_empty_proof_and_never_reaches_the_guest() { + let hash = commitment(&[]); + assert!(prove_aggregate(&hash, &[0; 12]).unwrap().is_empty()); + assert!(prove_aggregate(&[1; 32], &[0; 12]).is_err()); + assert!(verify_aggregate(&hash, &[]).is_err()); } - #[test] - fn oversized_root_fails_closed() { + fn proven(index: u8) -> (Dep, Vec) { + let message = keccak(&[0x51, index]); + let (witness, key) = prove_stark(ANY_INPUT_PROGRAM, &message).unwrap(); let mut dep = [0; 96]; - dep[31] = 0x11; - let hash = commitment(&[dep]); - assert!(verify_aggregate(&hash, &vec![0; MAX_MIXED_GUEST_BYTES + 1]).is_err()); - assert!(!verify_stark(&[0; 32], &[0; 32], &vec![0; MAX_BYTES + 1])); - } - - #[test] - fn duplicate_generic_claims_compress_once_and_reject_invalid_origins() { - let source = "from snark_lib import *\ndef main():\n p = GEN ** 0\n p[1] = 7\n p[GEN] = 9\n return\n"; - let mut message = [0; 32]; - message[0] = 7; - message[16] = 9; - let (first, key) = prove_stark(source, &message).unwrap(); - let program = lean_compiler::compile(&lean_compiler::parse(source).unwrap()); - let mut second = Vec::new(); - put_blob(&mut second, &encode_program(&program)); - let proof = { - let _guard = PROVER.lock().unwrap(); - cpu::prove(&program, pi(&message), 2).unwrap().0 - }; - cpu::verify(&program, &pi(&message), &proof).unwrap(); - second.extend(wire().serialize(&proof).unwrap()); - assert!(verify_stark(&message, &key, &first)); - assert!(!verify_stark(&message, &key, &second)); - let mut dep = [0; 96]; - dep[31] = 0x11; + dep[31] = LEAN_STARK; dep[32..64].copy_from_slice(&message); dep[64..].copy_from_slice(&key); - let leaf = |witness: &[u8]| { - let mut input = Vec::new(); - put_number(&mut input, 1); - input.extend_from_slice(&dep); - put_blob(&mut input, witness); - put_number(&mut input, 0); - put_deps(&mut input, &[]); - prove_aggregate(&commitment(&[dep]), &input).unwrap() - }; - let parent = leaf(&first); - let mut duplicated = Vec::new(); - put_number(&mut duplicated, 2); - for _ in 0..2 { - duplicated.extend_from_slice(&dep); - put_blob(&mut duplicated, &first); - } - put_number(&mut duplicated, 0); - put_deps(&mut duplicated, &[]); - let result = prove_aggregate(&commitment(&[dep]), &duplicated).unwrap(); - verify_aggregate(&commitment(&[dep]), &result).unwrap(); - assert!(result.len() < 1024 * 1024); - let mut input = Vec::new(); - put_number(&mut input, 1); - input.extend_from_slice(&dep); - put_blob(&mut input, &first); - put_number(&mut input, 2); - for _ in 0..2 { - put_deps(&mut input, &[dep]); - put_blob(&mut input, &parent); - } - put_deps(&mut input, &[]); - assert_eq!( - prove_aggregate(&commitment(&[dep]), &input).unwrap(), - parent - ); - let mut bad = first.clone(); - *bad.last_mut().unwrap() ^= 1; - for rejected in [&bad, &second] { - let mut input = Vec::new(); - put_number(&mut input, 2); - for witness in [&first, rejected] { - input.extend_from_slice(&dep); - put_blob(&mut input, witness); - } - put_number(&mut input, 1); - put_deps(&mut input, &[dep]); - put_blob(&mut input, &parent); - put_deps(&mut input, &[]); - assert!(prove_aggregate(&commitment(&[dep]), &input).is_err()); - } - } - - fn single_signature_input(seed: u8) -> ([u8; 32], Vec) { - let (sk, pk) = sphincs::key_gen_from_seed([seed; 32]); - let message = [seed; 32]; - let signature = [ - pk.flatten().as_slice(), - sphincs::sign(&sk, &message).to_bytes().as_slice(), - ] - .concat(); - let dep = sphincs_dependency(&pk, &message); - let mut input = Vec::new(); - put_number(&mut input, 1); - input.extend_from_slice(&dep); - put_blob(&mut input, &signature); - put_number(&mut input, 0); - put_deps(&mut input, &[]); - (commitment(&[dep]), input) - } - - #[test] - fn verification_and_proving_do_not_share_arena_phases() { - let (hash, input) = single_signature_input(44); - let proof = prove_aggregate(&hash, &input).unwrap(); - let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); - let verified = std::sync::Arc::new(AtomicUsize::new(0)); - let verifier = { - let (stop, verified, proof) = (stop.clone(), verified.clone(), proof.clone()); - std::thread::spawn(move || { - let mut failures = 0; - while !stop.load(Ordering::Acquire) { - failures += usize::from(verify_aggregate(&hash, &proof).is_err()); - verified.fetch_add(1, Ordering::Release); - } - failures - }) - }; - // Phase churn as a prover produces it, then a real proof whose arena buffers the verifier must not touch. - while verified.load(Ordering::Acquire) < 20 { - let _guard = begin_proof(); - for _ in 0..1000 { - in_arena_phase(|| drop(zk_alloc::enter_phase())); - } - } - let (other_hash, other_input) = single_signature_input(45); - let other = prove_aggregate(&other_hash, &other_input); - stop.store(true, Ordering::Release); - assert_eq!(verifier.join().unwrap(), 0); - verify_aggregate(&other_hash, &other.unwrap()).unwrap(); + (dep, witness) } #[test] - fn verification_does_not_wait_for_an_open_phase() { - const PHASE: std::time::Duration = std::time::Duration::from_secs(4); - let (hash, input) = single_signature_input(46); - let proof = prove_aggregate(&hash, &input).unwrap(); - let opened = std::sync::Arc::new(Barrier::new(2)); - let prover = { - let opened = opened.clone(); - std::thread::spawn(move || { - let _guard = begin_proof(); - in_arena_phase(|| { - let _phase = zk_alloc::enter_phase(); - opened.wait(); - std::thread::sleep(PHASE); - }); - }) - }; - opened.wait(); - let started = std::time::Instant::now(); - verify_aggregate(&hash, &proof).unwrap(); - let waited = started.elapsed(); - prover.join().unwrap(); + fn rejects_bad_witnesses_and_unknown_schemes() { + let (dep, witness) = signed(1); + assert!(verify_witness(&dep, &witness)); + let mut forged = witness.clone(); + forged[100] ^= 1; + assert!(!verify_witness(&dep, &forged)); + assert!(prove_aggregate(&hash_of(&[dep]), &input(&[(dep, forged)], &[], &[])).is_err()); + // A signature is no leanSTARK witness, and the scheme is part of the dependency. + let mut stark = dep; + stark[31] = LEAN_STARK; + assert!(!verify_witness(&stark, &witness)); + assert_ne!(hash_of(&[stark]), hash_of(&[dep])); + assert!( + prove_aggregate( + &hash_of(&[stark]), + &input(&[(stark, witness.clone())], &[], &[]) + ) + .is_err() + ); + let mut unknown = dep; + unknown[31] = 0x12; assert!( - waited < PHASE / 2, - "verification waited {waited:?} for the open phase" + prove_aggregate(&hash_of(&[dep]), &input(&[(unknown, witness)], &[], &[])).is_err() ); } - #[cfg(all(target_os = "linux", target_env = "gnu"))] #[test] - fn glibc_settings_parse_like_strtoul() { - assert_eq!(parse_c_integer("131072"), Some(131072)); - assert_eq!(parse_c_integer("0x40000"), Some(0x40000)); - assert_eq!(parse_c_integer("0100"), Some(64)); - assert_eq!(parse_c_integer("0"), Some(0)); - assert_eq!(parse_c_integer("4294967296"), Some(i32::MAX)); - assert_eq!(parse_c_integer("lots"), None); + fn mixed_commitment_joins_both_lists_under_a_domain() { + let sphincs = canonical((0..2).map(|i| signed(i).0).collect()); + let mut stark = sphincs[0]; + stark[31] = LEAN_STARK; + let list = |deps: &[Dep]| { + keccak( + &deps + .iter() + .flat_map(|d| d[32..].iter().copied()) + .collect::>(), + ) + }; + let mut domain = [0u8; 32]; + domain[..22].copy_from_slice(b"eip8288/leanstark-deps"); + let joined = + |s: &[Dep], g: &[Dep]| keccak(&[&list(s)[..], &list(g)[..], &domain[..]].concat()); + assert_eq!( + commitment(&canonical([&sphincs[..], &[stark]].concat())), + joined(&sphincs, &[stark]) + ); + assert_eq!(commitment(&[stark]), joined(&[], &[stark])); + assert_ne!(commitment(&[stark]), commitment(&sphincs[..1])); } - #[cfg(all(target_os = "linux", target_env = "gnu"))] #[test] - fn proving_leaves_host_heap_on_glibc_defaults() { - drop(begin_proof()); - // Above glibc's 32 MiB dynamic mmap threshold cap, below a thread arena's 64 MiB heap: only M_MMAP_MAX = 0 keeps it off mmap. - let before = unsafe { libc::mallinfo2() }.hblks; - let buffer = std::hint::black_box(vec![1u8; 40 << 20]); - assert!(unsafe { libc::mallinfo2() }.hblks > before); - drop(buffer); + fn proves_and_merges_leanstark_with_leansphincs() { + let (stark, stark_witness) = proven(1); + let (other, other_witness) = proven(2); + let (sig, sig_witness) = signed(3); + assert!(verify_witness(&stark, &stark_witness)); + assert!(!verify_witness(&other, &stark_witness)); + let mut forged = stark_witness.clone(); + *forged.last_mut().unwrap() ^= 1; + assert!(!verify_witness(&stark, &forged)); + assert!(prove_aggregate(&hash_of(&[stark]), &input(&[(stark, forged)], &[], &[])).is_err()); + + let mixed = [stark, sig]; + let leaf = prove_aggregate( + &hash_of(&mixed), + &input(&[(stark, stark_witness), (sig, sig_witness)], &[], &[]), + ) + .unwrap(); + assert!(verify_aggregate(&hash_of(&mixed), &leaf).is_ok()); + assert!(verify_aggregate(&hash_of(&[sig]), &leaf).is_err()); + assert!(verify_aggregate(&hash_of(&[stark]), &leaf).is_err()); + + // A mixed child plus a raw leanSTARK witness, with the child's signature discarded. + let declared = canonical(mixed.to_vec()); + let children = [(&declared[..], &leaf[..])]; + let kept = [stark, other]; + let merged = prove_aggregate( + &hash_of(&kept), + &input(&[(other, other_witness)], &children, &[sig]), + ) + .unwrap(); + assert!(verify_aggregate(&hash_of(&kept), &merged).is_ok()); + assert!(verify_aggregate(&hash_of(&[stark, other, sig]), &merged).is_err()); } #[test] - fn quiet_hook_preserves_unrelated_threads() { - const CHILD: &str = "NLEAN_QUIET_HOOK_TEST"; - if std::env::var_os(CHILD).is_none() { - let output = std::process::Command::new(std::env::current_exe().unwrap()) - .args([ - "--exact", - "tests::quiet_hook_preserves_unrelated_threads", - "--test-threads=1", - ]) - .env(CHILD, "1") - .output() - .unwrap(); - assert!( - output.status.success(), - "{}", - String::from_utf8_lossy(&output.stderr) - ); - return; - } - let diagnostics = std::sync::Arc::new(AtomicUsize::new(0)); - let observer = diagnostics.clone(); - std::panic::set_hook(Box::new(move |_| { - observer.fetch_add(1, Ordering::Relaxed); - })); + fn proves_merges_and_discards() { + let raw: Vec<_> = (0..5).map(signed).collect(); + let deps: Vec = raw.iter().map(|(dep, _)| *dep).collect(); + + let left = canonical(deps[..2].to_vec()); + let left_proof = prove_aggregate(&hash_of(&left), &input(&raw[..2], &[], &[])).unwrap(); + assert!(verify_aggregate(&hash_of(&left), &left_proof).is_ok()); + assert!(verify_aggregate(&hash_of(&deps[..3]), &left_proof).is_err()); + let mut tampered = left_proof.clone(); + *tampered.last_mut().unwrap() ^= 1; + assert!(verify_aggregate(&hash_of(&left), &tampered).is_err()); + + let right = canonical(deps[2..4].to_vec()); + let right_proof = prove_aggregate(&hash_of(&right), &input(&raw[2..4], &[], &[])).unwrap(); + + // Two children and one raw signature, with one child dependency discarded. + let children = [(&left[..], &left_proof[..]), (&right[..], &right_proof[..])]; + let kept = [deps[1], deps[2], deps[3], deps[4]]; + let merged = + prove_aggregate(&hash_of(&kept), &input(&raw[4..], &children, &deps[..1])).unwrap(); + assert!(verify_aggregate(&hash_of(&kept), &merged).is_ok()); + assert!(verify_aggregate(&hash_of(&deps), &merged).is_err()); + + // A child declared with dependencies it does not prove is refused. + let lying = [(&right[..], &left_proof[..])]; + assert!(prove_aggregate(&hash_of(&right), &input(&[], &lying, &[])).is_err()); + // A child that already covers the request is returned as is. assert_eq!( - checked(|| { - assert!( - std::thread::spawn(|| panic!("unrelated caller")) - .join() - .is_err() - ); - let count = parallel::num_threads(); - let barrier = Barrier::new(count); - let dispatcher = std::thread::current().id(); - parallel::for_each(count, |_| { - barrier.wait(); - if std::thread::current().id() != dispatcher { - panic!("contained upstream worker"); - } - }); - panic!("contained calling thread"); - }), - -1 - ); - assert_eq!(diagnostics.load(Ordering::Relaxed), 1); - assert!(std::panic::catch_unwind(|| panic!("caller outside ABI")).is_err()); - assert_eq!(diagnostics.load(Ordering::Relaxed), 2); - } - - #[test] - fn aggregation_input_allows_maximum_child_framing() { - let mut input = Vec::new(); - put_number(&mut input, 0); - put_number(&mut input, 2); - for _ in 0..2 { - put_deps(&mut input, &[]); - put_blob(&mut input, &vec![0; MAX_BYTES]); - } - put_deps(&mut input, &[]); - assert!(input.len() > MAX_BYTES && input.len() <= MAX_INPUT_BYTES); - // Buffer admission differs from proof validity; the fabricated child is still rejected. - assert!(unsafe { bytes_with_limit(input.as_ptr(), input.len(), MAX_INPUT_BYTES) }.is_ok()); - assert!(unsafe { bytes(input.as_ptr(), input.len()) }.is_err()); - assert!(prove_aggregate(&commitment(&[]), &input).is_err()); - assert!( - unsafe { bytes_with_limit(std::ptr::null(), MAX_INPUT_BYTES + 1, MAX_INPUT_BYTES) } - .is_err() + prove_aggregate(&hash_of(&left), &input(&[], &children[..1], &[])).unwrap(), + left_proof ); } } From 60558586c6842ab7e625affdb32a1fa1db0fac9d Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 12 Nov 2026 18:36:21 +0100 Subject: [PATCH 2/8] chore(ffi): record the vendored crates' upstream delta and how to move it crates/upstream.sh diff prints the EIP-8288 changes against upstream at the pinned revision; crates/upstream.sh update three-way merges the crates to a newer upstream revision and repins Cargo.toml and Cargo.lock. Co-Authored-By: Claude Opus 5.5 --- src/ffi/crates/upstream.sh | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100755 src/ffi/crates/upstream.sh diff --git a/src/ffi/crates/upstream.sh b/src/ffi/crates/upstream.sh new file mode 100755 index 0000000..78481f4 --- /dev/null +++ b/src/ffi/crates/upstream.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# The vendored crates are upstream leanVM at the revision Cargo.toml pins, plus this repository's EIP-8288 changes. +# Usage (LEANVM_REPO overrides the clone URL): +# crates/upstream.sh diff print those changes: upstream crates at the pin against crates/ +# crates/upstream.sh update move the crates and the pin to : each vendored file gets a three-way merge +# (ours, upstream at the pin, upstream at ); conflicts stay marked for review, +# files upstream added or removed are listed, not applied +set -euo pipefail +cd "$(dirname "$0")/.." +root=$PWD +mode=${1:?usage: crates/upstream.sh diff | update } +old=$(grep -o 'leanEthereum/leanVM", rev = "[0-9a-f]*"' Cargo.toml | grep -o '[0-9a-f]\{40\}' | sort -u) +[ "$(wc -l <<<"$old")" = 1 ] || { echo "Cargo.toml must pin exactly one leanVM revision" >&2; exit 1; } + +work=$(mktemp -d) +trap 'rm -rf -- "$work"' EXIT +git clone -q --filter=blob:none "${LEANVM_REPO:-https://github.com/leanEthereum/leanVM}" "$work/repo" +git -C "$work/repo" worktree add -q --detach "$work/base" "$old" + +if [ "$mode" = diff ]; then + for crate in rec_aggregation signatures; do + git diff --no-index --stat=200 -- "$work/base/crates/$crate" "crates/$crate" | sed "s|$work/base||g" || true + git diff --no-index -- "$work/base/crates/$crate" "crates/$crate" | sed "s|$work/base||g" || true + done + exit 0 +fi +[ "$mode" = update ] || { echo "usage: crates/upstream.sh diff | update " >&2; exit 1; } +new=$(git -C "$work/repo" rev-parse --verify "${2:?update needs a revision}^{commit}") +git -C "$work/repo" worktree add -q --detach "$work/next" "$new" + +conflicts=0 +for crate in rec_aggregation signatures; do + files=$(for tree in "$work/base" "$work/next" "$root"; do (cd "$tree/crates/$crate" && find . -type f); done | sort -u) + for f in $files; do + ours=crates/$crate/$f base=$work/base/crates/$crate/$f next=$work/next/crates/$crate/$f + if [ -f "$ours" ] && [ -f "$base" ] && [ -f "$next" ]; then + git merge-file -q -L ours -L "leanVM ${old:0:10}" -L "leanVM ${new:0:10}" "$ours" "$base" "$next" \ + || { echo "conflict: $ours"; conflicts=$((conflicts + 1)); } + elif [ ! -f "$ours" ] && [ -f "$base" ] && [ -f "$next" ] && ! cmp -s "$base" "$next"; then + echo "upstream changed a file this copy dropped: crates/$crate/$f" + elif [ ! -f "$base" ] && [ -f "$next" ]; then + echo "upstream added: crates/$crate/$f" + elif [ -f "$base" ] && [ ! -f "$next" ] && [ -f "$ours" ]; then + echo "upstream removed: $ours" + fi + done +done + +sed -i "s/$old/$new/g" Cargo.toml +cargo update -q -p leanvm_core +echo "leanVM ${old:0:10} -> ${new:0:10}; $conflicts conflicted file(s). Rebuild, rerun the tests and print the new AGGREGATED_VK." From 73c3644a0b91409891892f087cd2ab75fe7ca435 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 12 Nov 2026 18:36:21 +0100 Subject: [PATCH 3/8] feat(bindings): package the native test assets and test without a Nethermind checkout Nethermind.Crypto.Lean.Testing ships the vectors, the fixtures tool and the startup stub, built with the library, for consumers' native suites; its buildTransitive target copies the host RID's assets to lean-vectors/. The .NET tests check the ABI, key and limits and verify the packaged NiceTry v2 and leanSTARK vectors and witnesses from the packaged tool. Build Lean builds and stages the test assets; Test / Publish runs the whole Rust workspace, packs both packages and no longer checks out Nethermind to replace its source. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 2 + .github/workflows/build-lean.yml | 45 ++++++++-- .github/workflows/test-publish.yml | 78 ++-------------- AGENTS.md | 11 +-- README.md | 7 +- src/bindings/Directory.Build.props | 2 +- .../Nethermind.Crypto.Lean.Testing.csproj | 30 +++++++ .../Nethermind.Crypto.Lean.Testing.targets | 18 ++++ .../NativeLibraryTests.cs | 90 ++++++++++++++++++- .../Nethermind.Crypto.Lean.Tests.csproj | 3 + src/bindings/Nethermind.Crypto.Lean.slnx | 1 + .../Nethermind.Crypto.Lean.csproj | 2 +- 12 files changed, 194 insertions(+), 95 deletions(-) create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/Nethermind.Crypto.Lean.Testing.csproj create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/buildTransitive/Nethermind.Crypto.Lean.Testing.targets diff --git a/.gitattributes b/.gitattributes index 8c92d8b..85d8c67 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ * text=auto eol=lf src/bindings/Nethermind.Crypto.Lean/runtimes/**/*.so filter=lfs diff=lfs merge=lfs -text +src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/**/native/* filter=lfs diff=lfs merge=lfs -text +src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/*.bin filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/build-lean.yml b/.github/workflows/build-lean.yml index 1a0ee40..ea4f84f 100644 --- a/.github/workflows/build-lean.yml +++ b/.github/workflows/build-lean.yml @@ -48,14 +48,17 @@ jobs: # Both builds use the same target directory, so build paths can't make them differ. run: | export RUSTFLAGS="${{ matrix.rustflags }} --remap-path-prefix=$CARGO_HOME=/cargo --remap-path-prefix=$GITHUB_WORKSPACE=/src" + outputs="libnethermind_lean.so examples/fixtures examples/liblean_startup_stub.so" for build in first second; do rm -rf target - cargo build --release --locked --lib - strip target/release/libnethermind_lean.so - if [ $build = first ]; then cp target/release/libnethermind_lean.so /tmp/first.so; fi + cargo build --release --locked --lib --example fixtures --example lean_startup_stub + (cd target/release && strip $outputs) + if [ $build = first ]; then mkdir -p /tmp/first && (cd target/release && cp $outputs /tmp/first/); fi + done + for output in $outputs; do + sha256sum "/tmp/first/$(basename $output)" "target/release/$output" + cmp "/tmp/first/$(basename $output)" "target/release/$output" done - sha256sum /tmp/first.so target/release/libnethermind_lean.so - cmp /tmp/first.so target/release/libnethermind_lean.so - name: Require glibc 2.35 or older working-directory: src/ffi @@ -68,8 +71,12 @@ jobs: # Laid out as runtimes//native so that consumers download every RID into runtimes/ in one step. - name: Stage run: | - mkdir -p staging/runtimes/linux-${{ matrix.arch }}/native + mkdir -p staging/runtimes/linux-${{ matrix.arch }}/native testing/runtimes/linux-${{ matrix.arch }}/native testing/lean-vectors cp src/ffi/target/release/libnethermind_lean.so staging/runtimes/linux-${{ matrix.arch }}/native/ + cp src/ffi/target/release/examples/fixtures src/ffi/target/release/examples/liblean_startup_stub.so \ + testing/runtimes/linux-${{ matrix.arch }}/native/ + # The test vectors, with the aggregated verification key they were made under. + if [ ${{ matrix.arch }} = x64 ]; then src/ffi/target/release/examples/fixtures testing/lean-vectors; fi # A relative path: in a container job, runner.temp and github.workspace name host paths. - name: Upload artifact @@ -80,6 +87,14 @@ jobs: retention-days: ${{ env.RETENTION }} if-no-files-found: error + - name: Upload test assets + uses: actions/upload-artifact@v7 + with: + name: testing-linux-${{ matrix.arch }} + path: testing + retention-days: ${{ env.RETENTION }} + if-no-files-found: error + # The .NET smoke test on the built libraries, on hardware of each architecture, before they are committed. test: name: Test (${{ matrix.os }}) @@ -97,7 +112,7 @@ jobs: # Only the built libraries, never the committed ones (or their LFS pointers), may be tested. - name: Remove the committed libraries - run: rm -rf src/bindings/Nethermind.Crypto.Lean/runtimes + run: rm -rf src/bindings/Nethermind.Crypto.Lean/runtimes src/bindings/Nethermind.Crypto.Lean.Testing/runtimes src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors - name: Download the libraries uses: actions/download-artifact@v8 @@ -106,6 +121,13 @@ jobs: merge-multiple: true path: src/bindings/Nethermind.Crypto.Lean/runtimes + - name: Download the test assets + uses: actions/download-artifact@v8 + with: + pattern: testing-linux-* + merge-multiple: true + path: src/bindings/Nethermind.Crypto.Lean.Testing + - name: Set up .NET uses: actions/setup-dotnet@v6 @@ -154,7 +176,7 @@ jobs: # Only the built outputs, never the committed ones, may be committed. - name: Remove the committed outputs - run: rm -rf src/bindings/Nethermind.Crypto.Lean/runtimes THIRD-PARTY-NOTICES + run: rm -rf src/bindings/Nethermind.Crypto.Lean/runtimes src/bindings/Nethermind.Crypto.Lean.Testing/runtimes src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors THIRD-PARTY-NOTICES - name: Download the libraries uses: actions/download-artifact@v8 @@ -163,6 +185,13 @@ jobs: merge-multiple: true path: src/bindings/Nethermind.Crypto.Lean/runtimes + - name: Download the test assets + uses: actions/download-artifact@v8 + with: + pattern: testing-linux-* + merge-multiple: true + path: src/bindings/Nethermind.Crypto.Lean.Testing + - name: Download the notices uses: actions/download-artifact@v8 with: diff --git a/.github/workflows/test-publish.yml b/.github/workflows/test-publish.yml index a4c56ee..123ee67 100644 --- a/.github/workflows/test-publish.yml +++ b/.github/workflows/test-publish.yml @@ -100,76 +100,7 @@ jobs: # rust-toolchain.toml pins the toolchain, which rustup installs on first use. - name: Test working-directory: src/ffi - run: cargo test --release --locked -- --test-threads=1 - - # Runs the committed library, on hardware of each architecture: the release check that a cross-compiled or untested - # binary cannot pass. Nethermind's native suite supplies the tests; this repository's src/ffi supplies the fixtures and - # expectations. - native-suite: - name: Nethermind native suite (${{ matrix.rid }}) - strategy: - fail-fast: false - matrix: - include: - - rid: linux-x64 - runner: ubuntu-latest - - rid: linux-arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} - timeout-minutes: 60 - env: - # The NethermindEth/nethermind commit whose native suite runs against the library. Bump it with any ABI or - # verification key change. - NETHERMIND_REVISION: 1e5d1fa8a071b0b329a243437d00026e1d4a72df - NATIVE_TEST_PROJECT: Nethermind.Crypto.LeanFfi.Test - LEANVM_NUM_THREADS: 1 - steps: - - name: Check out repository - uses: actions/checkout@v7 - with: - lfs: true - - - name: Check out Nethermind - uses: actions/checkout@v7 - with: - repository: NethermindEth/nethermind - ref: ${{ env.NETHERMIND_REVISION }} - path: nethermind - - # The native suite builds its fixtures from tools/lean-ffi. - - name: Replace tools/lean-ffi with src/ffi - run: | - rm -rf nethermind/tools/lean-ffi - cp -r src/ffi nethermind/tools/lean-ffi - - - name: Set up .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: nethermind/global.json - - - name: Set up Rust - run: | - toolchain=$(sed -n 's/^channel = "\(.*\)"$/\1/p' src/ffi/rust-toolchain.toml) - test -n "$toolchain" - rustup toolchain install "$toolchain" --profile minimal - - # BuildLeanFfi builds the fixtures and a host-native library; the committed library then replaces the latter. - - name: Build the native tests - working-directory: nethermind - run: dotnet build "src/Nethermind/$NATIVE_TEST_PROJECT/$NATIVE_TEST_PROJECT.csproj" -c release --disable-build-servers -m:1 -p:BuildLeanFfi=true - - - name: Use the committed library - run: | - output="nethermind/src/Nethermind/artifacts/bin/$NATIVE_TEST_PROJECT/release" - test -f "$output/libnethermind_lean.so" - cp "src/bindings/Nethermind.Crypto.Lean/runtimes/${{ matrix.rid }}/native/libnethermind_lean.so" "$output/libnethermind_lean.so" - sha256sum "$output/libnethermind_lean.so" - - - name: Run startup checks and verifications - working-directory: nethermind - run: >- - dotnet test --project "src/Nethermind/$NATIVE_TEST_PROJECT/$NATIVE_TEST_PROJECT.csproj" -c release --no-build -p:BuildLeanFfi=true -- - --filter "FullyQualifiedName~NativeBackendStartupTests|FullyQualifiedName~NativeLeanProofVerifierTests|FullyQualifiedName~NativeBlockValidationTests" + run: cargo test --release --locked --workspace -- --test-threads=1 verify-package: name: Verify package assets @@ -199,7 +130,7 @@ jobs: publish: name: Publish - needs: [verify-formatting, test, rust-test, native-suite, verify-package] + needs: [verify-formatting, test, rust-test, verify-package] if: github.event_name == 'workflow_dispatch' && inputs.publish != 'No' || github.event_name == 'release' runs-on: ubuntu-latest permissions: @@ -239,7 +170,8 @@ jobs: - name: Publish working-directory: src/bindings run: | - dotnet pack ./Nethermind.Crypto.Lean -c release \ - -p:Version=${{ steps.version.outputs.version }}.${{ github.run_number }} + for package in Nethermind.Crypto.Lean Nethermind.Crypto.Lean.Testing; do + dotnet pack ./$package -c release -p:Version=${{ steps.version.outputs.version }}.${{ github.run_number }} + done dotnet nuget push ./artifacts/package/release/*.nupkg \ -k "${{ steps.login.outputs.NUGET_API_KEY }}" -s ${{ env.NUGET_SOURCE }} diff --git a/AGENTS.md b/AGENTS.md index f0b41dd..7362097 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,10 +4,10 @@ Nethermind's native EIP-8288 Lean backend (`libnethermind_lean.so`) and its NuGe ## Project structure -- [src/ffi](./src/ffi/): The Rust source of the library, a thin C ABI (`nlean_*`) over a pinned leanVM fork. -- [src/bindings](./src/bindings/): The .NET codebase. `Nethermind.Crypto.Lean` is a native-only package; `Nethermind.Crypto.Lean.Tests` smoke-tests the committed library through the `nlean_*` C API. +- [src/ffi](./src/ffi/): The Rust source of the library, a thin C ABI (`nlean_*`) over unmodified upstream leanVM pinned by git revision, and the EIP-8288 guest crates in [src/ffi/crates](./src/ffi/crates/) vendored from that revision. This repository is the only source of this code. +- [src/bindings](./src/bindings/): The .NET codebase. `Nethermind.Crypto.Lean` is a native-only package; `Nethermind.Crypto.Lean.Testing` packages the consumers' test assets; `Nethermind.Crypto.Lean.Tests` tests the committed library through the `nlean_*` C API. - [build-lean.yml](./.github/workflows/build-lean.yml): Builds [src/ffi](./src/ffi/), generates [THIRD-PARTY-NOTICES](./THIRD-PARTY-NOTICES) and opens a pull request with the resulting binaries. -- [test-publish.yml](./.github/workflows/test-publish.yml): Runs the Rust tests, and the .NET tests and Nethermind's native suite against the committed binaries, and optionally publishes on NuGet. +- [test-publish.yml](./.github/workflows/test-publish.yml): Runs the Rust tests and the .NET tests against the committed binaries, and optionally publishes both packages on NuGet. ## Coding guidelines @@ -17,8 +17,9 @@ Nethermind's native EIP-8288 Lean backend (`libnethermind_lean.so`) and its NuGe - Add tests for new code and bug fixes. - Use conventional commits; keep scoped and imperative. - Keep the native binaries under `src/bindings/Nethermind.Crypto.Lean/runtimes/` and [THIRD-PARTY-NOTICES](./THIRD-PARTY-NOTICES) built from the current [src/ffi](./src/ffi/). They are updated only by [build-lean.yml](./.github/workflows/build-lean.yml) (the binaries are Git LFS objects), so do not edit or rebuild them locally. -- Version the package as `.`: the native version is the crate `version` in [Cargo.toml](./src/ffi/Cargo.toml); [build-lean.yml](./.github/workflows/build-lean.yml) copies it into `VersionPrefix` in [Directory.Build.props](./src/bindings/Directory.Build.props), and [test-publish.yml](./.github/workflows/test-publish.yml) appends the run number. The crate version stays `0.1.0` until a versioning scheme is agreed; changing it also needs the `nethermind-lean` entry in [Cargo.lock](./src/ffi/Cargo.lock). -- Keep the smoke test's expected ABI version and aggregated verification key, and the ABI version in the package `Description`, in sync with [src/ffi](./src/ffi/). On a change of either, also bump `NETHERMIND_REVISION` in [test-publish.yml](./.github/workflows/test-publish.yml) to a Nethermind commit whose native suite expects the new library. +- Version the package as `.`: the native version is the crate `version` in [Cargo.toml](./src/ffi/Cargo.toml); [build-lean.yml](./.github/workflows/build-lean.yml) copies it into `VersionPrefix` in [Directory.Build.props](./src/bindings/Directory.Build.props), and [test-publish.yml](./.github/workflows/test-publish.yml) appends the run number. The crate's minor version is the C ABI version (`0.12.x` for ABI 12); bump the patch for a guest or key change at the same ABI. Changing it also needs the `nethermind-lean` entry in [Cargo.lock](./src/ffi/Cargo.lock). +- Keep the smoke test's expected ABI version and aggregated verification key, and the ABI version in the package `Description`, in sync with [src/ffi](./src/ffi/). Consumers pin both and check them at startup; a change of either is a new package version they adopt deliberately. - Prefer the latest versions of GitHub Actions and runners. +- Keep upstream leanVM unmodified and pinned to one revision in [src/ffi/Cargo.toml](./src/ffi/Cargo.toml). The vendored [src/ffi/crates](./src/ffi/crates/) are upstream at that revision plus EIP-8288 changes: `crates/upstream.sh diff` shows them, `crates/upstream.sh update ` moves the crates and the pin together. Keep the changes as small as the guest allows. - Keep [src/ffi/about.toml](./src/ffi/about.toml) accepting every dependency license; Build Lean generates [THIRD-PARTY-NOTICES](./THIRD-PARTY-NOTICES) from it and [src/ffi/about.hbs](./src/ffi/about.hbs). - Keep [AGENTS.md](./AGENTS.md) in sync with the ongoing development. diff --git a/README.md b/README.md index 794f294..4c7da36 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Native EIP-8288 Lean backend for [Nethermind](https://github.com/NethermindEth/nethermind), packaged for NuGet as `Nethermind.Crypto.Lean`. -The library, `libnethermind_lean.so`, is built from the Rust crate in [`src/ffi`](https://github.com/nethermindeth/lean-bindings/tree/main/src/ffi), a thin C ABI over a pinned [leanVM](https://github.com/Marchhill/leanVM) fork. +The library, `libnethermind_lean.so`, is built from the Rust crate in [`src/ffi`](https://github.com/nethermindeth/lean-bindings/tree/main/src/ffi), a thin C ABI over an unmodified, pinned [leanVM](https://github.com/leanEthereum/leanVM) and the EIP-8288 aggregation guest in `src/ffi/crates` (vendored from the same revision; see its README). This repository is the source of that code: consumers take the packages, not the source. ## Package @@ -23,12 +23,13 @@ The library, `libnethermind_lean.so`, is built from the Rust crate in [`src/ffi` - `src/ffi`: the Rust source of the library, and the `cargo-about` configuration (`about.toml`, `about.hbs`) for `THIRD-PARTY-NOTICES`. - `src/bindings/Nethermind.Crypto.Lean`: the package; `runtimes/*/native/libnethermind_lean.so` (Git LFS) are committed by **Build Lean**. -- `src/bindings/Nethermind.Crypto.Lean.Tests`: loads the committed library and checks its ABI version and aggregated verification key. +- `src/bindings/Nethermind.Crypto.Lean.Testing`: the test-asset package for consumers' native suites: the vectors, the `fixtures` signing/proving tool and an incompatible startup stub, all built with the library (Git LFS, committed by **Build Lean**). +- `src/bindings/Nethermind.Crypto.Lean.Tests`: loads the committed library and checks its ABI version, aggregated verification key and limits, and verifies the packaged vectors and a signature from the packaged tool. ## Workflows - **Build Lean** (manual): builds `src/ffi` for each RID, twice, and fails unless both builds are byte-identical; generates `THIRD-PARTY-NOTICES`; runs the smoke test on the built libraries on x64 and arm64 hardware; optionally opens a pull request with them into the branch it runs on. That pull request is opened with `GITHUB_TOKEN`, which starts no workflows, so run **Test / Publish** on its branch manually. -- **Test / Publish**: on pull requests and `main`, checks formatting, runs the Rust tests, the smoke test and Nethermind's native suite (startup checks and real verifications, with fixtures from `src/ffi`) against the committed library on x64 and arm64 hardware, and verifies a consumer gets the right library per RID. On a release or a manual run, publishes to NuGet (staging or production) through trusted publishing, only after all of these pass. +- **Test / Publish**: on pull requests and `main`, checks formatting, runs the Rust tests of the whole `src/ffi` workspace and the .NET tests against the committed library on x64 and arm64 hardware, and verifies a consumer gets the right library per RID. On a release or a manual run, publishes both packages to NuGet (staging or production) through trusted publishing, only after all of these pass. ## License diff --git a/src/bindings/Directory.Build.props b/src/bindings/Directory.Build.props index f3c8ee2..b82bd8d 100644 --- a/src/bindings/Directory.Build.props +++ b/src/bindings/Directory.Build.props @@ -15,7 +15,7 @@ Demerzel Solutions Limited $(Commit.Substring(0, 8)) - 0.1.0 + 0.12.0 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/Nethermind.Crypto.Lean.Testing.csproj b/src/bindings/Nethermind.Crypto.Lean.Testing/Nethermind.Crypto.Lean.Testing.csproj new file mode 100644 index 0000000..65bca86 --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/Nethermind.Crypto.Lean.Testing.csproj @@ -0,0 +1,30 @@ + + + + false + true + + $(NoWarn);NU5100;NU5128 + + + + Test assets for the native EIP-8288 Lean backend (ABI 12), built from the same source as Nethermind.Crypto.Lean: fixture vectors, the fixtures signing/proving tool and an incompatible startup stub. For test projects only. + Nethermind.Crypto.Lean.Testing + MIT + https://github.com/nethermindeth/lean-bindings + README.md + cryptography post-quantum eip-8288 leanvm testing + git + https://github.com/nethermindeth/lean-bindings + + + + + + + + + + + + diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/buildTransitive/Nethermind.Crypto.Lean.Testing.targets b/src/bindings/Nethermind.Crypto.Lean.Testing/buildTransitive/Nethermind.Crypto.Lean.Testing.targets new file mode 100644 index 0000000..292ef1c --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/buildTransitive/Nethermind.Crypto.Lean.Testing.targets @@ -0,0 +1,18 @@ + + + + <_LeanTestingRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier) + <_LeanTestingRid Condition="'$(_LeanTestingRid)' == ''">$(NETCoreSdkRuntimeIdentifier) + + + <_LeanTestingAsset Include="$(MSBuildThisFileDirectory)../lean-vectors/*.bin" /> + <_LeanTestingAsset Include="$(MSBuildThisFileDirectory)../runtimes/$(_LeanTestingRid)/native/*" /> + + + + + + + diff --git a/src/bindings/Nethermind.Crypto.Lean.Tests/NativeLibraryTests.cs b/src/bindings/Nethermind.Crypto.Lean.Tests/NativeLibraryTests.cs index bc314d3..baf782e 100644 --- a/src/bindings/Nethermind.Crypto.Lean.Tests/NativeLibraryTests.cs +++ b/src/bindings/Nethermind.Crypto.Lean.Tests/NativeLibraryTests.cs @@ -1,19 +1,23 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: MIT +using System.Diagnostics; using System.Runtime.InteropServices; namespace Nethermind.Crypto.Lean.Tests; /// -/// Loads the packaged library for the current RID and checks the values Nethermind's startup check pins, so a package -/// whose ABI or verification key drifts fails here before any Nethermind bump. Nethermind's native suite covers verification. +/// Loads the packaged library for the current RID and checks the values consumers pin (ABI, aggregated verification key, +/// limits), then verifies the packaged test vectors through the nlean_* C ABI, with no consumer checkout. /// public partial class NativeLibraryTests { private const string Library = "nethermind_lean"; - private const uint ExpectedAbi = 10; - private const string ExpectedAggregatedVk = "417152e8bd83e0dfb6f3e2bb8991e948f62c7f15d74b693b55979a40b239efa1"; + private const uint ExpectedAbi = 12; + private const string ExpectedAggregatedVk = "f7cf74585d02043051f0575c23c6b441b0b08ac9e273f5e989c5984b8a4ac0e5"; + // Proof bytes, dependencies, recursive children, NiceTry v2 witness bytes, aggregation input bytes, leanSTARK claims + // per list, leanSTARK instructions. + private static readonly uint[] ExpectedLimits = [8 * 1024 * 1024, 256, 16, 32 + 6176, 18 * 1024 * 1024, 16, 2048]; // A project reference leaves the library under runtimes//native; a package consumer gets it beside the binary. static NativeLibraryTests() => NativeLibrary.SetDllImportResolver(typeof(NativeLibraryTests).Assembly, static (name, assembly, searchPath) => @@ -25,6 +29,17 @@ static NativeLibraryTests() => NativeLibrary.SetDllImportResolver(typeof(NativeL [LibraryImport(Library)] private static unsafe partial int nlean_aggregated_vk(byte* output); + [LibraryImport(Library)] + private static unsafe partial int nlean_limits(uint* output, nuint length); + + [LibraryImport(Library)] + private static unsafe partial int nlean_verify_leansphincs(byte* data, byte* vk, byte* witness, nuint length); + + [LibraryImport(Library)] + private static unsafe partial int nlean_verify_leanstark(byte* data, byte* vk, byte* witness, nuint length); + + private static byte[] Vector(string name) => File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "lean-vectors", name)); + [Test] public async Task Should_match_abi_version() => await Assert.That(nlean_abi_version()).IsEqualTo(ExpectedAbi); @@ -35,6 +50,73 @@ public async Task Should_match_aggregated_verification_key() await Assert.That(result).IsEqualTo(1); await Assert.That(Convert.ToHexStringLower(vk)).IsEqualTo(ExpectedAggregatedVk); + // The packaged vectors come from the same build. + await Assert.That(Convert.ToHexStringLower(Vector("aggregated-key.bin"))).IsEqualTo(ExpectedAggregatedVk); + } + + [Test] + public async Task Should_match_limits() + { + uint[] limits = new uint[ExpectedLimits.Length]; + int result = GetLimits(limits); + + await Assert.That(result).IsEqualTo(1); + await Assert.That(limits).IsEquivalentTo(ExpectedLimits); + } + + [Test] + [Arguments("sphincs-message.bin", "sphincs-key.bin", "sphincs-signature.bin", false)] + [Arguments("stark-message.bin", "stark-key.bin", "stark-proof.bin", true)] + public async Task Should_verify_vector_and_reject_a_flipped_byte(string message, string key, string witness, bool leanStark) + { + byte[] data = Vector(message), vk = Vector(key), proof = Vector(witness); + + await Assert.That(Verify(leanStark, data, vk, proof)).IsEqualTo(1); + proof[^1] ^= 1; + await Assert.That(Verify(leanStark, data, vk, proof)).IsEqualTo(0); + } + + [Test] + [Arguments("--sign-message", false)] + [Arguments("--prove-stark", true)] + public async Task Should_verify_a_witness_from_the_packaged_fixtures_tool(string mode, bool leanStark) + { + byte[] message = Convert.FromHexString("8e1f2c3d4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0"); + string output = Path.GetTempFileName(); + try + { + using Process process = Process.Start(new ProcessStartInfo(Path.Combine(AppContext.BaseDirectory, "lean-vectors", "fixtures")) + { + ArgumentList = { mode, Convert.ToHexString(message), output }, + })!; + await process.WaitForExitAsync(); + await Assert.That(process.ExitCode).IsEqualTo(0); + + // `key ‖ witness`; a NiceTry v2 witness keeps its key prefix, a leanSTARK witness is the proof alone. + byte[] keyAndWitness = File.ReadAllBytes(output); + byte[] witness = leanStark ? keyAndWitness[32..] : keyAndWitness; + await Assert.That(Verify(leanStark, message, keyAndWitness[..32], witness)).IsEqualTo(1); + } + finally + { + File.Delete(output); + } + } + + private static unsafe int Verify(bool leanStark, byte[] data, byte[] vk, byte[] witness) + { + fixed (byte* d = data) + fixed (byte* k = vk) + fixed (byte* w = witness) + return leanStark + ? nlean_verify_leanstark(d, k, w, (nuint)witness.Length) + : nlean_verify_leansphincs(d, k, w, (nuint)witness.Length); + } + + private static unsafe int GetLimits(uint[] limits) + { + fixed (uint* output = limits) + return nlean_limits(output, (nuint)limits.Length); } private static unsafe (int Result, byte[] Vk) GetAggregatedVk() diff --git a/src/bindings/Nethermind.Crypto.Lean.Tests/Nethermind.Crypto.Lean.Tests.csproj b/src/bindings/Nethermind.Crypto.Lean.Tests/Nethermind.Crypto.Lean.Tests.csproj index 3201acc..0aa7c8b 100644 --- a/src/bindings/Nethermind.Crypto.Lean.Tests/Nethermind.Crypto.Lean.Tests.csproj +++ b/src/bindings/Nethermind.Crypto.Lean.Tests/Nethermind.Crypto.Lean.Tests.csproj @@ -14,4 +14,7 @@ + + + diff --git a/src/bindings/Nethermind.Crypto.Lean.slnx b/src/bindings/Nethermind.Crypto.Lean.slnx index 0e6f0ff..69bca8c 100644 --- a/src/bindings/Nethermind.Crypto.Lean.slnx +++ b/src/bindings/Nethermind.Crypto.Lean.slnx @@ -5,4 +5,5 @@ + diff --git a/src/bindings/Nethermind.Crypto.Lean/Nethermind.Crypto.Lean.csproj b/src/bindings/Nethermind.Crypto.Lean/Nethermind.Crypto.Lean.csproj index 6e6e687..715a0f9 100644 --- a/src/bindings/Nethermind.Crypto.Lean/Nethermind.Crypto.Lean.csproj +++ b/src/bindings/Nethermind.Crypto.Lean/Nethermind.Crypto.Lean.csproj @@ -8,7 +8,7 @@ - Native EIP-8288 Lean backend (libnethermind_lean, ABI 10) for linux-x64 (x86-64-v3 + PCLMULQDQ) and linux-arm64 (AES/PMULL). + Native EIP-8288 Lean backend (libnethermind_lean, ABI 12) for linux-x64 (x86-64-v3 + PCLMULQDQ) and linux-arm64 (AES/PMULL). Nethermind.Crypto.Lean MIT https://github.com/nethermindeth/lean-bindings From e2b5413562d5bd2d202ac9b503503e9f518e4f0f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Nov 2026 18:41:18 +0100 Subject: [PATCH 4/8] chore(deps): update Lean binaries to 0.12.0 (73c3644a0b) (#4) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../lean-vectors/aggregated-key.bin | 3 +++ .../lean-vectors/sphincs-key.bin | 3 +++ .../lean-vectors/sphincs-message.bin | 3 +++ .../lean-vectors/sphincs-multi-keys.bin | 3 +++ .../lean-vectors/sphincs-multi.bin | 3 +++ .../lean-vectors/sphincs-signature.bin | 3 +++ .../Nethermind.Crypto.Lean.Testing/lean-vectors/stark-key.bin | 3 +++ .../lean-vectors/stark-message.bin | 3 +++ .../lean-vectors/stark-proof.bin | 3 +++ .../runtimes/linux-arm64/native/fixtures | 3 +++ .../runtimes/linux-arm64/native/liblean_startup_stub.so | 3 +++ .../runtimes/linux-x64/native/fixtures | 3 +++ .../runtimes/linux-x64/native/liblean_startup_stub.so | 3 +++ .../runtimes/linux-arm64/native/libnethermind_lean.so | 4 ++-- .../runtimes/linux-x64/native/libnethermind_lean.so | 4 ++-- 15 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/aggregated-key.bin create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-key.bin create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-message.bin create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi-keys.bin create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi.bin create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-signature.bin create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-key.bin create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-message.bin create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-proof.bin create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/fixtures create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/liblean_startup_stub.so create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/fixtures create mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/liblean_startup_stub.so diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/aggregated-key.bin b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/aggregated-key.bin new file mode 100644 index 0000000..b7a435a --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/aggregated-key.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acc42e87bec594b5ebfa26d34fbe9e45f2990c96fc08478cb85f0e1195166393 +size 32 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-key.bin b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-key.bin new file mode 100644 index 0000000..4b9b6a9 --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-key.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71896163a93c64c86e436c261c43f17982e0c9265eac2c53ffc41ef74c8d6f58 +size 32 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-message.bin b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-message.bin new file mode 100644 index 0000000..564565e --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-message.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4bb06f8e4e3a7715d201d573d0aa423762e55dabd61a2c02278fa56cc6d294e0 +size 32 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi-keys.bin b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi-keys.bin new file mode 100644 index 0000000..001fec2 --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi-keys.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bcfa4676b306712dc800b13e71fddf3c93239d38ea4488f0fb852a808f6d785 +size 99840 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi.bin b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi.bin new file mode 100644 index 0000000..711b9c5 --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93d88d204525174ec4abaefe84a2a92a8f89e9ea1795a13010b51cd681aab460 +size 99840 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-signature.bin b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-signature.bin new file mode 100644 index 0000000..49f1121 --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-signature.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcf1b85cb2e45119b2da45da9d0c1512c9ed654f7c71d954f3a82883aecf8346 +size 6208 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-key.bin b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-key.bin new file mode 100644 index 0000000..de2af1c --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-key.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25d43f3199433e10b6c6968607ff6c4fe2d38ef5c1f0be236f837a5e61f70164 +size 32 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-message.bin b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-message.bin new file mode 100644 index 0000000..3290bb5 --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-message.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c0cc17a04942cc4f8e0fe0b302606d3108860c126428ba2ceeb5f9ed41c2b05 +size 32 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-proof.bin b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-proof.bin new file mode 100644 index 0000000..bb5853c --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-proof.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3421d51872c22ae06e215d461a4f521c5d82f12377a56493e0dde5cb81a08ff1 +size 183621 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/fixtures b/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/fixtures new file mode 100644 index 0000000..8df9dc1 --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/fixtures @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68a55226d3ea1b17b67b8be545099739c7548392f77cca92e95d1ad93bdf9086 +size 2563832 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/liblean_startup_stub.so b/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/liblean_startup_stub.so new file mode 100644 index 0000000..a077318 --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/liblean_startup_stub.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4147d99bf688c3918c280dc3f0efb7d4923ad61e00b5be98f310dcc5ceea58dc +size 1315936 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/fixtures b/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/fixtures new file mode 100644 index 0000000..6cc981a --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/fixtures @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:220347dd51a844d3b169a2186fbd61c7baca051959dedcee22fd9a694a23f18c +size 3076744 diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/liblean_startup_stub.so b/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/liblean_startup_stub.so new file mode 100644 index 0000000..ae40d01 --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/liblean_startup_stub.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d89aaa799fb583a9e70dfb757bd9284bfce3faf3b9450b2a5faf952475dc8dd2 +size 1462264 diff --git a/src/bindings/Nethermind.Crypto.Lean/runtimes/linux-arm64/native/libnethermind_lean.so b/src/bindings/Nethermind.Crypto.Lean/runtimes/linux-arm64/native/libnethermind_lean.so index 9f00254..d391360 100644 --- a/src/bindings/Nethermind.Crypto.Lean/runtimes/linux-arm64/native/libnethermind_lean.so +++ b/src/bindings/Nethermind.Crypto.Lean/runtimes/linux-arm64/native/libnethermind_lean.so @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d46f93958f6300c52065a9a16a189c4892d30399c5ddb18f34e75f6ff9581965 -size 2693928 +oid sha256:6173336e9e568f6b9fe8c631e8c7fd42382553d2f05ac9e7384f053e800ef665 +size 3088080 diff --git a/src/bindings/Nethermind.Crypto.Lean/runtimes/linux-x64/native/libnethermind_lean.so b/src/bindings/Nethermind.Crypto.Lean/runtimes/linux-x64/native/libnethermind_lean.so index 22bef2f..7853c4a 100644 --- a/src/bindings/Nethermind.Crypto.Lean/runtimes/linux-x64/native/libnethermind_lean.so +++ b/src/bindings/Nethermind.Crypto.Lean/runtimes/linux-x64/native/libnethermind_lean.so @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0aab601787d7fdb1c162abcc023bb56ba09517d0ce6d4b0cbe3e3d1c40fc3dbb -size 3270672 +oid sha256:62afe3b5b946804a3547b34afaf25d03699774ac53f8974cc3c643d91e79d99f +size 3859304 From d20edce7154ab326a9465ca2b58c70d8519ffb51 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 12 Nov 2026 18:49:13 +0100 Subject: [PATCH 5/8] fix(ffi): give the aggregation bench fresh signer keys CodeQL read the bench's fixed key seeds as hard-coded salts. The bench measures a workload's shape, so random keys lose nothing. Co-Authored-By: Claude Opus 5.5 --- src/ffi/crates/rec_aggregation/examples/agg_bench.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ffi/crates/rec_aggregation/examples/agg_bench.rs b/src/ffi/crates/rec_aggregation/examples/agg_bench.rs index b95cdc1..ccbb95b 100644 --- a/src/ffi/crates/rec_aggregation/examples/agg_bench.rs +++ b/src/ffi/crates/rec_aggregation/examples/agg_bench.rs @@ -20,9 +20,8 @@ type RawSphincs = ( fn signers(n: usize, salt: u8) -> Vec { (0..n) .map(|i| { - let mut seed = [salt; 32]; - seed[..8].copy_from_slice(&(i as u64).to_le_bytes()); - let (sk, pk) = lean_sphincs::key_gen_from_seed(seed); + // Fresh keys per run: the workload's shape, not its keys, is what the bench measures. + let (sk, pk) = lean_sphincs::key_gen_from_seed(rand::random()); let mut message = [7; 32]; message[0] = salt; message[31] = i as u8; From 63ea51edda3cbf69a0619655a2172d9ab9d39dd3 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 12 Nov 2026 18:56:09 +0100 Subject: [PATCH 6/8] fix(ffi): draw the aggregation bench's messages at random too CodeQL followed the per-run constant into the signed message. Co-Authored-By: Claude Opus 5.5 --- .../crates/rec_aggregation/examples/agg_bench.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/ffi/crates/rec_aggregation/examples/agg_bench.rs b/src/ffi/crates/rec_aggregation/examples/agg_bench.rs index ccbb95b..45d0e5b 100644 --- a/src/ffi/crates/rec_aggregation/examples/agg_bench.rs +++ b/src/ffi/crates/rec_aggregation/examples/agg_bench.rs @@ -17,14 +17,12 @@ type RawSphincs = ( lean_sphincs::LeanSphincsSignature, ); -fn signers(n: usize, salt: u8) -> Vec { +fn signers(n: usize) -> Vec { + // Fresh keys and messages per run: the workload's shape, not its values, is what the bench measures. (0..n) - .map(|i| { - // Fresh keys per run: the workload's shape, not its keys, is what the bench measures. + .map(|_| { let (sk, pk) = lean_sphincs::key_gen_from_seed(rand::random()); - let mut message = [7; 32]; - message[0] = salt; - message[31] = i as u8; + let message = rand::random(); (pk, message, lean_sphincs::sign(&sk, &message).expect("signs")) }) .collect() @@ -114,8 +112,8 @@ fn main() { match mode.as_str() { "sigs" => { let n: usize = std::env::args().nth(2).map_or(40, |a| a.parse().expect("n")); - let left = report(&format!("leaf {n} sigs"), leaf(vec![], signers(n, 1))); - let right = report(&format!("leaf {n} sigs (2nd)"), leaf(vec![], signers(n, 2))); + let left = report(&format!("leaf {n} sigs"), leaf(vec![], signers(n))); + let right = report(&format!("leaf {n} sigs (2nd)"), leaf(vec![], signers(n))); report("merge 2 children", || { aggregate_with_stats(&[left, right], vec![], vec![], &[], None, RATE).expect("proves") }); @@ -132,7 +130,7 @@ fn main() { report("leaf 16 small generic", leaf(deps.clone(), vec![])); report( "leaf 40 sigs + 4 small generic", - leaf(deps[..4].to_vec(), signers(40, 3)), + leaf(deps[..4].to_vec(), signers(40)), ); } _ => panic!("mode: sigs [n] | generic"), From 995a3dd0e2e10f38de3a4a62181908b3d69b0782 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 12 Nov 2026 20:26:52 +0100 Subject: [PATCH 7/8] feat(bindings): ship the test assets as an opt-in part of Nethermind.Crypto.Lean One package instead of two: the vectors, fixtures tool and startup stub move to testing/ in Nethermind.Crypto.Lean, outside runtimes/, and its buildTransitive target copies them to lean-vectors/ only with NethermindCryptoLeanTestAssets=true. A consumer publish without it gets the library alone, which Verify package assets now checks. The assets are the Build Lean outputs of 73c3644, moved unchanged. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 4 +-- .github/workflows/build-lean.yml | 8 ++--- .github/workflows/test-publish.yml | 7 +++-- AGENTS.md | 4 +-- README.md | 4 +-- .../Nethermind.Crypto.Lean.Testing.csproj | 30 ------------------- .../Nethermind.Crypto.Lean.Testing.targets | 18 ----------- .../Nethermind.Crypto.Lean.Tests.csproj | 7 +++-- src/bindings/Nethermind.Crypto.Lean.slnx | 1 - .../Nethermind.Crypto.Lean.csproj | 9 +++++- .../Nethermind.Crypto.Lean.targets | 21 +++++++++++++ .../testing}/lean-vectors/aggregated-key.bin | 0 .../testing}/lean-vectors/sphincs-key.bin | 0 .../testing}/lean-vectors/sphincs-message.bin | 0 .../lean-vectors/sphincs-multi-keys.bin | 0 .../testing}/lean-vectors/sphincs-multi.bin | 0 .../lean-vectors/sphincs-signature.bin | 0 .../testing}/lean-vectors/stark-key.bin | 0 .../testing}/lean-vectors/stark-message.bin | 0 .../testing}/lean-vectors/stark-proof.bin | 0 .../runtimes/linux-arm64/native/fixtures | 0 .../native/liblean_startup_stub.so | 0 .../runtimes/linux-x64/native/fixtures | 0 .../linux-x64/native/liblean_startup_stub.so | 0 src/ffi/README.md | 4 +-- 25 files changed, 50 insertions(+), 67 deletions(-) delete mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/Nethermind.Crypto.Lean.Testing.csproj delete mode 100644 src/bindings/Nethermind.Crypto.Lean.Testing/buildTransitive/Nethermind.Crypto.Lean.Testing.targets create mode 100644 src/bindings/Nethermind.Crypto.Lean/buildTransitive/Nethermind.Crypto.Lean.targets rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/lean-vectors/aggregated-key.bin (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/lean-vectors/sphincs-key.bin (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/lean-vectors/sphincs-message.bin (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/lean-vectors/sphincs-multi-keys.bin (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/lean-vectors/sphincs-multi.bin (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/lean-vectors/sphincs-signature.bin (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/lean-vectors/stark-key.bin (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/lean-vectors/stark-message.bin (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/lean-vectors/stark-proof.bin (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/runtimes/linux-arm64/native/fixtures (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/runtimes/linux-arm64/native/liblean_startup_stub.so (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/runtimes/linux-x64/native/fixtures (100%) rename src/bindings/{Nethermind.Crypto.Lean.Testing => Nethermind.Crypto.Lean/testing}/runtimes/linux-x64/native/liblean_startup_stub.so (100%) diff --git a/.gitattributes b/.gitattributes index 85d8c67..493541f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ * text=auto eol=lf src/bindings/Nethermind.Crypto.Lean/runtimes/**/*.so filter=lfs diff=lfs merge=lfs -text -src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/**/native/* filter=lfs diff=lfs merge=lfs -text -src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/*.bin filter=lfs diff=lfs merge=lfs -text +src/bindings/Nethermind.Crypto.Lean/testing/runtimes/**/native/* filter=lfs diff=lfs merge=lfs -text +src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/*.bin filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/build-lean.yml b/.github/workflows/build-lean.yml index ea4f84f..0040a94 100644 --- a/.github/workflows/build-lean.yml +++ b/.github/workflows/build-lean.yml @@ -112,7 +112,7 @@ jobs: # Only the built libraries, never the committed ones (or their LFS pointers), may be tested. - name: Remove the committed libraries - run: rm -rf src/bindings/Nethermind.Crypto.Lean/runtimes src/bindings/Nethermind.Crypto.Lean.Testing/runtimes src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors + run: rm -rf src/bindings/Nethermind.Crypto.Lean/runtimes src/bindings/Nethermind.Crypto.Lean/testing - name: Download the libraries uses: actions/download-artifact@v8 @@ -126,7 +126,7 @@ jobs: with: pattern: testing-linux-* merge-multiple: true - path: src/bindings/Nethermind.Crypto.Lean.Testing + path: src/bindings/Nethermind.Crypto.Lean/testing - name: Set up .NET uses: actions/setup-dotnet@v6 @@ -176,7 +176,7 @@ jobs: # Only the built outputs, never the committed ones, may be committed. - name: Remove the committed outputs - run: rm -rf src/bindings/Nethermind.Crypto.Lean/runtimes src/bindings/Nethermind.Crypto.Lean.Testing/runtimes src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors THIRD-PARTY-NOTICES + run: rm -rf src/bindings/Nethermind.Crypto.Lean/runtimes src/bindings/Nethermind.Crypto.Lean/testing THIRD-PARTY-NOTICES - name: Download the libraries uses: actions/download-artifact@v8 @@ -190,7 +190,7 @@ jobs: with: pattern: testing-linux-* merge-multiple: true - path: src/bindings/Nethermind.Crypto.Lean.Testing + path: src/bindings/Nethermind.Crypto.Lean/testing - name: Download the notices uses: actions/download-artifact@v8 diff --git a/.github/workflows/test-publish.yml b/.github/workflows/test-publish.yml index 123ee67..20f70b7 100644 --- a/.github/workflows/test-publish.yml +++ b/.github/workflows/test-publish.yml @@ -126,6 +126,8 @@ jobs: dotnet publish "$consumer" -c release -r "${rid%%:*}" --self-contained false -o "$out" readelf -h "$out/libnethermind_lean.so" | grep -q "Machine:.*${rid##*:}" test ! -e "$out/runtimes" + # The test assets reach only a consumer that opts in. + test ! -e "$out/lean-vectors" && test ! -e "$out/testing" && test ! -e "$out/fixtures" done publish: @@ -170,8 +172,7 @@ jobs: - name: Publish working-directory: src/bindings run: | - for package in Nethermind.Crypto.Lean Nethermind.Crypto.Lean.Testing; do - dotnet pack ./$package -c release -p:Version=${{ steps.version.outputs.version }}.${{ github.run_number }} - done + dotnet pack ./Nethermind.Crypto.Lean -c release \ + -p:Version=${{ steps.version.outputs.version }}.${{ github.run_number }} dotnet nuget push ./artifacts/package/release/*.nupkg \ -k "${{ steps.login.outputs.NUGET_API_KEY }}" -s ${{ env.NUGET_SOURCE }} diff --git a/AGENTS.md b/AGENTS.md index 7362097..0c55fb6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,9 +5,9 @@ Nethermind's native EIP-8288 Lean backend (`libnethermind_lean.so`) and its NuGe ## Project structure - [src/ffi](./src/ffi/): The Rust source of the library, a thin C ABI (`nlean_*`) over unmodified upstream leanVM pinned by git revision, and the EIP-8288 guest crates in [src/ffi/crates](./src/ffi/crates/) vendored from that revision. This repository is the only source of this code. -- [src/bindings](./src/bindings/): The .NET codebase. `Nethermind.Crypto.Lean` is a native-only package; `Nethermind.Crypto.Lean.Testing` packages the consumers' test assets; `Nethermind.Crypto.Lean.Tests` tests the committed library through the `nlean_*` C API. +- [src/bindings](./src/bindings/): The .NET codebase. `Nethermind.Crypto.Lean` is a native-only package; `Nethermind.Crypto.Lean` also carries, under `testing/`, the consumers' test assets, which `buildTransitive` copies only with `NethermindCryptoLeanTestAssets=true`; `Nethermind.Crypto.Lean.Tests` tests the committed library through the `nlean_*` C API. - [build-lean.yml](./.github/workflows/build-lean.yml): Builds [src/ffi](./src/ffi/), generates [THIRD-PARTY-NOTICES](./THIRD-PARTY-NOTICES) and opens a pull request with the resulting binaries. -- [test-publish.yml](./.github/workflows/test-publish.yml): Runs the Rust tests and the .NET tests against the committed binaries, and optionally publishes both packages on NuGet. +- [test-publish.yml](./.github/workflows/test-publish.yml): Runs the Rust tests and the .NET tests against the committed binaries, and optionally publishes the package on NuGet. ## Coding guidelines diff --git a/README.md b/README.md index 4c7da36..b38a0c9 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ The library, `libnethermind_lean.so`, is built from the Rust crate in [`src/ffi` - `src/ffi`: the Rust source of the library, and the `cargo-about` configuration (`about.toml`, `about.hbs`) for `THIRD-PARTY-NOTICES`. - `src/bindings/Nethermind.Crypto.Lean`: the package; `runtimes/*/native/libnethermind_lean.so` (Git LFS) are committed by **Build Lean**. -- `src/bindings/Nethermind.Crypto.Lean.Testing`: the test-asset package for consumers' native suites: the vectors, the `fixtures` signing/proving tool and an incompatible startup stub, all built with the library (Git LFS, committed by **Build Lean**). +- `src/bindings/Nethermind.Crypto.Lean/testing`: test assets for consumers' native suites: the vectors, the `fixtures` signing/proving tool and an incompatible startup stub, all built with the library (Git LFS, committed by **Build Lean**). They ship in the package outside `runtimes/`; a test project gets them in `lean-vectors/` only with `true`, so production publishes never contain them. - `src/bindings/Nethermind.Crypto.Lean.Tests`: loads the committed library and checks its ABI version, aggregated verification key and limits, and verifies the packaged vectors and a signature from the packaged tool. ## Workflows - **Build Lean** (manual): builds `src/ffi` for each RID, twice, and fails unless both builds are byte-identical; generates `THIRD-PARTY-NOTICES`; runs the smoke test on the built libraries on x64 and arm64 hardware; optionally opens a pull request with them into the branch it runs on. That pull request is opened with `GITHUB_TOKEN`, which starts no workflows, so run **Test / Publish** on its branch manually. -- **Test / Publish**: on pull requests and `main`, checks formatting, runs the Rust tests of the whole `src/ffi` workspace and the .NET tests against the committed library on x64 and arm64 hardware, and verifies a consumer gets the right library per RID. On a release or a manual run, publishes both packages to NuGet (staging or production) through trusted publishing, only after all of these pass. +- **Test / Publish**: on pull requests and `main`, checks formatting, runs the Rust tests of the whole `src/ffi` workspace and the .NET tests against the committed library on x64 and arm64 hardware, and verifies a consumer gets the right library per RID. On a release or a manual run, publishes to NuGet (staging or production) through trusted publishing, only after all of these pass. ## License diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/Nethermind.Crypto.Lean.Testing.csproj b/src/bindings/Nethermind.Crypto.Lean.Testing/Nethermind.Crypto.Lean.Testing.csproj deleted file mode 100644 index 65bca86..0000000 --- a/src/bindings/Nethermind.Crypto.Lean.Testing/Nethermind.Crypto.Lean.Testing.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - false - true - - $(NoWarn);NU5100;NU5128 - - - - Test assets for the native EIP-8288 Lean backend (ABI 12), built from the same source as Nethermind.Crypto.Lean: fixture vectors, the fixtures signing/proving tool and an incompatible startup stub. For test projects only. - Nethermind.Crypto.Lean.Testing - MIT - https://github.com/nethermindeth/lean-bindings - README.md - cryptography post-quantum eip-8288 leanvm testing - git - https://github.com/nethermindeth/lean-bindings - - - - - - - - - - - - diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/buildTransitive/Nethermind.Crypto.Lean.Testing.targets b/src/bindings/Nethermind.Crypto.Lean.Testing/buildTransitive/Nethermind.Crypto.Lean.Testing.targets deleted file mode 100644 index 292ef1c..0000000 --- a/src/bindings/Nethermind.Crypto.Lean.Testing/buildTransitive/Nethermind.Crypto.Lean.Testing.targets +++ /dev/null @@ -1,18 +0,0 @@ - - - - <_LeanTestingRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier) - <_LeanTestingRid Condition="'$(_LeanTestingRid)' == ''">$(NETCoreSdkRuntimeIdentifier) - - - <_LeanTestingAsset Include="$(MSBuildThisFileDirectory)../lean-vectors/*.bin" /> - <_LeanTestingAsset Include="$(MSBuildThisFileDirectory)../runtimes/$(_LeanTestingRid)/native/*" /> - - - - - - - diff --git a/src/bindings/Nethermind.Crypto.Lean.Tests/Nethermind.Crypto.Lean.Tests.csproj b/src/bindings/Nethermind.Crypto.Lean.Tests/Nethermind.Crypto.Lean.Tests.csproj index 0aa7c8b..1c49d6b 100644 --- a/src/bindings/Nethermind.Crypto.Lean.Tests/Nethermind.Crypto.Lean.Tests.csproj +++ b/src/bindings/Nethermind.Crypto.Lean.Tests/Nethermind.Crypto.Lean.Tests.csproj @@ -14,7 +14,10 @@ - - + + + true + + diff --git a/src/bindings/Nethermind.Crypto.Lean.slnx b/src/bindings/Nethermind.Crypto.Lean.slnx index 69bca8c..0e6f0ff 100644 --- a/src/bindings/Nethermind.Crypto.Lean.slnx +++ b/src/bindings/Nethermind.Crypto.Lean.slnx @@ -5,5 +5,4 @@ - diff --git a/src/bindings/Nethermind.Crypto.Lean/Nethermind.Crypto.Lean.csproj b/src/bindings/Nethermind.Crypto.Lean/Nethermind.Crypto.Lean.csproj index 715a0f9..9d2a53e 100644 --- a/src/bindings/Nethermind.Crypto.Lean/Nethermind.Crypto.Lean.csproj +++ b/src/bindings/Nethermind.Crypto.Lean/Nethermind.Crypto.Lean.csproj @@ -8,7 +8,7 @@ - Native EIP-8288 Lean backend (libnethermind_lean, ABI 12) for linux-x64 (x86-64-v3 + PCLMULQDQ) and linux-arm64 (AES/PMULL). + Native EIP-8288 Lean backend (libnethermind_lean, ABI 12) for linux-x64 (x86-64-v3 + PCLMULQDQ) and linux-arm64 (AES/PMULL), with opt-in test assets (NethermindCryptoLeanTestAssets=true). Nethermind.Crypto.Lean MIT https://github.com/nethermindeth/lean-bindings @@ -27,6 +27,13 @@ + + + + + + + diff --git a/src/bindings/Nethermind.Crypto.Lean/buildTransitive/Nethermind.Crypto.Lean.targets b/src/bindings/Nethermind.Crypto.Lean/buildTransitive/Nethermind.Crypto.Lean.targets new file mode 100644 index 0000000..d8ac7e8 --- /dev/null +++ b/src/bindings/Nethermind.Crypto.Lean/buildTransitive/Nethermind.Crypto.Lean.targets @@ -0,0 +1,21 @@ + + + + + <_LeanTestAssetsRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier) + <_LeanTestAssetsRid Condition="'$(_LeanTestAssetsRid)' == ''">$(NETCoreSdkRuntimeIdentifier) + <_LeanTestAssets>$(MSBuildThisFileDirectory)../testing + + + <_LeanTestAsset Include="$(_LeanTestAssets)/lean-vectors/*.bin" /> + <_LeanTestAsset Include="$(_LeanTestAssets)/runtimes/$(_LeanTestAssetsRid)/native/*" /> + + + + + + + diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/aggregated-key.bin b/src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/aggregated-key.bin similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/aggregated-key.bin rename to src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/aggregated-key.bin diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-key.bin b/src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/sphincs-key.bin similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-key.bin rename to src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/sphincs-key.bin diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-message.bin b/src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/sphincs-message.bin similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-message.bin rename to src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/sphincs-message.bin diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi-keys.bin b/src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/sphincs-multi-keys.bin similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi-keys.bin rename to src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/sphincs-multi-keys.bin diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi.bin b/src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/sphincs-multi.bin similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-multi.bin rename to src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/sphincs-multi.bin diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-signature.bin b/src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/sphincs-signature.bin similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/sphincs-signature.bin rename to src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/sphincs-signature.bin diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-key.bin b/src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/stark-key.bin similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-key.bin rename to src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/stark-key.bin diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-message.bin b/src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/stark-message.bin similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-message.bin rename to src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/stark-message.bin diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-proof.bin b/src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/stark-proof.bin similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/lean-vectors/stark-proof.bin rename to src/bindings/Nethermind.Crypto.Lean/testing/lean-vectors/stark-proof.bin diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/fixtures b/src/bindings/Nethermind.Crypto.Lean/testing/runtimes/linux-arm64/native/fixtures similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/fixtures rename to src/bindings/Nethermind.Crypto.Lean/testing/runtimes/linux-arm64/native/fixtures diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/liblean_startup_stub.so b/src/bindings/Nethermind.Crypto.Lean/testing/runtimes/linux-arm64/native/liblean_startup_stub.so similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-arm64/native/liblean_startup_stub.so rename to src/bindings/Nethermind.Crypto.Lean/testing/runtimes/linux-arm64/native/liblean_startup_stub.so diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/fixtures b/src/bindings/Nethermind.Crypto.Lean/testing/runtimes/linux-x64/native/fixtures similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/fixtures rename to src/bindings/Nethermind.Crypto.Lean/testing/runtimes/linux-x64/native/fixtures diff --git a/src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/liblean_startup_stub.so b/src/bindings/Nethermind.Crypto.Lean/testing/runtimes/linux-x64/native/liblean_startup_stub.so similarity index 100% rename from src/bindings/Nethermind.Crypto.Lean.Testing/runtimes/linux-x64/native/liblean_startup_stub.so rename to src/bindings/Nethermind.Crypto.Lean/testing/runtimes/linux-x64/native/liblean_startup_stub.so diff --git a/src/ffi/README.md b/src/ffi/README.md index b644cd6..4e09cbd 100644 --- a/src/ffi/README.md +++ b/src/ffi/README.md @@ -31,8 +31,8 @@ fails if the backend is unavailable. Verification fails closed; proving requires `cargo run --release --locked --example fixtures -- `, run from `src/ffi`, produces genuine test signatures and proofs and prints the pinned recursive verification key; `fixtures --sign-message ` signs -one message. **Build Lean** packages the vectors, this tool and the `lean_startup_stub` example as -`Nethermind.Crypto.Lean.Testing` for consumers' native suites. +one message, and `fixtures --prove-stark ` proves a leanSTARK dependency on it. **Build Lean** packages the vectors, +this tool and the `lean_startup_stub` example as opt-in test assets of `Nethermind.Crypto.Lean`. The C ABI is version 12, with Cdecl calls, 32-byte hashes/keys and `size_t` buffer lengths. `nlean_limits` writes seven u32 values (proof bytes, dependencies, recursive children, From 93c979872bd3224d7a55aa76da70772d87cf74e4 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 12 Nov 2026 21:09:08 +0100 Subject: [PATCH 8/8] fix(bindings): find the test assets under the SDK's portable RID A distro-built SDK reports a distro RID such as ubuntu.24.04-x64, under which the package has no test assets; the portable RID comes first now. Co-Authored-By: Claude Opus 5.5 --- .../buildTransitive/Nethermind.Crypto.Lean.targets | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bindings/Nethermind.Crypto.Lean/buildTransitive/Nethermind.Crypto.Lean.targets b/src/bindings/Nethermind.Crypto.Lean/buildTransitive/Nethermind.Crypto.Lean.targets index d8ac7e8..1f4ec5b 100644 --- a/src/bindings/Nethermind.Crypto.Lean/buildTransitive/Nethermind.Crypto.Lean.targets +++ b/src/bindings/Nethermind.Crypto.Lean/buildTransitive/Nethermind.Crypto.Lean.targets @@ -5,6 +5,8 @@ <_LeanTestAssetsRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier) + + <_LeanTestAssetsRid Condition="'$(_LeanTestAssetsRid)' == ''">$(NETCoreSdkPortableRuntimeIdentifier) <_LeanTestAssetsRid Condition="'$(_LeanTestAssetsRid)' == ''">$(NETCoreSdkRuntimeIdentifier) <_LeanTestAssets>$(MSBuildThisFileDirectory)../testing