From d47f9db54bf9d5a553acdc73178110dc57933472 Mon Sep 17 00:00:00 2001 From: Hyunsoo Shin Date: Tue, 4 Aug 2026 18:25:51 +0900 Subject: [PATCH 1/3] node/cn: verify a blob tx sidecar only once A replayed blob transaction is decoded into a fresh sidecar object, so the sidecar's own validation cache never hits and the handler verified its KZG proofs again on every copy. One single-blob v1 sidecar measures ~15.5ms, and a 12MiB TxMsg fits about 90 of them, so a captured transaction could be resent indefinitely at no cost to the sender. The handler now keeps a bounded set of blob tx hashes it already verified and skips the verification for those. Constraint: the tx pool verifies the sidecar independently before admitting a transaction, so skipping the early check cannot let a bad sidecar through Rejected: reject duplicate hashes within one message | subsumed, the set already skips the repeats inside a single batch Rejected: global KZG semaphore or per-peer token buckets | the repeated verification is what made a small message expensive, and bounding concurrency would also delay legitimate propagation Confidence: high Scope-risk: narrow Not-tested: a rotating set of distinct captured blob txs still costs one verification each, which is bandwidth-bound Co-Authored-By: Claude Opus 5 (1M context) --- node/cn/handler.go | 11 ++++++++- node/cn/handler_msg_test.go | 49 ++++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/node/cn/handler.go b/node/cn/handler.go index 42e40a006..4f6a2ffab 100644 --- a/node/cn/handler.go +++ b/node/cn/handler.go @@ -87,6 +87,9 @@ const ( // ExtraNonSnapPeers is the number of non-snap peers allowed to connect more than snap peers. ExtraNonSnapPeers = 5 + + // maxVerifiedBlobTxs bounds verifiedBlobTxs. + maxVerifiedBlobTxs = 1024 ) var ( @@ -114,6 +117,10 @@ type ProtocolManager struct { chainconfig *params.ChainConfig maxPeers int + // verifiedBlobTxs holds blob tx hashes whose sidecar already passed KZG + // verification. The tx pool verifies the sidecar again before admitting it. + verifiedBlobTxs *knownHashSet + downloader ProtocolManagerDownloader fetcher ProtocolManagerFetcher peers PeerSet @@ -181,6 +188,7 @@ func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, ne handler: handler, nodetype: nodetype, txResendUseLegacy: cnconfig.TxResendUseLegacy, + verifiedBlobTxs: newKnownHashSet(maxVerifiedBlobTxs), blobSidecarReqManager: &sidecarReqManager{ list: make(map[common.Hash]*sidecarReq), cooldown: 10 * time.Second, @@ -1562,7 +1570,7 @@ func handleTxMsg(pm *ProtocolManager, p Peer, msg p2p.Msg) error { // defensive measure against potential DoS attacks. if tx.Type() == types.TxTypeEthereumBlob { sidecar := tx.BlobTxSidecar() - if sidecar != nil { + if sidecar != nil && !pm.verifiedBlobTxs.Contains(tx.Hash()) { // If any of the transaction contains invalid KZG sidecar, terminate transaction processing immediately. // KZG verification is computationally expensive, so this acts as a // defensive measure against potential DoS attacks. @@ -1570,6 +1578,7 @@ func handleTxMsg(pm *ProtocolManager, p Peer, msg p2p.Msg) error { logger.Warn("Disconnect peer for protocol violation", "peer", p.GetID(), "error", err) return errResp(ErrDecode, "Invalid blob transaction with sidecar: %v", errKZGVerificationError) } + pm.verifiedBlobTxs.Add(tx.Hash()) } } p.AddToKnownTxs(tx.Hash()) diff --git a/node/cn/handler_msg_test.go b/node/cn/handler_msg_test.go index ba54da252..5b0c403d3 100644 --- a/node/cn/handler_msg_test.go +++ b/node/cn/handler_msg_test.go @@ -291,33 +291,28 @@ func TestHandleTxMsg(t *testing.T) { } } -func TestHandleTxMsg_KZGVerificationError(t *testing.T) { - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() - - pm := &ProtocolManager{} +// prepareBlobTxMsg returns a protocol manager, a peer and a signed blob transaction +// with a valid v1 sidecar. Callers corrupt the sidecar via blobTx.BlobTxSidecar(). +func prepareBlobTxMsg(t *testing.T, mockCtrl *gomock.Controller) (*ProtocolManager, *MockPeer, *types.Transaction) { + pm := &ProtocolManager{verifiedBlobTxs: newKnownHashSet(maxVerifiedBlobTxs)} pm.acceptTxs.Store(1) mockTxPool := mocks.NewMockTxPool(mockCtrl) + mockTxPool.EXPECT().HandleTxMsg(gomock.Any()).AnyTimes() pm.txpool = mockTxPool mockPeer := NewMockPeer(mockCtrl) mockPeer.EXPECT().GetVersion().Return(kaia63).AnyTimes() mockPeer.EXPECT().GetID().Return("test-peer").AnyTimes() + mockPeer.EXPECT().AddToKnownTxs(gomock.Any()).AnyTimes() - // Generate a blob transaction with invalid KZG proof var ( - addr = crypto.PubkeyToAddress(keys[0].PublicKey) - signer = types.MakeSigner(params.TestChainConfig, common.Big0) blob = kzg4844.Blob{} commitment, _ = kzg4844.BlobToCommitment(&blob) proofs, _ = kzg4844.ComputeCellProofs(&blob) - blobhash = common.Hash(kzg4844.CalcBlobHashV1(sha256.New(), &commitment)) ) - // corrupt a byte in the commitment - commitment[0] = commitment[0] ^ 0xFF blobTx, err := types.NewTransactionWithMap(types.TxTypeEthereumBlob, map[types.TxValueKeyType]interface{}{ types.TxValueKeyNonce: uint64(0), - types.TxValueKeyTo: addr, + types.TxValueKeyTo: crypto.PubkeyToAddress(keys[0].PublicKey), types.TxValueKeyAmount: big.NewInt(0), types.TxValueKeyGasLimit: uint64(10000000), types.TxValueKeyGasFeeCap: big.NewInt(25), @@ -325,7 +320,7 @@ func TestHandleTxMsg_KZGVerificationError(t *testing.T) { types.TxValueKeyData: []byte{}, types.TxValueKeyAccessList: types.AccessList{}, types.TxValueKeyBlobFeeCap: big.NewInt(25), - types.TxValueKeyBlobHashes: []common.Hash{blobhash}, + types.TxValueKeyBlobHashes: []common.Hash{common.Hash(kzg4844.CalcBlobHashV1(sha256.New(), &commitment))}, types.TxValueKeySidecar: &types.BlobTxSidecar{ Version: types.BlobSidecarVersion1, Blobs: []kzg4844.Blob{blob}, @@ -335,17 +330,37 @@ func TestHandleTxMsg_KZGVerificationError(t *testing.T) { types.TxValueKeyChainID: params.TestChainConfig.ChainID, }) require.NoError(t, err) - require.NoError(t, blobTx.Sign(signer, keys[0])) + require.NoError(t, blobTx.Sign(types.MakeSigner(params.TestChainConfig, common.Big0), keys[0])) + return pm, mockPeer, blobTx +} - txs := types.Transactions{blobTx} - msg := generateMsg(t, TxMsg, txs) +func TestHandleTxMsg_KZGVerificationError(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + pm, mockPeer, blobTx := prepareBlobTxMsg(t, mockCtrl) + blobTx.BlobTxSidecar().Commitments[0][0] ^= 0xFF // Should return error and disconnect peer - err = handleTxMsg(pm, mockPeer, msg) + err := handleTxMsg(pm, mockPeer, generateMsg(t, TxMsg, types.Transactions{blobTx})) require.Error(t, err) assert.Contains(t, err.Error(), errKZGVerificationError.Error()) } +func TestHandleTxMsg_BlobSidecarVerifiedOnce(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + pm, mockPeer, blobTx := prepareBlobTxMsg(t, mockCtrl) + require.NoError(t, handleTxMsg(pm, mockPeer, generateMsg(t, TxMsg, types.Transactions{blobTx}))) + require.True(t, pm.verifiedBlobTxs.Contains(blobTx.Hash())) + + // The tx hash does not cover the sidecar, so the replay carries a broken proof + // under the same hash and passes only because the verification is skipped. + blobTx.BlobTxSidecar().Proofs[0][0] ^= 0xFF + assert.NoError(t, handleTxMsg(pm, mockPeer, generateMsg(t, TxMsg, types.Transactions{blobTx}))) +} + func prepareTestHandleBlockHeaderFetchRequestMsg(t *testing.T) (*gomock.Controller, *MockPeer, *mocks.MockBlockChain, *ProtocolManager) { mockCtrl := gomock.NewController(t) mockPeer := NewMockPeer(mockCtrl) From d0782886285eeff0955ae57cecf26a7626e8cf88 Mon Sep 17 00:00:00 2001 From: Hyunsoo Shin Date: Tue, 11 Aug 2026 17:24:13 +0900 Subject: [PATCH 2/3] node/cn: key the verified sidecar set by the sidecar The set was keyed by transaction hash, which does not cover the sidecar. A sender could bump the nonce to replay one sidecar under unlimited distinct hashes, so every copy missed the set and paid a full verification, and a sidecar swapped under an already verified hash hit the set and skipped verification entirely. Key the set by what the verification consumes instead: the blob hashes, the sidecar version, its blobs and its proofs. Commitments are excluded because each blob hash is the sha256 of one, so the hashes already pin them. Also reject a blob transaction that declares no blob hashes. Every length check in the verification compares against the declared hashes, so it passes vacuously, while the pool rejects such a transaction unconditionally - the handler forwarded something that could never be valid and left the sender connected. Constraint: the key has to cover the blobs, not only the proofs, since the proofs are verified against them Rejected: keep the transaction hash and add a sidecar fingerprint | any key that omits the blobs is bypassable the same way Confidence: high Scope-risk: narrow Not-tested: a rotating set of distinct captured blob txs still costs one verification each, which is bandwidth-bound Co-Authored-By: Claude Opus 5 (1M context) --- node/cn/handler.go | 44 +++++++++++++---- node/cn/handler_msg_test.go | 95 ++++++++++++++++++++++++++++--------- 2 files changed, 107 insertions(+), 32 deletions(-) diff --git a/node/cn/handler.go b/node/cn/handler.go index 4f6a2ffab..12d35f415 100644 --- a/node/cn/handler.go +++ b/node/cn/handler.go @@ -99,8 +99,26 @@ var ( errUnknownProcessingError = errors.New("unknown error during the msg processing") errUnsupportedEnginePolicy = errors.New("unsupported engine or policy") errKZGVerificationError = errors.New("KZG verification error") + errBloblessBlobTx = errors.New("blobless blob transaction") ) +// blobSidecarKey identifies what ValidateWithBlobHashes consumes. Commitments are left +// out because each hash is the sha256 of one, so the hashes already pin them. +func blobSidecarKey(hashes []common.Hash, sc *types.BlobTxSidecar) common.Hash { + parts := make([][]byte, 0, len(hashes)+1+len(sc.Blobs)+len(sc.Proofs)) + for i := range hashes { + parts = append(parts, hashes[i][:]) + } + parts = append(parts, []byte{sc.Version}) + for i := range sc.Blobs { + parts = append(parts, sc.Blobs[i][:]) + } + for i := range sc.Proofs { + parts = append(parts, sc.Proofs[i][:]) + } + return crypto.Keccak256Hash(parts...) +} + func errResp(code errCode, format string, v ...interface{}) error { return fmt.Errorf("%v - %v", code, fmt.Sprintf(format, v...)) } @@ -117,8 +135,7 @@ type ProtocolManager struct { chainconfig *params.ChainConfig maxPeers int - // verifiedBlobTxs holds blob tx hashes whose sidecar already passed KZG - // verification. The tx pool verifies the sidecar again before admitting it. + // verifiedBlobTxs holds blobSidecarKey values that already passed KZG verification. verifiedBlobTxs *knownHashSet downloader ProtocolManagerDownloader @@ -1569,16 +1586,23 @@ func handleTxMsg(pm *ProtocolManager, p Peer, msg p2p.Msg) error { // KZG verification is computationally expensive, so this acts as a // defensive measure against potential DoS attacks. if tx.Type() == types.TxTypeEthereumBlob { + // Without blob hashes the verification below passes vacuously. + if len(tx.BlobHashes()) == 0 { + return errResp(ErrDecode, "Invalid blob transaction with sidecar: %v", errBloblessBlobTx) + } sidecar := tx.BlobTxSidecar() - if sidecar != nil && !pm.verifiedBlobTxs.Contains(tx.Hash()) { - // If any of the transaction contains invalid KZG sidecar, terminate transaction processing immediately. - // KZG verification is computationally expensive, so this acts as a - // defensive measure against potential DoS attacks. - if err := sidecar.ValidateWithBlobHashes(tx.BlobHashes()); err != nil { - logger.Warn("Disconnect peer for protocol violation", "peer", p.GetID(), "error", err) - return errResp(ErrDecode, "Invalid blob transaction with sidecar: %v", errKZGVerificationError) + if sidecar != nil { + key := blobSidecarKey(tx.BlobHashes(), sidecar) + if !pm.verifiedBlobTxs.Contains(key) { + // If any of the transaction contains invalid KZG sidecar, terminate transaction processing immediately. + // KZG verification is computationally expensive, so this acts as a + // defensive measure against potential DoS attacks. + if err := sidecar.ValidateWithBlobHashes(tx.BlobHashes()); err != nil { + logger.Warn("Disconnect peer for protocol violation", "peer", p.GetID(), "error", err) + return errResp(ErrDecode, "Invalid blob transaction with sidecar: %v", errKZGVerificationError) + } + pm.verifiedBlobTxs.Add(key) } - pm.verifiedBlobTxs.Add(tx.Hash()) } } p.AddToKnownTxs(tx.Hash()) diff --git a/node/cn/handler_msg_test.go b/node/cn/handler_msg_test.go index 5b0c403d3..2aae15410 100644 --- a/node/cn/handler_msg_test.go +++ b/node/cn/handler_msg_test.go @@ -305,13 +305,30 @@ func prepareBlobTxMsg(t *testing.T, mockCtrl *gomock.Controller) (*ProtocolManag mockPeer.EXPECT().GetID().Return("test-peer").AnyTimes() mockPeer.EXPECT().AddToKnownTxs(gomock.Any()).AnyTimes() - var ( - blob = kzg4844.Blob{} - commitment, _ = kzg4844.BlobToCommitment(&blob) - proofs, _ = kzg4844.ComputeCellProofs(&blob) - ) - blobTx, err := types.NewTransactionWithMap(types.TxTypeEthereumBlob, map[types.TxValueKeyType]interface{}{ - types.TxValueKeyNonce: uint64(0), + sidecar, hashes := newBlobSidecar(t) + return pm, mockPeer, newBlobTx(t, 0, hashes, sidecar) +} + +// newBlobSidecar returns a valid v1 sidecar and the blob hashes it commits to. +func newBlobSidecar(t *testing.T) (*types.BlobTxSidecar, []common.Hash) { + blob := kzg4844.Blob{} + commitment, err := kzg4844.BlobToCommitment(&blob) + require.NoError(t, err) + proofs, err := kzg4844.ComputeCellProofs(&blob) + require.NoError(t, err) + return &types.BlobTxSidecar{ + Version: types.BlobSidecarVersion1, + Blobs: []kzg4844.Blob{blob}, + Commitments: []kzg4844.Commitment{commitment}, + Proofs: proofs, + }, []common.Hash{common.Hash(kzg4844.CalcBlobHashV1(sha256.New(), &commitment))} +} + +// newBlobTx signs a blob transaction carrying the given hashes and sidecar. The nonce is +// a parameter so a caller can replay one sidecar under a different transaction hash. +func newBlobTx(t *testing.T, nonce uint64, hashes []common.Hash, sidecar *types.BlobTxSidecar) *types.Transaction { + tx, err := types.NewTransactionWithMap(types.TxTypeEthereumBlob, map[types.TxValueKeyType]interface{}{ + types.TxValueKeyNonce: nonce, types.TxValueKeyTo: crypto.PubkeyToAddress(keys[0].PublicKey), types.TxValueKeyAmount: big.NewInt(0), types.TxValueKeyGasLimit: uint64(10000000), @@ -320,18 +337,13 @@ func prepareBlobTxMsg(t *testing.T, mockCtrl *gomock.Controller) (*ProtocolManag types.TxValueKeyData: []byte{}, types.TxValueKeyAccessList: types.AccessList{}, types.TxValueKeyBlobFeeCap: big.NewInt(25), - types.TxValueKeyBlobHashes: []common.Hash{common.Hash(kzg4844.CalcBlobHashV1(sha256.New(), &commitment))}, - types.TxValueKeySidecar: &types.BlobTxSidecar{ - Version: types.BlobSidecarVersion1, - Blobs: []kzg4844.Blob{blob}, - Commitments: []kzg4844.Commitment{commitment}, - Proofs: proofs, - }, - types.TxValueKeyChainID: params.TestChainConfig.ChainID, + types.TxValueKeyBlobHashes: hashes, + types.TxValueKeySidecar: sidecar, + types.TxValueKeyChainID: params.TestChainConfig.ChainID, }) require.NoError(t, err) - require.NoError(t, blobTx.Sign(types.MakeSigner(params.TestChainConfig, common.Big0), keys[0])) - return pm, mockPeer, blobTx + require.NoError(t, tx.Sign(types.MakeSigner(params.TestChainConfig, common.Big0), keys[0])) + return tx } func TestHandleTxMsg_KZGVerificationError(t *testing.T) { @@ -353,12 +365,51 @@ func TestHandleTxMsg_BlobSidecarVerifiedOnce(t *testing.T) { pm, mockPeer, blobTx := prepareBlobTxMsg(t, mockCtrl) require.NoError(t, handleTxMsg(pm, mockPeer, generateMsg(t, TxMsg, types.Transactions{blobTx}))) - require.True(t, pm.verifiedBlobTxs.Contains(blobTx.Hash())) + require.Equal(t, 1, pm.verifiedBlobTxs.Len()) + + // The same sidecar under a different transaction hash must reuse the entry, or a + // sender replays one sidecar for free by bumping the nonce. + replay := newBlobTx(t, 1, blobTx.BlobHashes(), blobTx.BlobTxSidecar()) + require.NotEqual(t, blobTx.Hash(), replay.Hash()) + require.NoError(t, handleTxMsg(pm, mockPeer, generateMsg(t, TxMsg, types.Transactions{replay}))) + assert.Equal(t, 1, pm.verifiedBlobTxs.Len(), "the replay was verified again") + + // A sidecar swapped under an already verified transaction hash must not reuse the + // entry, whichever half of the proof relation was altered. + for name, tamper := range map[string]func(*types.BlobTxSidecar){ + "blob": func(sc *types.BlobTxSidecar) { sc.Blobs[0][0] ^= 0xFF }, + "proof": func(sc *types.BlobTxSidecar) { sc.Proofs[0][0] ^= 0xFF }, + "version": func(sc *types.BlobTxSidecar) { sc.Version = 0 }, + } { + t.Run(name, func(t *testing.T) { + pm, mockPeer, blobTx := prepareBlobTxMsg(t, mockCtrl) + require.NoError(t, handleTxMsg(pm, mockPeer, generateMsg(t, TxMsg, types.Transactions{blobTx}))) + + tamper(blobTx.BlobTxSidecar()) + err := handleTxMsg(pm, mockPeer, generateMsg(t, TxMsg, types.Transactions{blobTx})) + require.Error(t, err) + assert.Contains(t, err.Error(), errKZGVerificationError.Error()) + }) + } +} - // The tx hash does not cover the sidecar, so the replay carries a broken proof - // under the same hash and passes only because the verification is skipped. - blobTx.BlobTxSidecar().Proofs[0][0] ^= 0xFF - assert.NoError(t, handleTxMsg(pm, mockPeer, generateMsg(t, TxMsg, types.Transactions{blobTx}))) +func TestHandleTxMsg_BloblessBlobTx(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + pm, mockPeer, _ := prepareBlobTxMsg(t, mockCtrl) + + // Every check in ValidateWithBlobHashes compares against the declared hashes, so + // it passes vacuously when there are none. The pool rejects such a transaction + // unconditionally, so the handler must not forward it. + sidecar, _ := newBlobSidecar(t) + sidecar.Blobs, sidecar.Commitments, sidecar.Proofs = nil, nil, nil + blobless := newBlobTx(t, 0, nil, sidecar) + require.NoError(t, sidecar.ValidateWithBlobHashes(nil), "expected the vacuous pass this guards") + + err := handleTxMsg(pm, mockPeer, generateMsg(t, TxMsg, types.Transactions{blobless})) + require.Error(t, err) + assert.Contains(t, err.Error(), errBloblessBlobTx.Error()) } func prepareTestHandleBlockHeaderFetchRequestMsg(t *testing.T) (*gomock.Controller, *MockPeer, *mocks.MockBlockChain, *ProtocolManager) { From afa66aa33007da6f3addd0dd3400945426f15b7e Mon Sep 17 00:00:00 2001 From: Hyunsoo Shin Date: Wed, 12 Aug 2026 10:10:27 +0900 Subject: [PATCH 3/3] node/cn: bind the whole verification input into the sidecar key The key left out the commitments, on the grounds that each blob hash is the sha256 of one. That binding is enforced by ValidateBlobCommitmentHashes, which is exactly what a cache hit skips, so a commitment could be swapped under an already verified sidecar: the key still matched, the verification never ran, and the peer stayed connected where it is otherwise dropped. The elements are also fixed-size and were concatenated without their counts, so sidecars of different shapes encode to the same bytes and share a key the same way. Add the commitments and prefix the four element counts. The key is then an injective encoding of everything ValidateWithBlobHashes reads, so a cache hit means that exact input passed before and the handler rejects what it rejected before this cache existed. Constraint: anything the verification reads has to be in the key, since the key decides whether the verification runs at all Rejected: keep the commitments out and rely on the hashes | the check that ties them together is the one being skipped Rejected: encode with RLP instead of prefixing counts | copies the whole sidecar into a buffer before hashing, on the path this cache exists to keep cheap Confidence: high Scope-risk: narrow Not-tested: the count prefix has no test of its own Co-Authored-By: Claude Opus 5 (1M context) --- node/cn/handler.go | 17 +++++++++++++---- node/cn/handler_msg_test.go | 9 +++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/node/cn/handler.go b/node/cn/handler.go index 12d35f415..8b89cb521 100644 --- a/node/cn/handler.go +++ b/node/cn/handler.go @@ -23,6 +23,7 @@ package cn import ( + "encoding/binary" "encoding/json" "errors" "fmt" @@ -102,17 +103,25 @@ var ( errBloblessBlobTx = errors.New("blobless blob transaction") ) -// blobSidecarKey identifies what ValidateWithBlobHashes consumes. Commitments are left -// out because each hash is the sha256 of one, so the hashes already pin them. +// blobSidecarKey identifies what ValidateWithBlobHashes consumes. The counts are prefixed +// because every element is fixed-size, so an unprefixed concatenation is ambiguous. func blobSidecarKey(hashes []common.Hash, sc *types.BlobTxSidecar) common.Hash { - parts := make([][]byte, 0, len(hashes)+1+len(sc.Blobs)+len(sc.Proofs)) + counts := binary.BigEndian.AppendUint32(nil, uint32(len(hashes))) + counts = binary.BigEndian.AppendUint32(counts, uint32(len(sc.Blobs))) + counts = binary.BigEndian.AppendUint32(counts, uint32(len(sc.Commitments))) + counts = binary.BigEndian.AppendUint32(counts, uint32(len(sc.Proofs))) + + parts := make([][]byte, 0, 2+len(hashes)+len(sc.Blobs)+len(sc.Commitments)+len(sc.Proofs)) + parts = append(parts, counts, []byte{sc.Version}) for i := range hashes { parts = append(parts, hashes[i][:]) } - parts = append(parts, []byte{sc.Version}) for i := range sc.Blobs { parts = append(parts, sc.Blobs[i][:]) } + for i := range sc.Commitments { + parts = append(parts, sc.Commitments[i][:]) + } for i := range sc.Proofs { parts = append(parts, sc.Proofs[i][:]) } diff --git a/node/cn/handler_msg_test.go b/node/cn/handler_msg_test.go index 2aae15410..36dbd36f6 100644 --- a/node/cn/handler_msg_test.go +++ b/node/cn/handler_msg_test.go @@ -375,11 +375,12 @@ func TestHandleTxMsg_BlobSidecarVerifiedOnce(t *testing.T) { assert.Equal(t, 1, pm.verifiedBlobTxs.Len(), "the replay was verified again") // A sidecar swapped under an already verified transaction hash must not reuse the - // entry, whichever half of the proof relation was altered. + // entry, whichever input of the verification was altered. for name, tamper := range map[string]func(*types.BlobTxSidecar){ - "blob": func(sc *types.BlobTxSidecar) { sc.Blobs[0][0] ^= 0xFF }, - "proof": func(sc *types.BlobTxSidecar) { sc.Proofs[0][0] ^= 0xFF }, - "version": func(sc *types.BlobTxSidecar) { sc.Version = 0 }, + "blob": func(sc *types.BlobTxSidecar) { sc.Blobs[0][0] ^= 0xFF }, + "proof": func(sc *types.BlobTxSidecar) { sc.Proofs[0][0] ^= 0xFF }, + "version": func(sc *types.BlobTxSidecar) { sc.Version = 0 }, + "commitment": func(sc *types.BlobTxSidecar) { sc.Commitments[0][0] ^= 0xFF }, } { t.Run(name, func(t *testing.T) { pm, mockPeer, blobTx := prepareBlobTxMsg(t, mockCtrl)