Skip to content

rpc/jsonrpc: lex-order executionWitness codes and state#21531

Merged
awskii merged 2 commits into
mainfrom
awskii/witness-canonical-sort
Jun 1, 2026
Merged

rpc/jsonrpc: lex-order executionWitness codes and state#21531
awskii merged 2 commits into
mainfrom
awskii/witness-canonical-sort

Conversation

@awskii
Copy link
Copy Markdown
Member

@awskii awskii commented May 30, 2026

Order codes and state lex-ascending by content (was: codes by codehash, state by trie-traversal). Confirmed against zkevm@v0.4.0 fixtures.

state is sorted after verifyWitnessStateless — its RLPDecode treats node 0 as the trie root, so the witness stays root-first through verification.

No pass-rate change (multiset compare); serialization alignment only.

Builds on #21518. Refs #20534 (causes 1 & 3).

Witness codes were ordered by codehash and state nodes by trie-traversal
order. The canonical stateless-witness format orders both lex-ascending by
content. Verified against the EEST zkevm@v0.4.0 corpus: all multi-entry
fixtures store codes and state sorted lex by content.

State is sorted after verifyWitnessStateless: the stateless verifier's
RLPDecode treats the first node as the trie root, so the witness must stay
root-first until verification completes, then be reordered for output.

Pass rate is unchanged (the suite compares as a multiset); this only aligns
serialization order.
@@ -705,6 +705,10 @@ func (api *DebugAPIImpl) ExecutionWitness(ctx context.Context, blockNrOrHash rpc
return nil, err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be useful insert a short comment to avoid in future we change the order:
Must sort after verifyWitnessStateless: the verifier expects result.State[0] to be the trie root

@awskii awskii enabled auto-merge May 31, 2026 14:12
@awskii awskii requested a review from lupin012 May 31, 2026 14:16
@yperbasis yperbasis requested a review from Copilot June 1, 2026 08:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aligns debug_executionWitness serialization with the canonical (fixture-expected) ordering by sorting both state trie nodes and codes lexicographically by their byte content, while keeping internal stateless verification root-first during decoding.

Changes:

  • Sort ExecutionWitnessResult.State lexicographically-by-content after verifyWitnessStateless completes.
  • Sort the deduplicated pre-state codes lexicographically-by-content (instead of by codehash).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

out.SortedCodes = append(out.SortedCodes, c)
}

preCode := rs.GetPreStateCode()
Comment on lines +708 to +711
// Sort after verifyWitnessStateless: RLPDecode treats result.State[0] as the trie root.
slices.SortFunc(result.State, func(a, b hexutil.Bytes) int {
return bytes.Compare(a, b)
})
Copy link
Copy Markdown
Member

@yperbasis yperbasis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — the load-bearing invariant is correct: trie.RLPDecode treats State[0] as the root, and the new sort runs after verifyWitnessStateless, so self-verification is untouched. The only consumer of the sorted result is JSON serialization (no internal caller depends on order), and the change is type- and determinism-safe (codes are pairwise-distinct via the hash-keyed map; the multiset is preserved, so the pass rate is unchanged as stated). Nice cleanup dropping codeWithHash.

Findings & suggestions

  1. No test locks in the new order (main gap). TestExecutionWitness only asserts non-nil + lengths. An ordering change with no assertion can be silently reverted by the next refactor — and the sort-after-verify invariant is only incidentally guarded today. Suggest pinning it:

    require.True(t, slices.IsSortedFunc(result.Codes, func(a, b hexutil.Bytes) int { return bytes.Compare(a, b) }))
    require.True(t, slices.IsSortedFunc(result.State, func(a, b hexutil.Bytes) int { return bytes.Compare(a, b) }))
  2. Follow-up (non-blocking): the emitted witness is no longer round-trippable through our own RLPDecode. It computes keccak(State[0]) as the root, and that lookup never fails (every node's hash is in the map) — so decoding the lex-sorted output wouldn't error, it would silently build a trie rooted at the lexicographically-smallest node. No in-repo path hits this (the only decode is pre-sort), but the positional-root assumption is now inconsistent with what we emit. Worth a tracking issue to locate the root by the known state-root hash instead of by position.

  3. Minor: the canonical-order claim rests on fixture inspection (zkevm@v0.4.0) — a link to the format spec in the PR body would make it auditable.

@awskii awskii added this pull request to the merge queue Jun 1, 2026
Merged via the queue into main with commit 8e4b40a Jun 1, 2026
91 checks passed
@awskii awskii deleted the awskii/witness-canonical-sort branch June 1, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

debug_executionWitness should be 100% correct and work fine for Zilkworm

4 participants