Skip to content

Commit d2bbde2

Browse files
jwasingerfjl
andauthored
eth: check blob transaction validity on the peer goroutine when received (#31219)
This ensures that if we receive a blob transaction announcement where we cannot link the tx to the sidecar commitments, we will drop the sending peer. This check is added in the protocol handler for the PooledTransactions message. Tests for this have also been added in the cross-client "eth" protocol test suite. --------- Co-authored-by: Felix Lange <[email protected]>
1 parent ebc3232 commit d2bbde2

File tree

5 files changed

+234
-12
lines changed

5 files changed

+234
-12
lines changed

cmd/devp2p/internal/ethtest/conn.go

+5
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,16 @@ func (c *Conn) Write(proto Proto, code uint64, msg any) error {
130130
return err
131131
}
132132

133+
var errDisc error = fmt.Errorf("disconnect")
134+
133135
// ReadEth reads an Eth sub-protocol wire message.
134136
func (c *Conn) ReadEth() (any, error) {
135137
c.SetReadDeadline(time.Now().Add(timeout))
136138
for {
137139
code, data, _, err := c.Conn.Read()
140+
if code == discMsg {
141+
return nil, errDisc
142+
}
138143
if err != nil {
139144
return nil, err
140145
}

cmd/devp2p/internal/ethtest/suite.go

+197
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
package ethtest
1818

1919
import (
20+
"context"
2021
"crypto/rand"
22+
"fmt"
2123
"reflect"
24+
"sync"
25+
"time"
2226

2327
"github.com/ethereum/go-ethereum/common"
2428
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
@@ -79,6 +83,8 @@ func (s *Suite) EthTests() []utesting.Test {
7983
{Name: "InvalidTxs", Fn: s.TestInvalidTxs},
8084
{Name: "NewPooledTxs", Fn: s.TestNewPooledTxs},
8185
{Name: "BlobViolations", Fn: s.TestBlobViolations},
86+
{Name: "TestBlobTxWithoutSidecar", Fn: s.TestBlobTxWithoutSidecar},
87+
{Name: "TestBlobTxWithMismatchedSidecar", Fn: s.TestBlobTxWithMismatchedSidecar},
8288
}
8389
}
8490

@@ -825,3 +831,194 @@ func (s *Suite) TestBlobViolations(t *utesting.T) {
825831
conn.Close()
826832
}
827833
}
834+
835+
// mangleSidecar returns a copy of the given blob transaction where the sidecar
836+
// data has been modified to produce a different commitment hash.
837+
func mangleSidecar(tx *types.Transaction) *types.Transaction {
838+
sidecar := tx.BlobTxSidecar()
839+
copy := types.BlobTxSidecar{
840+
Blobs: append([]kzg4844.Blob{}, sidecar.Blobs...),
841+
Commitments: append([]kzg4844.Commitment{}, sidecar.Commitments...),
842+
Proofs: append([]kzg4844.Proof{}, sidecar.Proofs...),
843+
}
844+
// zero the first commitment to alter the sidecar hash
845+
copy.Commitments[0] = kzg4844.Commitment{}
846+
return tx.WithBlobTxSidecar(&copy)
847+
}
848+
849+
func (s *Suite) TestBlobTxWithoutSidecar(t *utesting.T) {
850+
t.Log(`This test checks that a blob transaction first advertised/transmitted without blobs will result in the sending peer being disconnected, and the full transaction should be successfully retrieved from another peer.`)
851+
tx := s.makeBlobTxs(1, 2, 42)[0]
852+
badTx := tx.WithoutBlobTxSidecar()
853+
s.testBadBlobTx(t, tx, badTx)
854+
}
855+
856+
func (s *Suite) TestBlobTxWithMismatchedSidecar(t *utesting.T) {
857+
t.Log(`This test checks that a blob transaction first advertised/transmitted without blobs, whose commitment don't correspond to the blob_versioned_hashes in the transaction, will result in the sending peer being disconnected, and the full transaction should be successfully retrieved from another peer.`)
858+
tx := s.makeBlobTxs(1, 2, 43)[0]
859+
badTx := mangleSidecar(tx)
860+
s.testBadBlobTx(t, tx, badTx)
861+
}
862+
863+
// readUntil reads eth protocol messages until a message of the target type is
864+
// received. It returns an error if there is a disconnect, or if the context
865+
// is cancelled before a message of the desired type can be read.
866+
func readUntil[T any](ctx context.Context, conn *Conn) (*T, error) {
867+
for {
868+
select {
869+
case <-ctx.Done():
870+
return nil, context.Canceled
871+
default:
872+
}
873+
received, err := conn.ReadEth()
874+
if err != nil {
875+
if err == errDisc {
876+
return nil, errDisc
877+
}
878+
continue
879+
}
880+
881+
switch res := received.(type) {
882+
case *T:
883+
return res, nil
884+
}
885+
}
886+
}
887+
888+
// readUntilDisconnect reads eth protocol messages until the peer disconnects.
889+
// It returns whether the peer disconnects in the next 100ms.
890+
func readUntilDisconnect(conn *Conn) (disconnected bool) {
891+
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
892+
defer cancel()
893+
_, err := readUntil[struct{}](ctx, conn)
894+
return err == errDisc
895+
}
896+
897+
func (s *Suite) testBadBlobTx(t *utesting.T, tx *types.Transaction, badTx *types.Transaction) {
898+
stage1, stage2, stage3 := new(sync.WaitGroup), new(sync.WaitGroup), new(sync.WaitGroup)
899+
stage1.Add(1)
900+
stage2.Add(1)
901+
stage3.Add(1)
902+
903+
errc := make(chan error)
904+
905+
badPeer := func() {
906+
// announce the correct hash from the bad peer.
907+
// when the transaction is first requested before transmitting it from the bad peer,
908+
// trigger step 2: connection and announcement by good peers
909+
910+
conn, err := s.dial()
911+
if err != nil {
912+
errc <- fmt.Errorf("dial fail: %v", err)
913+
return
914+
}
915+
defer conn.Close()
916+
917+
if err := conn.peer(s.chain, nil); err != nil {
918+
errc <- fmt.Errorf("bad peer: peering failed: %v", err)
919+
return
920+
}
921+
922+
ann := eth.NewPooledTransactionHashesPacket{
923+
Types: []byte{types.BlobTxType},
924+
Sizes: []uint32{uint32(badTx.Size())},
925+
Hashes: []common.Hash{badTx.Hash()},
926+
}
927+
928+
if err := conn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil {
929+
errc <- fmt.Errorf("sending announcement failed: %v", err)
930+
return
931+
}
932+
933+
req, err := readUntil[eth.GetPooledTransactionsPacket](context.Background(), conn)
934+
if err != nil {
935+
errc <- fmt.Errorf("failed to read GetPooledTransactions message: %v", err)
936+
return
937+
}
938+
939+
stage1.Done()
940+
stage2.Wait()
941+
942+
// the good peer is connected, and has announced the tx.
943+
// proceed to send the incorrect one from the bad peer.
944+
945+
resp := eth.PooledTransactionsPacket{RequestId: req.RequestId, PooledTransactionsResponse: eth.PooledTransactionsResponse(types.Transactions{badTx})}
946+
if err := conn.Write(ethProto, eth.PooledTransactionsMsg, resp); err != nil {
947+
errc <- fmt.Errorf("writing pooled tx response failed: %v", err)
948+
return
949+
}
950+
if !readUntilDisconnect(conn) {
951+
errc <- fmt.Errorf("expected bad peer to be disconnected")
952+
return
953+
}
954+
stage3.Done()
955+
}
956+
957+
goodPeer := func() {
958+
stage1.Wait()
959+
960+
conn, err := s.dial()
961+
if err != nil {
962+
errc <- fmt.Errorf("dial fail: %v", err)
963+
return
964+
}
965+
defer conn.Close()
966+
967+
if err := conn.peer(s.chain, nil); err != nil {
968+
errc <- fmt.Errorf("peering failed: %v", err)
969+
return
970+
}
971+
972+
ann := eth.NewPooledTransactionHashesPacket{
973+
Types: []byte{types.BlobTxType},
974+
Sizes: []uint32{uint32(tx.Size())},
975+
Hashes: []common.Hash{tx.Hash()},
976+
}
977+
978+
if err := conn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil {
979+
errc <- fmt.Errorf("sending announcement failed: %v", err)
980+
return
981+
}
982+
983+
// wait until the bad peer has transmitted the incorrect transaction
984+
stage2.Done()
985+
stage3.Wait()
986+
987+
// the bad peer has transmitted the bad tx, and been disconnected.
988+
// transmit the same tx but with correct sidecar from the good peer.
989+
990+
var req *eth.GetPooledTransactionsPacket
991+
req, err = readUntil[eth.GetPooledTransactionsPacket](context.Background(), conn)
992+
if err != nil {
993+
errc <- fmt.Errorf("reading pooled tx request failed: %v", err)
994+
return
995+
}
996+
997+
if req.GetPooledTransactionsRequest[0] != tx.Hash() {
998+
errc <- fmt.Errorf("requested unknown tx hash")
999+
return
1000+
}
1001+
1002+
resp := eth.PooledTransactionsPacket{RequestId: req.RequestId, PooledTransactionsResponse: eth.PooledTransactionsResponse(types.Transactions{tx})}
1003+
if err := conn.Write(ethProto, eth.PooledTransactionsMsg, resp); err != nil {
1004+
errc <- fmt.Errorf("writing pooled tx response failed: %v", err)
1005+
return
1006+
}
1007+
if readUntilDisconnect(conn) {
1008+
errc <- fmt.Errorf("unexpected disconnect")
1009+
return
1010+
}
1011+
close(errc)
1012+
}
1013+
1014+
if err := s.engine.sendForkchoiceUpdated(); err != nil {
1015+
t.Fatalf("send fcu failed: %v", err)
1016+
}
1017+
1018+
go goodPeer()
1019+
go badPeer()
1020+
err := <-errc
1021+
if err != nil {
1022+
t.Fatalf("%v", err)
1023+
}
1024+
}

core/txpool/validation.go

+2-12
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package txpool
1818

1919
import (
20-
"crypto/sha256"
2120
"errors"
2221
"fmt"
2322
"math/big"
@@ -170,20 +169,11 @@ func validateBlobSidecar(hashes []common.Hash, sidecar *types.BlobTxSidecar) err
170169
if len(sidecar.Blobs) != len(hashes) {
171170
return fmt.Errorf("invalid number of %d blobs compared to %d blob hashes", len(sidecar.Blobs), len(hashes))
172171
}
173-
if len(sidecar.Commitments) != len(hashes) {
174-
return fmt.Errorf("invalid number of %d blob commitments compared to %d blob hashes", len(sidecar.Commitments), len(hashes))
175-
}
176172
if len(sidecar.Proofs) != len(hashes) {
177173
return fmt.Errorf("invalid number of %d blob proofs compared to %d blob hashes", len(sidecar.Proofs), len(hashes))
178174
}
179-
// Blob quantities match up, validate that the provers match with the
180-
// transaction hash before getting to the cryptography
181-
hasher := sha256.New()
182-
for i, vhash := range hashes {
183-
computed := kzg4844.CalcBlobHashV1(hasher, &sidecar.Commitments[i])
184-
if vhash != computed {
185-
return fmt.Errorf("blob %d: computed hash %#x mismatches transaction one %#x", i, computed, vhash)
186-
}
175+
if err := sidecar.ValidateBlobCommitmentHashes(hashes); err != nil {
176+
return err
187177
}
188178
// Blob commitments match with the hashes in the transaction, verify the
189179
// blobs themselves via KZG

core/types/tx_blob.go

+17
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package types
1919
import (
2020
"bytes"
2121
"crypto/sha256"
22+
"fmt"
2223
"math/big"
2324

2425
"github.com/ethereum/go-ethereum/common"
@@ -85,6 +86,22 @@ func (sc *BlobTxSidecar) encodedSize() uint64 {
8586
return rlp.ListSize(blobs) + rlp.ListSize(commitments) + rlp.ListSize(proofs)
8687
}
8788

89+
// ValidateBlobCommitmentHashes checks whether the given hashes correspond to the
90+
// commitments in the sidecar
91+
func (sc *BlobTxSidecar) ValidateBlobCommitmentHashes(hashes []common.Hash) error {
92+
if len(sc.Commitments) != len(hashes) {
93+
return fmt.Errorf("invalid number of %d blob commitments compared to %d blob hashes", len(sc.Commitments), len(hashes))
94+
}
95+
hasher := sha256.New()
96+
for i, vhash := range hashes {
97+
computed := kzg4844.CalcBlobHashV1(hasher, &sc.Commitments[i])
98+
if vhash != computed {
99+
return fmt.Errorf("blob %d: computed hash %#x mismatches transaction one %#x", i, computed, vhash)
100+
}
101+
}
102+
return nil
103+
}
104+
88105
// blobTxWithBlobs is used for encoding of transactions when blobs are present.
89106
type blobTxWithBlobs struct {
90107
BlobTx *BlobTx

eth/handler_eth.go

+13
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ func (h *ethHandler) Handle(peer *eth.Peer, packet eth.Packet) error {
6969
return h.txFetcher.Enqueue(peer.ID(), *packet, false)
7070

7171
case *eth.PooledTransactionsResponse:
72+
// If we receive any blob transactions missing sidecars, or with
73+
// sidecars that don't correspond to the versioned hashes reported
74+
// in the header, disconnect from the sending peer.
75+
for _, tx := range *packet {
76+
if tx.Type() == types.BlobTxType {
77+
if tx.BlobTxSidecar() == nil {
78+
return errors.New("received sidecar-less blob transaction")
79+
}
80+
if err := tx.BlobTxSidecar().ValidateBlobCommitmentHashes(tx.BlobHashes()); err != nil {
81+
return err
82+
}
83+
}
84+
}
7285
return h.txFetcher.Enqueue(peer.ID(), *packet, true)
7386

7487
default:

0 commit comments

Comments
 (0)