feat(evm): inspect lossless Solidity terminal CBOR metadata (#2790)
feat(evm): retain lossless terminal CBOR metadata Refs morluto/rea#2388 Co-authored-by: Matheus Breguêz <matbrgz@gmail.com> Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
a3daf8a07b
commit
48a683ac8c
|
|
@ -43,3 +43,5 @@ jobs:
|
|||
echo "REA_VERIFY_SOLC_MODULE=${evm_fixture_root}/node_modules/solc/index.js" >> "${GITHUB_ENV}"
|
||||
- name: Verify source-owned compiled bytecode through public CLI and MCP
|
||||
run: npm run verify:evm:interface
|
||||
- name: Verify content-hash-free and CBOR-free owned compiler artifacts
|
||||
run: npm run verify:solidity:metadata
|
||||
|
|
|
|||
|
|
@ -39,6 +39,18 @@ license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md)
|
|||
- Upstream also returns terminal metadata during selector recovery. Its reported
|
||||
values remain in `raw_result`; integer precision and non-text-key coverage are
|
||||
unknown in the JS binding. Full bytecode retains the original encoded metadata.
|
||||
- `bytecode.metadata` independently interprets a plausible terminal CBOR root
|
||||
map from the selected bytes. Integers use decimal strings; byte strings use hex,
|
||||
and scalar keys retain their types. The recorded offset/length addresses the
|
||||
original bytecode, so summary delivery does not copy an opaque CBOR payload.
|
||||
Nested containers have partial scalar coverage; unsupported or malformed
|
||||
candidates report `not-decoded` with a diagnostic. Decoding is bounded to 64
|
||||
container levels and the terminal payload's encoded size.
|
||||
- An unavailable trailer does not prove metadata absence or Solidity origin.
|
||||
Compiler-version bytes/text are metadata declarations, not verified compiler,
|
||||
source-file or deployed-contract identity. Historical omission parses as a null
|
||||
unknown facet without changing the original Evidence record. Metadata/source
|
||||
URLs are not fetched.
|
||||
- Selectors, argument strings and state mutability are **inferences**. Function
|
||||
names/signatures are not looked up. Optimized code may produce different
|
||||
mutability candidates; recovered types are not a verified source ABI.
|
||||
|
|
@ -125,3 +137,15 @@ The real verification lane traces representative raw/hex CLI workflows and check
|
|||
that only declared Node/prlimit launchers and process ownership inspection run.
|
||||
Socket traces permit local stdio socket pairs and reject attempted network requests.
|
||||
These observations verify those fixtures; they are not a general network sandbox.
|
||||
|
||||
For the independent metadata parser, `npm run verify:solidity:metadata` uses an
|
||||
existing caller-selected solc 0.8.30 module:
|
||||
|
||||
```sh
|
||||
REA_VERIFY_SOLC_MODULE=/absolute/path/to/solc/index.js npm run verify:solidity:metadata
|
||||
```
|
||||
|
||||
It compiles the owned fixture with unoptimized, viaIR, content-hash-free and
|
||||
CBOR-free settings, verifies the observed trailer facts and reports local decode
|
||||
timings. No contract is executed. This lane does not establish the complete
|
||||
Linux EVM provider's CLI/MCP, resource-limit or cleanup behavior.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
# Implementation plan
|
||||
|
||||
1. Use cborg 6.1.3 (Apache-2.0) as a pinned format decoder, with a bounded
|
||||
tokenizer at its owning adapter boundary. Decode maps as Map, preserve integer
|
||||
precision and strictly validate emitted text bytes before conversion.
|
||||
2. Add the portable metadata-result schema in src/domain/evm/. Use exact byte
|
||||
ranges into the already retained bytecode rather than copying an opaque CBOR
|
||||
hex payload into every selected summary.
|
||||
3. Add src/evm/SolidityMetadata.ts to interpret the terminal length/root-map
|
||||
candidate. Return unavailable, decoded, partial or not-decoded facts; keep
|
||||
unsupported nested values explicit instead of recursively expanding them.
|
||||
4. Compute the facts in EvmoleInterfaceWorker from its selected bytes. The host
|
||||
provider validates ranges/identity and projects the additive metadata field.
|
||||
Preserve raw engine output without rewriting its unknown-precision metadata.
|
||||
5. Ensure the canonical EVM bytecode shape carries the optional field into complete
|
||||
and summary Evidence. Include decoding profile, coverage and limitations.
|
||||
6. Add producer, worker, historical contract and public MCP boundary coverage.
|
||||
Compile an owned Solidity fixture with a pinned caller-supplied solc module,
|
||||
and verify its CBOR compiler version. Run relevant static/architecture checks.
|
||||
7. Extend the existing real Linux CLI/MCP verifier with independent metadata
|
||||
assertions. Run the compiler-setting matrix in its CI lane and classify its
|
||||
verifier as EVM-owned, with a production CI-planner regression.
|
||||
|
||||
No new execution provider or target authority is introduced: a format parser is
|
||||
added inside the existing EVM worker. Linux x64 provider admission, target-free
|
||||
semantics, cleanup, limits and complete original Evidence remain unchanged.
|
||||
No metadata JSON or source URL is automatically acquired.
|
||||
|
||||
The parser bounds nesting to 64 and token/collection growth to the selected
|
||||
terminal payload's byte count (the format's terminal length is at most 65535).
|
||||
Every data item consumes encoded bytes; broad declared collection lengths cannot
|
||||
justify allocation without that lower bound. Expanded results still pass the
|
||||
worker's existing 16 MiB output check.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Solidity terminal metadata inspection
|
||||
|
||||
Issue: https://github.com/morluto/rea/issues/2388
|
||||
|
||||
## Outcome
|
||||
|
||||
The existing EVM interface operation should retain independently decoded terminal
|
||||
CBOR candidate facts from the exact selected bytes instead of relying on precision
|
||||
unknowns in the engine's reported metadata. This increment covers trailer ranges,
|
||||
bounded decoding, exact integer/non-text scalar key evidence and compiler-version
|
||||
representations.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Preserve the existing carrier/decoded identities and original full bytecode.
|
||||
- Report a plausible terminal root-map candidate separately from proof of Solidity
|
||||
origin. Missing or non-map trailers remain unavailable, not proven absent.
|
||||
- Interpret scalar root-map keys/values losslessly: integers as decimal strings,
|
||||
bytes as hex, strings, booleans and null. Preserve undecoded/unsupported values
|
||||
through the original bytecode plus exact candidate offset/length.
|
||||
- Report partial and not-decoded coverage, parser/profile failures and unsupported
|
||||
CBOR forms explicitly. Do not silently coerce unknown keys into object properties.
|
||||
- Interpret admitted solc release/prerelease representations only from observed
|
||||
entries; preserve ambiguity if duplicate compiler-version keys occur.
|
||||
- Keep the additive field compatible with historical Evidence and retain
|
||||
it through selected summary delivery and public CLI/MCP.
|
||||
Historical omission parses as a null unknown facet without mutating the source.
|
||||
- Bound nesting before decoder recursion and collection lengths before allocation.
|
||||
Keep parsing inside the existing owned worker and its resource/output budgets.
|
||||
- Do not fetch metadata or source content, contact RPC or execute contract code.
|
||||
|
||||
## Acceptance and limits
|
||||
|
||||
Owned compiler fixtures and malformed/synthetic trailers cover integers beyond
|
||||
Number precision, non-text keys, nested/uninterpreted values, invalid UTF-8,
|
||||
invalid lengths, unsupported tags and nesting bounds. Public boundary tests
|
||||
preserve the field and older omission. A real pinned solc artifact verifies actual
|
||||
metadata facts; the existing Linux-only EVM host profile remains unchanged.
|
||||
|
||||
## Deferred issue criteria
|
||||
|
||||
Binding a supplied metadata JSON to IPFS/Swarm commitments is a later independent
|
||||
increment of #2388. This PR must reference rather than close the whole issue.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Tasks
|
||||
|
||||
- [x] Define single-feature requirements and implementation plan before code.
|
||||
- [x] Add the pinned decoder and bounded owner boundary.
|
||||
- [x] Implement lossless scalar metadata facts and compiler-version evidence.
|
||||
- [x] Integrate worker/provider contracts and summary/historical compatibility.
|
||||
- [x] Verify producer, worker and public boundary behavior with owned fixtures.
|
||||
- [x] Run relevant repository checks and prepare a separately scoped PR.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Verification
|
||||
|
||||
After integrating upstream c462a4ebd, 47 tests passed
|
||||
across the parser, domain contract, application service, compiled worker and MCP
|
||||
boundary (five files). Compiled-worker goldens include BOM text, partial nested
|
||||
values, unavailable trailers, malformed UTF-8 and unsupported floats. Five
|
||||
upstream adapter tests also passed after its stricter integer validation landed.
|
||||
All six static tasks and architecture guards
|
||||
passed. After integration, test discovery found all 1004 files exactly once.
|
||||
The CI-planner selection passed all 85 tests, including the new EVM-verifier case.
|
||||
|
||||
The parser regressions cover exact large integers and typed keys, malformed UTF-8,
|
||||
duplicate keys, excessive nesting, unsupported tags/floats, length bounds and
|
||||
historical omission. A real defect in the decoder's default text handling removed
|
||||
an initial U+FEFF. The owning tokenizer now validates and decodes the retained
|
||||
text bytes with BOM preservation; independent fixtures check both values and
|
||||
compiler-key identity.
|
||||
|
||||
The owned fixture was compiled with caller-selected solc 0.8.30, using unoptimized,
|
||||
optimized via-IR, content-hash-free and CBOR-free settings. All four metadata
|
||||
checks passed again after integration. No contract was executed. Local first decode
|
||||
took approximately 0.81 ms or less. The originally reported approximately 2.92 ms
|
||||
for 100 warm iterations included equality assertions and is not a decode-only
|
||||
measurement. The verifier now measures parsing separately from those assertions;
|
||||
updated warm timings have not been collected. These historical figures describe
|
||||
these fixtures and this host, not a general workload.
|
||||
Peak process RSS
|
||||
included the compiler (299536 KiB); complete process-family CPU/memory and agent
|
||||
token costs were not measured.
|
||||
|
||||
Compiled worker tests exercise unchanged EVMole WASM on this macOS host. MCP tests
|
||||
use the production server and contracts with an injected provider seam. The full
|
||||
Linux-only provider CLI/MCP, resource-limit and cleanup lane remains unverified
|
||||
here. Metadata JSON commitment binding remains deferred under issue #2388.
|
||||
|
||||
The Linux CLI/MCP verifier asserts exact compiler bytes, declared version and
|
||||
candidate ranges for its existing owned raw/hex artifacts. Its CI lane additionally
|
||||
runs the new compiler-setting matrix. A regression using the actual CI planner
|
||||
checks that edits to the new verifier select the EVM lane.
|
||||
|
||||
The later merge/review correction adds decoded-summary and omitted historical
|
||||
metadata coverage. Tests and provider reruns for that correction were skipped at
|
||||
the maintainer's request; the earlier runs above do not validate the changed head.
|
||||
|
|
@ -2,7 +2,7 @@ REA npm dependency inventory
|
|||
Prepared from the root package-lock.json; refresh this inventory when locked dependencies change.
|
||||
License identifiers are upstream lockfile declarations, not a legal assessment.
|
||||
Runtime includes transitive and optional platform packages; development includes build, test and website tooling.
|
||||
Locked package entries: 586; runtime: 174.
|
||||
Locked package entries: 587; runtime: 175.
|
||||
Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt.
|
||||
|
||||
@algolia/abtesting@1.25.0 | development | MIT
|
||||
|
|
@ -1430,6 +1430,11 @@ canonicalize@3.0.0 | runtime | Apache-2.0
|
|||
Package: https://www.npmjs.com/package/canonicalize/v/3.0.0
|
||||
Archive: https://registry.npmjs.org/canonicalize/-/canonicalize-3.0.0.tgz
|
||||
Integrity: sha512-yYLfHyDMIXRyRqsKBRLX023riFLpXY2YOfdtqKXZRZy9qsfOJ9U+4F9YZL7MEzL5+ziN2x2nlBvY/Voi3EBljA==
|
||||
cborg@6.1.3 | runtime | Apache-2.0
|
||||
Lockfile path: node_modules/cborg
|
||||
Package: https://www.npmjs.com/package/cborg/v/6.1.3
|
||||
Archive: https://registry.npmjs.org/cborg/-/cborg-6.1.3.tgz
|
||||
Integrity: sha512-Kb/wD/AFuVcqvVjR+HYNOjNiuhVJ2C+RWxQE2Yqq2WVlVkRjdvVLkkM6QsL3NMsBRh+rgNc+aXmcraA+X0HObQ==
|
||||
ccount@2.0.1 | development | MIT
|
||||
Lockfile path: node_modules/ccount
|
||||
Package: https://www.npmjs.com/package/ccount/v/2.0.1
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
REA third-party npm runtime copyright, license and NOTICE texts
|
||||
Collected from integrity-verified locked npm archives. Release-specific supplements and upstream notice gaps are identified in their entries.
|
||||
External engines are separately obtained; see THIRD_PARTY_NOTICES.md.
|
||||
Runtime dependency fingerprint (SHA-256): 5de9cf97e7c327da1892efeff30b40507e4c7f8e496b5d305b8016c162bc15e3
|
||||
Runtime dependency fingerprint (SHA-256): 63d5b55e027a0cfb3e77989ca337aca905f1652d10cfbfc4d4dd876d3ecd4f3a
|
||||
|
||||
===== @babel/helper-string-parser@8.0.0 =====
|
||||
Declared license: MIT
|
||||
|
|
@ -2456,6 +2456,234 @@ Scope: npm runtime dependency, including optional platform packages.
|
|||
limitations under the License.
|
||||
|
||||
|
||||
===== cborg@6.1.3 =====
|
||||
Declared license: Apache-2.0
|
||||
Archive: https://registry.npmjs.org/cborg/-/cborg-6.1.3.tgz
|
||||
Integrity: sha512-Kb/wD/AFuVcqvVjR+HYNOjNiuhVJ2C+RWxQE2Yqq2WVlVkRjdvVLkkM6QsL3NMsBRh+rgNc+aXmcraA+X0HObQ==
|
||||
Scope: npm runtime dependency, including optional platform packages.
|
||||
|
||||
--- package/LICENSE ---
|
||||
|
||||
Copyright 2020 Rod Vagg
|
||||
|
||||
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-2.0 full license (licenses/Apache-2.0.txt) ---
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
===== clean-git-ref@2.0.1 =====
|
||||
Declared license: Apache-2.0
|
||||
Archive: https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"ajv": "8.20.0",
|
||||
"ajv-formats": "3.0.1",
|
||||
"canonicalize": "3.0.0",
|
||||
"cborg": "6.1.3",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "9.0.0",
|
||||
"evmole": "0.9.3",
|
||||
|
|
@ -4181,6 +4182,15 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cborg": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/cborg/-/cborg-6.1.3.tgz",
|
||||
"integrity": "sha512-Kb/wD/AFuVcqvVjR+HYNOjNiuhVJ2C+RWxQE2Yqq2WVlVkRjdvVLkkM6QsL3NMsBRh+rgNc+aXmcraA+X0HObQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"cborg": "lib/bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ccount": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -270,6 +270,7 @@
|
|||
"verify:wasm:package": "npm run build:cached && node scripts/verify-packaged-wasm-artifact.mjs",
|
||||
"verify:wasm:artifact": "npm run build:cached && node scripts/verify-wasm-artifact.mjs",
|
||||
"verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs",
|
||||
"verify:solidity:metadata": "npm run build:cached && node scripts/verify-solidity-metadata.mjs",
|
||||
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs",
|
||||
"mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check",
|
||||
"compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/bundle-runtime.mjs",
|
||||
|
|
@ -309,6 +310,7 @@
|
|||
"ajv": "8.20.0",
|
||||
"ajv-formats": "3.0.1",
|
||||
"canonicalize": "3.0.0",
|
||||
"cborg": "6.1.3",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "9.0.0",
|
||||
"evmole": "0.9.3",
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ export const SCOPES = {
|
|||
"src/artifacts/readStableArtifact*.ts",
|
||||
"src/artifacts/ArtifactReader.ts",
|
||||
"scripts/verify-evm-interface.mjs",
|
||||
"scripts/verify-solidity-metadata.mjs",
|
||||
"scripts/fixtures/evm-interface.sol",
|
||||
"third_party/evmole/**",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -225,6 +225,26 @@ try {
|
|||
for (const mode of ["cli", "mcp"]) {
|
||||
const report = await inspect(mode, path, encoding);
|
||||
assert.equal(report.bytecode.hex, hex);
|
||||
assert.equal(report.bytecode.metadata.status, "decoded");
|
||||
assert.deepEqual(report.bytecode.metadata.compiler_version, {
|
||||
representation: "release-bytes",
|
||||
value: "0.8.30",
|
||||
});
|
||||
assert.equal(
|
||||
report.bytecode.metadata.bytecode_offset +
|
||||
report.bytecode.metadata.cbor_bytes +
|
||||
2,
|
||||
Buffer.from(hex, "hex").length,
|
||||
);
|
||||
assert.ok(
|
||||
report.bytecode.metadata.entries.some(
|
||||
({ key, value }) =>
|
||||
key.kind === "text" &&
|
||||
key.value === "solc" &&
|
||||
value.kind === "bytes" &&
|
||||
value.value === "00081e",
|
||||
),
|
||||
);
|
||||
assert.deepEqual(
|
||||
report.functions
|
||||
.filter(({ dispatch }) => dispatch === "abi")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { isAbsolute } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { inspectSolidityMetadata } from "../dist/evm/SolidityMetadata.js";
|
||||
|
||||
const compilerModule = process.env.REA_VERIFY_SOLC_MODULE;
|
||||
if (!isAbsolute(compilerModule ?? ""))
|
||||
throw new Error(
|
||||
"Select an absolute caller-installed solc 0.8.30 module with REA_VERIFY_SOLC_MODULE.",
|
||||
);
|
||||
const solc = (await import(pathToFileURL(compilerModule).href)).default;
|
||||
assert.equal(solc.version(), "0.8.30+commit.73712a01.Emscripten.clang");
|
||||
const source = await readFile(
|
||||
new URL("./fixtures/evm-interface.sol", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const cases = [
|
||||
{ name: "unoptimized", optimizer: false, viaIR: false, metadata: {} },
|
||||
{ name: "viaIR", optimizer: true, viaIR: true, metadata: {} },
|
||||
{
|
||||
name: "without-content-hash",
|
||||
optimizer: false,
|
||||
viaIR: false,
|
||||
metadata: { bytecodeHash: "none" },
|
||||
},
|
||||
{
|
||||
name: "without-CBOR",
|
||||
optimizer: false,
|
||||
viaIR: false,
|
||||
metadata: { appendCBOR: false },
|
||||
},
|
||||
];
|
||||
const reports = [];
|
||||
for (const fixture of cases) {
|
||||
const compiled = JSON.parse(
|
||||
solc.compile(
|
||||
JSON.stringify({
|
||||
language: "Solidity",
|
||||
sources: { "fixture.sol": { content: source } },
|
||||
settings: {
|
||||
optimizer: { enabled: fixture.optimizer },
|
||||
viaIR: fixture.viaIR,
|
||||
metadata: fixture.metadata,
|
||||
outputSelection: { "*": { "*": ["evm.deployedBytecode.object"] } },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
!(compiled.errors ?? []).some((error) => error.severity === "error"),
|
||||
JSON.stringify(compiled.errors),
|
||||
);
|
||||
const bytes = Buffer.from(
|
||||
compiled.contracts["fixture.sol"].InterfaceFixture.evm.deployedBytecode
|
||||
.object,
|
||||
"hex",
|
||||
);
|
||||
const cpu = process.cpuUsage();
|
||||
const started = performance.now();
|
||||
const parsed = inspectSolidityMetadata(bytes);
|
||||
const firstMs = performance.now() - started;
|
||||
const firstCpu = process.cpuUsage(cpu);
|
||||
if (fixture.metadata.appendCBOR === false)
|
||||
assert.equal(parsed.status, "unavailable");
|
||||
else {
|
||||
assert.equal(parsed.status, "decoded");
|
||||
assert.deepEqual(parsed.compiler_version, {
|
||||
representation: "release-bytes",
|
||||
value: "0.8.30",
|
||||
});
|
||||
assert.equal(parsed.bytecode_offset + parsed.cbor_bytes + 2, bytes.length);
|
||||
if (fixture.metadata.bytecodeHash === "none")
|
||||
assert.deepEqual(
|
||||
parsed.entries.map(({ key }) => key),
|
||||
[{ kind: "text", value: "solc" }],
|
||||
);
|
||||
}
|
||||
const warmResults = new Array(100);
|
||||
const warmStart = performance.now();
|
||||
for (let index = 0; index < warmResults.length; index++)
|
||||
warmResults[index] = inspectSolidityMetadata(bytes);
|
||||
const warmMs = performance.now() - warmStart;
|
||||
for (const result of warmResults) assert.deepEqual(result, parsed);
|
||||
reports.push({
|
||||
fixture: fixture.name,
|
||||
bytes: bytes.length,
|
||||
status: parsed.status,
|
||||
first_decode_ms: firstMs,
|
||||
first_decode_cpu_us: firstCpu,
|
||||
warm_100_decode_ms: warmMs,
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
compiler_version: solc.version(),
|
||||
contract_execution: "not-performed",
|
||||
cases: reports,
|
||||
process_peak_rss_kib: process.resourceUsage().maxRSS,
|
||||
measurements:
|
||||
"In-process decode timings; peak RSS includes the compiler. End-to-end Linux provider/CLI costs and agent tokens were not measured.",
|
||||
}),
|
||||
);
|
||||
|
|
@ -2,6 +2,14 @@ import { expect, it } from "vitest";
|
|||
import { evmInterfaceSchema } from "./evmInterface.js";
|
||||
import { evmInterfaceFixture } from "../../../tests/fixtures/evm/interface.js";
|
||||
|
||||
it("accepts historical metadata omission as an unknown facet without changing the source", () => {
|
||||
const value = evmInterfaceFixture();
|
||||
const { metadata: _metadata, ...oldBytecode } = value.bytecode;
|
||||
const old = { ...value, bytecode: oldBytecode };
|
||||
expect(evmInterfaceSchema.parse(old).bytecode.metadata).toBeNull();
|
||||
expect(old.bytecode).not.toHaveProperty("metadata");
|
||||
});
|
||||
|
||||
it.each(["byte-count", "body-offset", "selector", "authenticity"])(
|
||||
"rejects false interface evidence: %s",
|
||||
(problem) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { localPathStringSchema } from "../localPath.js";
|
||||
import { solidityMetadataSchema } from "./solidityMetadata.js";
|
||||
|
||||
const byteOffset = z.number().int().nonnegative();
|
||||
const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
|
||||
|
|
@ -34,6 +35,7 @@ export const evmInterfaceSchema = z
|
|||
kind: z.literal("unknown"),
|
||||
deployment_authenticity: z.literal("unknown"),
|
||||
hardfork: z.literal("unknown"),
|
||||
metadata: solidityMetadataSchema.nullable().default(null),
|
||||
}),
|
||||
evidence_kind: z.literal("inferred"),
|
||||
functions: z.array(
|
||||
|
|
@ -62,6 +64,23 @@ export const evmInterfaceSchema = z
|
|||
message:
|
||||
"Selected byte count does not match the bytecode hex representation.",
|
||||
});
|
||||
const metadata = value.bytecode.metadata;
|
||||
if (
|
||||
metadata !== null &&
|
||||
metadata.status !== "unavailable" &&
|
||||
(metadata.bytecode_offset === null ||
|
||||
metadata.cbor_bytes === null ||
|
||||
metadata.bytecode_offset + metadata.cbor_bytes + 2 !==
|
||||
value.bytecode.bytes ||
|
||||
metadata.cbor_bytes !==
|
||||
Number.parseInt(value.bytecode.hex.slice(-4), 16))
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["bytecode", "metadata"],
|
||||
message:
|
||||
"Metadata range must match the terminal length suffix in selected bytecode.",
|
||||
});
|
||||
for (const [index, fn] of value.functions.entries())
|
||||
if (fn.bytecode_offset >= value.bytecode.bytes)
|
||||
context.addIssue({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
import { z } from "zod";
|
||||
|
||||
/** Scalar view of CBOR; uninterpreted containers remain in original bytecode. */
|
||||
export const solidityMetadataScalarSchema = z.discriminatedUnion("kind", [
|
||||
z.strictObject({ kind: z.literal("text"), value: z.string().max(65535) }),
|
||||
z.strictObject({
|
||||
kind: z.literal("integer"),
|
||||
value: z
|
||||
.string()
|
||||
.max(21)
|
||||
.regex(/^-?(?:0|[1-9][0-9]*)$/),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("bytes"),
|
||||
value: z
|
||||
.string()
|
||||
.max(131070)
|
||||
.regex(/^(?:[a-f0-9]{2})*$/),
|
||||
}),
|
||||
z.strictObject({ kind: z.literal("boolean"), value: z.boolean() }),
|
||||
z.strictObject({ kind: z.literal("null") }),
|
||||
z.strictObject({
|
||||
kind: z.literal("uninterpreted"),
|
||||
container: z.enum(["array", "map"]),
|
||||
}),
|
||||
]);
|
||||
|
||||
/** A trailer candidate, not authentication of Solidity origin or deployment. */
|
||||
export const solidityMetadataSchema = z
|
||||
.strictObject({
|
||||
profile: z.literal("terminal-cbor-scalars-v1"),
|
||||
status: z.enum(["unavailable", "decoded", "partial", "not-decoded"]),
|
||||
bytecode_offset: z.number().int().nonnegative().nullable(),
|
||||
cbor_bytes: z.number().int().positive().max(65535).nullable(),
|
||||
entries: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
key: solidityMetadataScalarSchema,
|
||||
value: solidityMetadataScalarSchema,
|
||||
}),
|
||||
)
|
||||
.max(32767),
|
||||
compiler_version: z
|
||||
.strictObject({
|
||||
representation: z.enum(["release-bytes", "prerelease-text"]),
|
||||
value: z.string().min(1).max(65535),
|
||||
})
|
||||
.nullable(),
|
||||
diagnostic: z.string().max(512).nullable(),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
const unavailable = value.status === "unavailable";
|
||||
if (
|
||||
unavailable
|
||||
? value.bytecode_offset !== null || value.cbor_bytes !== null
|
||||
: value.bytecode_offset === null || value.cbor_bytes === null
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Only unavailable metadata omits the candidate range.",
|
||||
});
|
||||
const unexamined = unavailable || value.status === "not-decoded";
|
||||
if (
|
||||
unexamined &&
|
||||
(value.entries.length !== 0 || value.compiler_version !== null)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message:
|
||||
"Unavailable interpretation cannot report decoded entries or compiler identity.",
|
||||
});
|
||||
const uninterpreted = value.entries.some(
|
||||
({ key, value: entryValue }) =>
|
||||
key.kind === "uninterpreted" || entryValue.kind === "uninterpreted",
|
||||
);
|
||||
if (
|
||||
(value.status === "decoded" && uninterpreted) ||
|
||||
(value.status === "partial" && !uninterpreted)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Scalar coverage must preserve uninterpreted containers.",
|
||||
});
|
||||
});
|
||||
|
||||
export type SolidityMetadata = z.infer<typeof solidityMetadataSchema>;
|
||||
export type SolidityMetadataScalar = z.infer<
|
||||
typeof solidityMetadataScalarSchema
|
||||
>;
|
||||
|
|
@ -30,6 +30,7 @@ import {
|
|||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { InspectEvmInterfaceInput } from "../domain/evm/evmInterface.js";
|
||||
import { evmInterfaceSchema } from "../domain/evm/evmInterface.js";
|
||||
import { solidityMetadataSchema } from "../domain/evm/solidityMetadata.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
|
@ -67,6 +68,7 @@ const replySchema = z.discriminatedUnion("ok", [
|
|||
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
||||
bytes: z.number().int().nonnegative(),
|
||||
hex: z.string().regex(/^(?:[a-f0-9]{2})*$/),
|
||||
metadata: solidityMetadataSchema.optional(),
|
||||
}),
|
||||
raw: evmoleInterfaceReplySchema,
|
||||
}),
|
||||
|
|
@ -331,6 +333,7 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
|
|||
"Recovered selectors, argument strings and mutability are upstream inferences, not verified ABI signatures or names. Empty results do not prove the absence of callable dispatch.",
|
||||
"Bytecode kind, hardfork and deployed-contract authenticity are unknown. This inspection performs no target runtime execution, chain/RPC lookup or network request.",
|
||||
"Original carrier and decoded bytecode SHA-256 are distinct identities; SHA-256 is not the Ethereum Keccak code hash.",
|
||||
"Terminal CBOR metadata is an independently decoded candidate, not proof of Solidity origin, source identity or deployment. Nested or unsupported values retain explicit unavailable interpretation and original byte ranges.",
|
||||
"The owned worker has a 256 MiB JavaScript heap, 3 GiB virtual address-space limit and 30-second deadline. Virtual address space is distinct from resident memory; limits fail without partial success.",
|
||||
];
|
||||
const validated = evmInterfaceSchema.safeParse({
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
EvmCarrierFailure,
|
||||
} from "./EvmBytecodeCarrier.js";
|
||||
import { projectEvmoleInterface } from "./EvmoleInterfaceAdapter.js";
|
||||
import { inspectSolidityMetadata } from "./SolidityMetadata.js";
|
||||
import {
|
||||
EVM_INTERFACE_LIMITS,
|
||||
EVM_FILE_SIZE_FAILURE_EXIT,
|
||||
|
|
@ -64,6 +65,7 @@ const main = async (): Promise<void> => {
|
|||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes: bytes.length,
|
||||
hex: Buffer.from(bytes).toString("hex"),
|
||||
metadata: inspectSolidityMetadata(bytes),
|
||||
},
|
||||
raw: inferred.raw,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import { expect, it } from "vitest";
|
||||
import { inspectSolidityMetadata } from "./SolidityMetadata.js";
|
||||
|
||||
const bytecode = (hex: string) => {
|
||||
const payload = Buffer.from(hex, "hex");
|
||||
const length = Buffer.alloc(2);
|
||||
length.writeUInt16BE(payload.length);
|
||||
return Buffer.concat([Buffer.from("00", "hex"), payload, length]);
|
||||
};
|
||||
|
||||
it("reads a compiler version from actual trailer bytes without a magic-prefix assumption", () => {
|
||||
const result = inspectSolidityMetadata(bytecode("a164736f6c634300081e"));
|
||||
expect(result).toMatchObject({
|
||||
status: "decoded",
|
||||
bytecode_offset: 1,
|
||||
cbor_bytes: 10,
|
||||
compiler_version: { representation: "release-bytes", value: "0.8.30" },
|
||||
entries: [
|
||||
{
|
||||
key: { kind: "text", value: "solc" },
|
||||
value: { kind: "bytes", value: "00081e" },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves integer and non-text key precision beyond Number", () => {
|
||||
expect(
|
||||
inspectSolidityMetadata(bytecode("a1011bffffffffffffffff")).entries,
|
||||
).toEqual([
|
||||
{
|
||||
key: { kind: "integer", value: "1" },
|
||||
value: { kind: "integer", value: "18446744073709551615" },
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
inspectSolidityMetadata(bytecode("a1003bffffffffffffffff")).entries[0]
|
||||
?.value,
|
||||
).toEqual({
|
||||
kind: "integer",
|
||||
value: "-18446744073709551616",
|
||||
});
|
||||
});
|
||||
|
||||
it("retains opaque nested values as partial rather than fabricating scalar values", () => {
|
||||
expect(inspectSolidityMetadata(bytecode("a161788100"))).toMatchObject({
|
||||
status: "partial",
|
||||
entries: [
|
||||
{
|
||||
key: { kind: "text", value: "x" },
|
||||
value: { kind: "uninterpreted", container: "array" },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves a UTF-8 BOM inside a CBOR text value rather than stripping it", () => {
|
||||
expect(
|
||||
inspectSolidityMetadata(bytecode("a1617864efbbbf61")).entries[0]?.value,
|
||||
).toEqual({
|
||||
kind: "text",
|
||||
value: "\ufeffa",
|
||||
});
|
||||
expect(
|
||||
inspectSolidityMetadata(bytecode("a167efbbbf736f6c634300081e"))
|
||||
.compiler_version,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["invalid text", "a1617861ff"],
|
||||
["truncated value", "a16178"],
|
||||
["unsupported tag", "a16178c000"],
|
||||
["floating point", "a16178fa3f800000"],
|
||||
["duplicate key", "a2617801617802"],
|
||||
["excessive nesting", "a16178" + "81".repeat(65) + "00"],
|
||||
["impossible collection", "a161788a00"],
|
||||
])("keeps exact ranges and unavailable interpretation for %s", (_name, hex) => {
|
||||
expect(inspectSolidityMetadata(bytecode(hex))).toMatchObject({
|
||||
status: "not-decoded",
|
||||
bytecode_offset: 1,
|
||||
entries: [],
|
||||
compiler_version: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["", "60006000f3", "000000", "00010001"])(
|
||||
"does not claim absent Solidity metadata for an unavailable trailer: %s",
|
||||
(hex) =>
|
||||
expect(inspectSolidityMetadata(Buffer.from(hex, "hex"))).toMatchObject({
|
||||
status: "unavailable",
|
||||
bytecode_offset: null,
|
||||
cbor_bytes: null,
|
||||
entries: [],
|
||||
}),
|
||||
);
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import { decode, Tokenizer, Type, type Token } from "cborg";
|
||||
import type {
|
||||
SolidityMetadata,
|
||||
SolidityMetadataScalar,
|
||||
} from "../domain/evm/solidityMetadata.js";
|
||||
|
||||
const PROFILE = "terminal-cbor-scalars-v1";
|
||||
const MAX_DEPTH = 64;
|
||||
const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
||||
const decodeOptions = {
|
||||
useMaps: true,
|
||||
allowBigInt: true,
|
||||
allowIndefinite: false,
|
||||
allowUndefined: false,
|
||||
allowInfinity: false,
|
||||
allowNaN: false,
|
||||
rejectDuplicateMapKeys: true,
|
||||
retainStringBytes: true,
|
||||
} as const;
|
||||
|
||||
/** Bound recursive decoder entry before collection interpretation/allocation. */
|
||||
class BoundedMetadataTokenizer extends Tokenizer {
|
||||
readonly remaining: number[] = [];
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes, decodeOptions);
|
||||
}
|
||||
|
||||
override next(): Token {
|
||||
while (this.remaining.at(-1) === 0) this.remaining.pop();
|
||||
const token = super.next();
|
||||
if (
|
||||
Type.equals(token.type, Type.tag) ||
|
||||
Type.equals(token.type, Type.float)
|
||||
)
|
||||
throw new Error(
|
||||
"CBOR tags and floating-point values are outside this metadata profile.",
|
||||
);
|
||||
if (Type.equals(token.type, Type.string)) {
|
||||
if (token.byteValue === undefined)
|
||||
throw new Error(
|
||||
"CBOR text bytes were unavailable for strict UTF-8 validation.",
|
||||
);
|
||||
token.value = utf8.decode(token.byteValue);
|
||||
}
|
||||
const parent = this.remaining.length - 1;
|
||||
if (parent >= 0) this.remaining[parent]!--;
|
||||
if (
|
||||
Type.equals(token.type, Type.map) ||
|
||||
Type.equals(token.type, Type.array)
|
||||
) {
|
||||
const count: unknown = token.value;
|
||||
const children =
|
||||
typeof count === "number"
|
||||
? count * (Type.equals(token.type, Type.map) ? 2 : 1)
|
||||
: NaN;
|
||||
if (
|
||||
!Number.isSafeInteger(children) ||
|
||||
children < 0 ||
|
||||
children > this.data.length
|
||||
)
|
||||
throw new Error(
|
||||
"Declared CBOR collection exceeds the encoded item-byte bound.",
|
||||
);
|
||||
if (this.remaining.length >= MAX_DEPTH)
|
||||
throw new Error("CBOR nesting exceeds the 64-level metadata profile.");
|
||||
this.remaining.push(children);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
const scalar = (value: unknown): SolidityMetadataScalar => {
|
||||
if (typeof value === "string") return { kind: "text", value };
|
||||
if (typeof value === "bigint")
|
||||
return { kind: "integer", value: value.toString() };
|
||||
if (typeof value === "number" && Number.isSafeInteger(value))
|
||||
return { kind: "integer", value: String(value) };
|
||||
if (typeof value === "boolean") return { kind: "boolean", value };
|
||||
if (value === null) return { kind: "null" };
|
||||
if (value instanceof Uint8Array)
|
||||
return { kind: "bytes", value: Buffer.from(value).toString("hex") };
|
||||
if (value instanceof Map) return { kind: "uninterpreted", container: "map" };
|
||||
if (Array.isArray(value))
|
||||
return { kind: "uninterpreted", container: "array" };
|
||||
throw new Error(
|
||||
"CBOR decoder returned a value outside the admitted profile.",
|
||||
);
|
||||
};
|
||||
|
||||
/** Parse only a plausible terminal root-map candidate; never establish absence. */
|
||||
export const inspectSolidityMetadata = (
|
||||
bytes: Uint8Array,
|
||||
): SolidityMetadata => {
|
||||
const empty: SolidityMetadata = {
|
||||
profile: PROFILE,
|
||||
status: "unavailable",
|
||||
bytecode_offset: null,
|
||||
cbor_bytes: null,
|
||||
entries: [],
|
||||
compiler_version: null,
|
||||
diagnostic:
|
||||
"No plausible terminal CBOR map was identified; Solidity origin and metadata absence remain unknown.",
|
||||
};
|
||||
if (bytes.length < 3) return empty;
|
||||
const length = bytes[bytes.length - 2]! * 256 + bytes[bytes.length - 1]!;
|
||||
const offset = bytes.length - 2 - length;
|
||||
if (length === 0 || offset < 0 || bytes[offset]! >> 5 !== 5) return empty;
|
||||
const selected = bytes.subarray(offset, bytes.length - 2);
|
||||
const result: SolidityMetadata = {
|
||||
...empty,
|
||||
status: "not-decoded",
|
||||
bytecode_offset: offset,
|
||||
cbor_bytes: length,
|
||||
};
|
||||
try {
|
||||
const raw: unknown = decode(selected, {
|
||||
...decodeOptions,
|
||||
tokenizer: new BoundedMetadataTokenizer(selected),
|
||||
});
|
||||
if (!(raw instanceof Map))
|
||||
throw new Error("Terminal CBOR candidate did not decode to a root map.");
|
||||
for (const [key, value] of raw as Map<unknown, unknown>)
|
||||
result.entries.push({ key: scalar(key), value: scalar(value) });
|
||||
result.status = result.entries.some(
|
||||
({ key, value }) =>
|
||||
key.kind === "uninterpreted" || value.kind === "uninterpreted",
|
||||
)
|
||||
? "partial"
|
||||
: "decoded";
|
||||
result.diagnostic =
|
||||
result.status === "partial"
|
||||
? "Nested containers are uninterpreted; their exact bytes remain at the recorded range in original bytecode."
|
||||
: null;
|
||||
const version: unknown = raw.get("solc");
|
||||
if (version instanceof Uint8Array && version.length === 3)
|
||||
result.compiler_version = {
|
||||
representation: "release-bytes",
|
||||
value: [...version].join("."),
|
||||
};
|
||||
else if (typeof version === "string" && version.length > 0)
|
||||
result.compiler_version = {
|
||||
representation: "prerelease-text",
|
||||
value: version,
|
||||
};
|
||||
return result;
|
||||
} catch (cause: unknown) {
|
||||
return {
|
||||
...result,
|
||||
status: "not-decoded",
|
||||
entries: [],
|
||||
compiler_version: null,
|
||||
diagnostic: (cause instanceof Error
|
||||
? cause.message
|
||||
: String(cause)
|
||||
).slice(0, 512),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, it } from "vitest";
|
||||
import { solidityMetadataSchema } from "../../../src/domain/evm/solidityMetadata.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
/** Real compiled worker and unchanged WASM engine; no Linux provider-admission claim. */
|
||||
it.each([
|
||||
["compiler", "00a164736f6c634300081e000a", "decoded"],
|
||||
["integer", "00a1011bffffffffffffffff000b", "decoded"],
|
||||
["BOM text", "00a1617864efbbbf610008", "decoded"],
|
||||
["nested value", "00a1617881000005", "partial"],
|
||||
["unavailable trailer", "00", "unavailable"],
|
||||
["unsupported tag", "00a16178c0000005", "not-decoded"],
|
||||
["floating point", "00a16178fa3f8000000008", "not-decoded"],
|
||||
["invalid UTF-8", "00a1617861ff0005", "not-decoded"],
|
||||
])(
|
||||
"retains independent %s metadata through the actual worker",
|
||||
async (kind, hex, status) => {
|
||||
const directory = await createTestTempDirectory("rea-solidity-worker-");
|
||||
const snapshot = join(directory, "selected.hex");
|
||||
const reply = join(directory, "reply.json");
|
||||
const request = join(directory, "request.json");
|
||||
await writeFile(snapshot, hex);
|
||||
await writeFile(
|
||||
request,
|
||||
JSON.stringify({
|
||||
snapshot_path: snapshot,
|
||||
reply_path: reply,
|
||||
failure_marker_path: join(directory, "resource.failure"),
|
||||
encoding: "hex",
|
||||
}),
|
||||
);
|
||||
await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
"--max-old-space-size=256",
|
||||
"--disable-wasm-trap-handler",
|
||||
new URL("../../../dist/evm/EvmoleInterfaceWorker.js", import.meta.url)
|
||||
.pathname,
|
||||
request,
|
||||
],
|
||||
{ timeout: 30_000, maxBuffer: 1024 * 1024 },
|
||||
);
|
||||
const value = JSON.parse(await readFile(reply, "utf8"));
|
||||
expect(value.ok, JSON.stringify(value)).toBe(true);
|
||||
expect(value.bytecode.hex).toBe(hex);
|
||||
const metadata = solidityMetadataSchema.parse(value.bytecode.metadata);
|
||||
expect(metadata.status).toBe(status);
|
||||
if (kind === "integer")
|
||||
expect(metadata.entries[0]?.value).toEqual({
|
||||
kind: "integer",
|
||||
value: "18446744073709551615",
|
||||
});
|
||||
if (kind === "compiler")
|
||||
expect(metadata.compiler_version?.value).toBe("0.8.30");
|
||||
if (kind === "BOM text")
|
||||
expect(metadata.entries[0]?.value).toEqual({
|
||||
kind: "text",
|
||||
value: "\ufeffa",
|
||||
});
|
||||
if (kind === "nested value")
|
||||
expect(metadata.entries[0]?.value).toEqual({
|
||||
kind: "uninterpreted",
|
||||
container: "array",
|
||||
});
|
||||
if (kind === "unavailable trailer") {
|
||||
expect(metadata.bytecode_offset).toBeNull();
|
||||
expect(metadata.entries).toEqual([]);
|
||||
expect(metadata.compiler_version).toBeNull();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -449,6 +449,10 @@ it.each([
|
|||
sqlite: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"scripts/verify-solidity-metadata.mjs",
|
||||
{ code: true, evm: true, browser: false, package: false },
|
||||
],
|
||||
[
|
||||
"src/browser/execution/evaluate.ts",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { createHash } from "node:crypto";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it, onTestFinished, vi } from "vitest";
|
||||
import { EvmInterfaceService } from "../../../src/application/evm/EvmInterfaceService.js";
|
||||
|
|
@ -10,6 +11,7 @@ import {
|
|||
} from "../../../src/domain/analysisErrorCore.js";
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { evmInterfaceSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
|
||||
import { inspectSolidityMetadata } from "../../../src/evm/SolidityMetadata.js";
|
||||
import { err, ok } from "../../../src/domain/result.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
|
|
@ -26,8 +28,21 @@ import {
|
|||
evmInterfaceExecution,
|
||||
} from "../../fixtures/evm/interface.js";
|
||||
|
||||
it("advertises exact valid schemas and records inline EVM interface evidence without a binary target", async () => {
|
||||
const decodedMetadataFixture = () => {
|
||||
const bytes = Buffer.from("00a1011bffffffffffffffff000b", "hex");
|
||||
const value = evmInterfaceFixture();
|
||||
value.bytecode = {
|
||||
...value.bytecode,
|
||||
hex: bytes.toString("hex"),
|
||||
bytes: bytes.length,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
metadata: inspectSolidityMetadata(bytes),
|
||||
};
|
||||
return value;
|
||||
};
|
||||
|
||||
it("advertises exact valid schemas and records inline EVM interface evidence without a binary target", async () => {
|
||||
const value = decodedMetadataFixture();
|
||||
const inspect = vi.fn(() =>
|
||||
Promise.resolve(ok(evmInterfaceExecution(value))),
|
||||
);
|
||||
|
|
@ -280,7 +295,8 @@ it("settles the admitted EVM operation after SDK cancellation and accepts a subs
|
|||
});
|
||||
|
||||
it("returns the exact inferred interface summary while preserving complete defaults and retained source", async () => {
|
||||
const value = evmInterfaceFixture();
|
||||
const value = decodedMetadataFixture();
|
||||
expect(value.bytecode.metadata?.status).toBe("decoded");
|
||||
value.functions = [
|
||||
{
|
||||
selector: "0x11223344",
|
||||
|
|
@ -419,6 +435,7 @@ it.each(["valid", "wrong-subject", "no-subject"])(
|
|||
"preserves historical portable EVM Evidence and rejects incompatible identity: %s",
|
||||
async (scenario) => {
|
||||
const value = evmInterfaceFixture();
|
||||
const { metadata: _metadata, ...historicalBytecode } = value.bytecode;
|
||||
const source = createEvidence(
|
||||
scenario === "no-subject"
|
||||
? undefined
|
||||
|
|
@ -434,7 +451,7 @@ it.each(["valid", "wrong-subject", "no-subject"])(
|
|||
{
|
||||
operation: "inspect_evm_interface",
|
||||
parameters: { path: value.artifact.path, encoding: "hex" },
|
||||
result: value,
|
||||
result: { ...value, bytecode: historicalBytecode },
|
||||
confidence: "inferred",
|
||||
rawResult: { original: true },
|
||||
},
|
||||
|
|
@ -463,6 +480,7 @@ it.each(["valid", "wrong-subject", "no-subject"])(
|
|||
expect(view.parent_evidence_id).toBe(source.evidence_id);
|
||||
expect(view.summary.functions).toEqual([]);
|
||||
expect(view.summary.discovery_completeness).toBe("unknown");
|
||||
expect(view.summary.bytecode.metadata).toBeNull();
|
||||
expect(view.coverage).toMatchObject({
|
||||
total: 0,
|
||||
examined: 0,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export const evmInterfaceFixture = (
|
|||
kind: "unknown",
|
||||
deployment_authenticity: "unknown",
|
||||
hardfork: "unknown",
|
||||
metadata: null,
|
||||
},
|
||||
evidence_kind: "inferred",
|
||||
functions: [],
|
||||
|
|
|
|||
Loading…
Reference in New Issue