aivm: compare the recorded receipt_root unconditionally

verifyImported exists so every validator reaches byte-identical engine state from
the same committed intents, and its comparison was written
`wantRoot != (common.Hash{}) && got != wantRoot`. The zero-hash exemption made the
guarantee opt-out: a proposer recorded intents, wrote an all-zero receipt_root,
and every follower applied those intents and returned nil without ever checking
its resulting engine state matched the proposer's. Proven: a block whose
receipt_root was zeroed while the engine's real root was non-zero verified with
no error.

Nothing honest relied on the exemption. A proposer stamps the engine's own root
(vm.go:622), and before anything settles the root is legitimately zero on both
sides — so plain equality admits every honest block and rejects only a genuine
divergence.

Test: TestVerifyRejectsBlockClaimingZeroReceiptRoot.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-29 16:48:10 -07:00
co-authored by Hanzo Dev
parent 14b1b66956
commit c7aa384e15
2 changed files with 70 additions and 1 deletions
+8 -1
View File
@@ -126,7 +126,14 @@ func (vm *VM) verifyImported(height uint64, recorded []CIntent, wantRoot common.
return err
}
}
if got := vm.quorum.ReceiptRoot(vm.qstate); wantRoot != (common.Hash{}) && got != wantRoot {
// The recorded root must equal the one these imports actually produce, with no
// exemption for the zero hash. Skipping the comparison when the block claims
// zero made the determinism check opt-out: a proposer wrote a zero receipt_root
// and followers applied its intents without ever checking that their engine
// state agreed. An honest proposer stamps the engine's own root (vm.go:622), and
// a block with no folded receipts carries zero on both sides, so an equality
// check admits every honest case and only rejects a genuine divergence.
if got := vm.quorum.ReceiptRoot(vm.qstate); got != wantRoot {
return ErrReceiptRootMismatch
}
return nil
+62
View File
@@ -0,0 +1,62 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package aivm
import (
"context"
"testing"
"github.com/holiman/uint256"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
)
// TestVerifyRejectsBlockClaimingZeroReceiptRoot proves the receipt-root
// comparison has no exemption for the zero hash.
//
// verifyImported exists so every validator reaches byte-identical engine state
// from the same committed inputs. Exempting the zero hash made that guarantee
// opt-out: a proposer recorded intents, wrote a zero receipt_root, and followers
// applied the intents without ever checking their engine state agreed with the
// proposer's.
func TestVerifyRejectsBlockClaimingZeroReceiptRoot(t *testing.T) {
require := require.New(t)
v, reqr, _ := newAIVMForConsensus(t)
e, st, lg := v.QuorumEngine()
reward := uint256.NewInt(1_000_000_000_000_000_000)
// Drive one task to settlement so the engine has folded a receipt and the
// receipt_root is non-zero. Until something settles the root is legitimately
// zero on both sides, which is why the exemption looked harmless.
settled := mkIntent(e, reqr, testN, testThr, uint256.NewInt(1), reward)
taskID, err := e.ImportCommittedIntent(st, lg, acceptAll, settled, 100)
require.NoError(err)
selected, _ := e.SelectOperators(st, taskID, modelSpec, testN)
out := h(0x42)
for _, op := range selected {
require.NoError(e.CommitResponse(st, taskID, op, opCommit(taskID, op, out, h(0x01), h(0x02)), 101))
require.NoError(e.RevealResponse(st, taskID, op, out, h(0x01), h(0x02), 131))
}
_, err = e.Settle(st, lg, taskID, 161)
require.NoError(err)
require.NoError(v.commitEngine())
root := e.ReceiptRoot(st)
require.NotEqual(common.Hash{}, root, "a settled receipt must move the root")
// An honest proposer stamps the engine's own root and verifies.
v.EnqueueCommittedIntent(mkIntent(e, reqr, testN, testThr, uint256.NewInt(2), reward))
blk, err := v.BuildBlock(context.Background())
require.NoError(err)
honest := blk.(*Block)
require.Len(honest.ImportedIntents, 1)
require.Equal(root, honest.ReceiptRoot)
require.NoError(honest.Verify(context.Background()))
// The same block claiming a zero root must be refused, not waved through.
tampered := *honest
tampered.ReceiptRoot = common.Hash{}
require.ErrorIs(tampered.Verify(context.Background()), ErrReceiptRootMismatch)
}