Skip to content

fix(cli): verify metaboard bytes against the describedByMetaV1 hash - #284

Merged
thedavidmeister merged 1 commit into
mainfrom
2026-08-25-issue-210
Aug 26, 2026
Merged

fix(cli): verify metaboard bytes against the describedByMetaV1 hash#284
thedavidmeister merged 1 commit into
mainfrom
2026-08-25-issue-210

Conversation

@thedavidmeister

Copy link
Copy Markdown
Contributor

Closes #210.

The defect

describedByMetaV1() returns the keccak256 of the meta bytes that describe the
contract. That binding is enforced on the way in by
LibDescribedByMeta.emitForDescribedAddress, which reverts MetadataMismatch
unless keccak256(meta) == described.describedByMetaV1(), and reproduced by the
subgraph, which indexes metaHash = keccak256(event.params.meta).

AuthoringMetaV2::fetch_for_contract queried the metaboard by that hash and
then cbor-decoded whatever came back without ever hashing it. A metaboard
endpoint — or a buggy or compromised indexer behind it — could answer any hash
with any words and the fetch succeeded, turning a content-addressed lookup into
a trust-the-server lookup.

The change

Before the returned bytes are decoded, keccak256(meta_bytes) is compared to
the hash the contract named; a mismatch is a new
AuthoringMetaV2Error::MetaHashMismatch { expected, actual }, wrapped in the
usual FetchAuthoringMetaV2WordError with the same address/rpcs/metaboard
context as every other failure. The guard sits before cbor_decode, so
unverified bytes are never parsed at all. Nothing else in the pipeline moves,
and an honest metaboard sees no behaviour change.

The issue leaves open that a caller-supplied metaboard URL may be considered
trusted, in which case the old behaviour was by design. This resolves it toward
verifying: the check is one keccak256 over bytes already in memory, and the hash
is the only thing that ties the on-chain claim to the off-chain bytes. Say so if
trust-the-server is the intended contract and this is the PR to reject.

How this relates to the adjacent work

QA

  • Discriminating tests:

    • test_fetch_for_contract_rejects_meta_bytes_not_matching_the_hash — a well
      formed authoring meta document served under 0x11…11, which is not its
      hash; asserts MetaHashMismatch and both of its fields.
    • test_fetch_for_contract_checks_the_hash_before_decoding — bytes that are
      neither the hashed content nor valid cbor; asserts the mismatch is what is
      reported, i.e. nothing unverified reaches the decoder.

    Both verified failing on base by deleting the guard and rerunning the module:
    16 passed; 2 failed, the first panicking on unwrap_err() of
    Ok(AuthoringMetaV2 { … "description 1" … }) — base returns the unverified
    words — and the second reporting
    MetaError(SerdeCborError(…)) where MetaHashMismatch is expected.

  • Two existing tests are changed in place rather than duplicated:
    test_fetch_for_contract_success_decodes_first_meta and
    test_fetch_for_contract_invalid_cbor_is_meta_error both mocked
    describedByMetaV1() as 0x01…01 while serving bytes hashing to something
    else — the exact condition now rejected. They now mock the hash of the bytes
    they serve (new meta_hex_hash test helper), which is what an honest metaboard
    answers with. They pass on base as well, so they are guards against
    over-fixing into "reject everything", not base discriminators.

  • Mutations applied (each ran all 18 module tests; the tree was restored and
    verified byte-identical afterwards):

    • guard deleted, i.e. base behaviour -> killed by both new tests (16 passed,
      2 failed).
    • if meta_bytes_hash.0 != metahash -> == -> 14 passed, 4 failed: both new
      tests plus success_decodes_first_meta and invalid_cbor_is_meta_error, so
      the condition is pinned in both directions.
    • expected / actual fields swapped -> killed only by
      rejects_meta_bytes_not_matching_the_hash (17 passed, 1 failed), the test
      that asserts both fields.
    • guard moved after cbor_decode + try_into -> killed only by
      checks_the_hash_before_decoding (17 passed, 1 failed), pinning the
      ordering.
  • Oracle, from outside the function under test: LibDescribedByMeta
    (src/lib/LibDescribedByMeta.sol) hashes the raw meta bytes and reverts unless
    they match describedByMetaV1(); subgraph/src/metaBoard.ts sets
    metaHash = keccak256(event.params.meta); get_metabytes_by_hash returns
    those same bytes hex-decoded. So keccak256 over the returned bytes verbatim —
    no rain-meta-document re-encoding — is the hash the contract names. Test
    expectations are computed by the test helper from the served bytes, not by the
    code under test, and the word fixtures are the pre-existing chisel-encoded
    WORDS_PAYLOAD_HEX.

  • Category check: the issue asks for (a) verifying keccak256(meta_bytes)
    against the describedByMetaV1 hash — done, before any decode; and notes (b)
    that further entries under the same hash are ignored without a consistency
    check — the bytes actually used are now verified, which is this issue's half,
    while reachability of the later entries is fetch_for_contract only considers metas[0] and the first cbor document: a valid authoring meta later in the list is unreachable #157 and stays in PR fix: scan every metaboard meta and cbor document for the authoring meta #251.

Local verification, in the repo's nix develop:

  • cargo test -p rain-metadata --lib -- authoring::v2 -> 18 passed, 0 failed.
  • cargo clippy -p rain-metadata --all-targets -- -D warnings -> clean.
  • cargo fmt --all -- --check -> clean.

The full suite was not run locally (shared machine); CI covers it.

🤖 Generated with Claude Code

fetch_for_contract queried the metaboard by the contract's meta hash and
decoded the answer without hashing it, so any endpoint could serve any
words for any hash. keccak256 of the returned bytes is now checked
against the hash before they are decoded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 27 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 01e9d296-8014-4c26-bb29-4eb77cf3f799

📥 Commits

Reviewing files that changed from the base of the PR and between 2eabb12 and bb49c6b.

📒 Files selected for processing (1)
  • crates/cli/src/meta/types/authoring/v2.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister
thedavidmeister merged commit a545259 into main Aug 26, 2026
11 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai assess this PR size classification for the totality of the PR with the following criterias and report it in your comment:

S/M/L PR Classification Guidelines:

This guide helps classify merged pull requests by effort and complexity rather than just line count. The goal is to assess the difficulty and scope of changes after they have been completed.

Small (S)

Characteristics:

  • Simple bug fixes, typos, or minor refactoring
  • Single-purpose changes affecting 1-2 files
  • Documentation updates
  • Configuration tweaks
  • Changes that require minimal context to review

Review Effort: Would have taken 5-10 minutes

Examples:

  • Fix typo in variable name
  • Update README with new instructions
  • Adjust configuration values
  • Simple one-line bug fixes
  • Import statement cleanup

Medium (M)

Characteristics:

  • Feature additions or enhancements
  • Refactoring that touches multiple files but maintains existing behavior
  • Breaking changes with backward compatibility
  • Changes requiring some domain knowledge to review

Review Effort: Would have taken 15-30 minutes

Examples:

  • Add new feature or component
  • Refactor common utility functions
  • Update dependencies with minor breaking changes
  • Add new component with tests
  • Performance optimizations
  • More complex bug fixes

Large (L)

Characteristics:

  • Major feature implementations
  • Breaking changes or API redesigns
  • Complex refactoring across multiple modules
  • New architectural patterns or significant design changes
  • Changes requiring deep context and multiple review rounds

Review Effort: Would have taken 45+ minutes

Examples:

  • Complete new feature with frontend/backend changes
  • Protocol upgrades or breaking changes
  • Major architectural refactoring
  • Framework or technology upgrades

Additional Factors to Consider

When deciding between sizes, also consider:

  • Test coverage impact: More comprehensive test changes lean toward larger classification
  • Risk level: Changes to critical systems bump up a size category
  • Team familiarity: Novel patterns or technologies increase complexity

Notes:

  • the assessment must be for the totality of the PR, that means comparing the base branch to the last commit of the PR
  • the assessment output must be exactly one of: S, M or L (single-line comment) in format of: SIZE={S/M/L}
  • do not include any additional text, only the size classification
  • your assessment comment must not include tips or additional sections
  • do NOT tag me or anyone else on your comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AuthoringMetaV2::fetch_for_contract trusts subgraph meta bytes without checking them against the describedByMetaV1 hash

1 participant