Skip to content

fix: compare hashes instead of CIDs for anchor proof validation#754

Merged
stbrody merged 1 commit intomainfrom
fix/single-event-anchor-hash-validation
Jan 6, 2026
Merged

fix: compare hashes instead of CIDs for anchor proof validation#754
stbrody merged 1 commit intomainfrom
fix/single-event-anchor-hash-validation

Conversation

@m0ar
Copy link
Collaborator

@m0ar m0ar commented Jan 6, 2026

The blockchain only stores the 32-byte hash, not the CID codec. When single event batches are anchored, no tree node wrapper is added so the proof is just the raw (dag-jose) event CID. However, the validation reconstruction assumed the codec to be dag-cbor (what is used to encode the tree), causing validation failures even for the node that did the anchoring (as well as others).

Since the security property we care about is that the hash matches, this patch instead compares the hashes directly rather than full CIDs.

Here are the logs from our node anchoring a single event, then immediately getting the concluder stuck in a loop, failing to validate it:

{
  "timestamp": "2025-12-19T13:49:10.002266Z",
  "level": "INFO",
  "fields": {
    "message": "Anchoring root CID: bagcqcerapdx4z23gwfeyowfq3q7n7orcrqnafwtbz6wdkijgqdjy2mgxgzuq on chain 100"
  },
  "target": "ceramic_anchor_evm::evm_transaction_manager"
}
{
  "timestamp": "2025-12-19T13:49:10.161033Z",
  "level": "INFO",
  "fields": {
    "message": "Connected to EVM chain with ID: 100"
  },
  "target": "ceramic_anchor_evm::evm_transaction_manager"
}
{
  "timestamp": "2025-12-19T13:49:10.197830Z",
  "level": "INFO",
  "fields": {
    "message": "Starting wallet balance: 23476419449376212335 wei"
  },
  "target": "ceramic_anchor_evm::evm_transaction_manager"
}
{
  "timestamp": "2025-12-19T13:56:11.821410Z",
  "level": "INFO",
  "fields": {
    "message": "Anchor transaction submitted: 0x2ea17b24e5ebc069cc5ab69706f0d82011a560dd7fabf69960af3483eda1b7a6"
  },
  "target": "ceramic_anchor_evm::evm_transaction_manager"
}
{
  "timestamp": "2025-12-19T13:56:31.842508Z",
  "level": "INFO",
  "fields": {
    "message": "Transaction 0x2ea17b24e5ebc069cc5ab69706f0d82011a560dd7fabf69960af3483eda1b7a6 confirmed with 4 confirmations"
  },
  "target": "ceramic_anchor_evm::evm_transaction_manager"
}
{
  "timestamp": "2025-12-19T13:56:31.864128Z",
  "level": "INFO",
  "fields": {
    "message": "Ending wallet balance: 23476419390560577133 wei"
  },
  "target": "ceramic_anchor_evm::evm_transaction_manager"
}
{
  "timestamp": "2025-12-19T13:56:31.864165Z",
  "level": "INFO",
  "fields": {
    "message": "Total gas cost: 58815635202 wei"
  },
  "target": "ceramic_anchor_evm::evm_transaction_manager"
}
{
  "timestamp": "2025-12-19T13:56:31.864218Z",
  "level": "INFO",
  "fields": {
    "message": "store anchor batch: proof=bafyreibssfsa2l6od43y3nok4ioo33xorp2eryvsbd6kscvsm6ty3fxbrm, events=1"
  },
  "target": "ceramic_anchor_service::anchor"
}
{
  "timestamp": "2025-12-19T13:56:32.811501Z",
  "level": "WARN",
  "fields": {
    "message": "failed to poll_next_batch of conclusion_events",
    "err": "Execution error: Application error encountered: Failed to discover chain proof: InvalidProof(\"the root CID is not in the transaction (anchor proof root=bagcqcerapdx4z23gwfeyowfq3q7n7orcrqnafwtbz6wdkijgqdjy2mgxgzuq, blockchain transaction root=bafyreibapdx4z23gwfeyowfq3q7n7orcrqnafwtbz6wdkijgqdjy2mgxgy)\")"
  },
  "target": "ceramic_pipeline::concluder"
}
{
  "timestamp": "2025-12-19T13:56:33.743869Z",
  "level": "WARN",
  "fields": {
    "message": "failed to poll_next_batch of conclusion_events",
    "err": "Execution error: Application error encountered: Failed to discover chain proof: InvalidProof(\"the root CID is not in the transaction (anchor proof root=bagcqcerapdx4z23gwfeyowfq3q7n7orcrqnafwtbz6wdkijgqdjy2mgxgzuq, blockchain transaction root=bafyreibapdx4z23gwfeyowfq3q7n7orcrqnafwtbz6wdkijgqdjy2mgxgy)\")"
  },
  "target": "ceramic_pipeline::concluder"
}

The blockchain only stores the 32-byte hash, not the CID codec. When
validating single-event anchors where the original event uses dag-jose
codec, reconstruction assumed dag-cbor, causing validation failure.

Since the security property we care about is that the hash matches,
compare hashes directly rather than full CIDs.
@m0ar m0ar requested a review from stbrody January 6, 2026 16:28
@m0ar m0ar self-assigned this Jan 6, 2026
@m0ar m0ar requested a review from a team as a code owner January 6, 2026 16:28
@m0ar m0ar added the bug Something isn't working label Jan 6, 2026
@m0ar m0ar requested review from stephhuynh18 and removed request for a team January 6, 2026 16:28
@m0ar m0ar temporarily deployed to tnet-prod-2024 January 6, 2026 16:50 — with GitHub Actions Inactive
Copy link
Collaborator

@stbrody stbrody left a comment

Choose a reason for hiding this comment

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

So it feels like the code is making some incorrect assumptions about the codec right now

Probably the "right" fix would be to update the ChainInclusionProof to store the hash instead of the Cid, since we can't reliably reconstruct the full Cid if the blockchain txn doesn't include the codec.

That said, this seems like a perfectly fine workaround and is definitely the quicker fix, so LGTM

@stbrody stbrody added this pull request to the merge queue Jan 6, 2026
Merged via the queue into main with commit 4feebce Jan 6, 2026
23 checks passed
@stbrody stbrody deleted the fix/single-event-anchor-hash-validation branch January 6, 2026 21:06
@m0ar
Copy link
Collaborator Author

m0ar commented Jan 7, 2026

So it feels like the code is making some incorrect assumptions about the codec right now

Probably the "right" fix would be to update the ChainInclusionProof to store the hash instead of the Cid, since we can't reliably reconstruct the full Cid if the blockchain txn doesn't include the codec.

That said, this seems like a perfectly fine workaround and is definitely the quicker fix, so LGTM

Yeah agreed. I don't think this is a new bug, probably CAS never anchored a single-node tree so this wasn't hit before self-anchoring. At least I think the merkle tree construction is the same implementation.

The main benefit of fixing the validation instead is that nodes can auto-heal these events on a version bump. Otherwise, it seems like every node needs to do manual intervention not to have the concluder endlessly failing to validate the affected events.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants