From 86df16d6e62470f610423b49947a13a295f486d0 Mon Sep 17 00:00:00 2001 From: healthykim Date: Thu, 4 Sep 2025 19:29:37 +0900 Subject: [PATCH] add: validation logic with new type BlobTxCellSidecar --- core/txpool/blobpool/blobpool.go | 24 ++++++-- core/txpool/blobpool/blobpool_test.go | 6 +- core/txpool/validation.go | 44 +++++++------- core/types/custody_bitmap.go | 85 +++++++++++++++++++++++++++ core/types/tx_blob.go | 38 ++++++++++++ 5 files changed, 166 insertions(+), 31 deletions(-) create mode 100644 core/types/custody_bitmap.go diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 55d24c7a93..d92cfc84dc 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1164,7 +1164,15 @@ func (p *BlobPool) checkDelegationLimit(tx *types.Transaction) error { // validateTx checks whether a transaction is valid according to the consensus // rules and adheres to some heuristic limits of the local node (price and size). -func (p *BlobPool) validateTx(tx *types.Transaction) error { +func (p *BlobPool) validateTx(tx *types.Transaction, cellSidecars *types.BlobTxCellSidecar) error { + + if err := txpool.ValidateBlobSidecar(tx, cellSidecars, p.head, &txpool.ValidationOptions{ + Config: p.chain.Config(), + MaxBlobCount: maxBlobsPerTx, + }); err != nil { + return err + } + if err := p.ValidateTxBasics(tx); err != nil { return err } @@ -1443,7 +1451,11 @@ func (p *BlobPool) Add(txs []*types.Transaction, sync bool) []error { adds = make([]*types.Transaction, 0, len(txs)) ) for i, tx := range txs { - errs[i] = p.add(tx) + if errs[i] != nil { + continue + } + cellSidecar := tx.BlobTxSidecar().ToBlobTxCellSidecar() + errs[i] = p.add(tx.WithoutBlobTxSidecar(), cellSidecar) if errs[i] == nil { adds = append(adds, tx.WithoutBlobTxSidecar()) } @@ -1457,7 +1469,7 @@ func (p *BlobPool) Add(txs []*types.Transaction, sync bool) []error { // add inserts a new blob transaction into the pool if it passes validation (both // consensus validity and pool restrictions). -func (p *BlobPool) add(tx *types.Transaction) (err error) { +func (p *BlobPool) add(tx *types.Transaction, cellSidecar *types.BlobTxCellSidecar) (err error) { // The blob pool blocks on adding a transaction. This is because blob txs are // only even pulled from the network, so this method will act as the overload // protection for fetches. @@ -1471,7 +1483,7 @@ func (p *BlobPool) add(tx *types.Transaction) (err error) { }(time.Now()) // Ensure the transaction is valid from all perspectives - if err := p.validateTx(tx); err != nil { + if err := p.validateTx(tx, cellSidecar); err != nil { log.Trace("Transaction validation failed", "hash", tx.Hash(), "err", err) switch { case errors.Is(err, txpool.ErrUnderpriced): @@ -1513,6 +1525,10 @@ func (p *BlobPool) add(tx *types.Transaction) (err error) { } }() } + + //todo(healthykim) remove this and seperate database or introduce list + blobSidecar := cellSidecar.ToBlobTxSidecar() + tx = tx.WithBlobTxSidecar(blobSidecar) // Transaction permitted into the pool from a nonce and cost perspective, // insert it into the database and update the indices blob, err := rlp.EncodeToBytes(tx) diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index e46529a241..1016461080 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -1323,7 +1323,7 @@ func TestBlobCountLimit(t *testing.T) { // Check that first succeeds second fails. if errs[0] != nil { - t.Fatalf("expected tx with 7 blobs to succeed") + t.Fatalf("expected tx with 7 blobs to succeed, got: %v", errs[0]) } if !errors.Is(errs[1], txpool.ErrTxBlobLimitExceeded) { t.Fatalf("expected tx with 8 blobs to fail, got: %v", errs[1]) @@ -1752,7 +1752,7 @@ func TestAdd(t *testing.T) { // Add each transaction one by one, verifying the pool internals in between for j, add := range tt.adds { signed, _ := types.SignNewTx(keys[add.from], types.LatestSigner(params.MainnetChainConfig), add.tx) - if err := pool.add(signed); !errors.Is(err, add.err) { + if err := pool.add(signed.WithoutBlobTxSidecar(), signed.BlobTxSidecar().ToBlobTxCellSidecar()); !errors.Is(err, add.err) { t.Errorf("test %d, tx %d: adding transaction error mismatch: have %v, want %v", i, j, err, add.err) } if add.err == nil { @@ -2124,7 +2124,7 @@ func benchmarkPoolPending(b *testing.B, datacap uint64) { b.Fatal(err) } statedb.AddBalance(addr, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified) - pool.add(tx) + pool.add(tx.WithoutBlobTxSidecar(), tx.BlobTxSidecar().ToBlobTxCellSidecar()) } statedb.Commit(0, true, false) defer pool.Close() diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 46974fad3c..a594413771 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -63,9 +63,6 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types if opts.Accept&(1< opts.MaxBlobCount { - return fmt.Errorf("%w: blob count %v, limit %v", ErrTxBlobLimitExceeded, blobCount, opts.MaxBlobCount) - } // Before performing any expensive validations, sanity check that the tx is // smaller than the maximum limit the pool can meaningfully handle if tx.Size() > opts.MaxSize { @@ -139,9 +136,6 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types if tx.GasTipCapIntCmp(opts.MinTip) < 0 { return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.GasTipCap(), opts.MinTip) } - if tx.Type() == types.BlobTxType { - return validateBlobTx(tx, head, opts) - } if tx.Type() == types.SetCodeTxType { if len(tx.SetCodeAuthorizations()) == 0 { return errors.New("set code tx must have at least one authorization tuple") @@ -150,28 +144,26 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types return nil } -// validateBlobTx implements the blob-transaction specific validations. -func validateBlobTx(tx *types.Transaction, head *types.Header, opts *ValidationOptions) error { - sidecar := tx.BlobTxSidecar() - if sidecar == nil { - return errors.New("missing sidecar in blob transaction") - } +func ValidateBlobSidecar(tx *types.Transaction, sidecar *types.BlobTxCellSidecar, head *types.Header, opts *ValidationOptions) error { // Ensure the blob fee cap satisfies the minimum blob gas price if tx.BlobGasFeeCapIntCmp(blobTxMinBlobGasPrice) < 0 { return fmt.Errorf("%w: blob fee cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.BlobGasFeeCap(), blobTxMinBlobGasPrice) } - // Ensure the number of items in the blob transaction and various side - // data match up before doing any expensive validations + // Verify whether the blob count is consistent with other parts of the sidecar and the transaction + blobCount := len(sidecar.Cells) / sidecar.CellIndices.OneCount() hashes := tx.BlobHashes() - if len(hashes) == 0 { + if blobCount == 0 { return errors.New("blobless blob transaction") } - if len(hashes) > params.BlobTxMaxBlobs { - return fmt.Errorf("too many blobs in transaction: have %d, permitted %d", len(hashes), params.BlobTxMaxBlobs) + if blobCount != len(sidecar.Commitments) || blobCount != len(hashes) { + return fmt.Errorf("invalid number of %d blobs compared to %d commitments and %d blob hashes", blobCount, len(sidecar.Commitments), len(tx.BlobHashes())) } - if len(sidecar.Blobs) != len(hashes) { - return fmt.Errorf("invalid number of %d blobs compared to %d blob hashes", len(sidecar.Blobs), len(hashes)) + + // Check whether the blob count does not exceed the max blob count + if blobCount > opts.MaxBlobCount { + return fmt.Errorf("%w: blob count %v, limit %v", ErrTxBlobLimitExceeded, blobCount, opts.MaxBlobCount) } + if err := sidecar.ValidateBlobCommitmentHashes(hashes); err != nil { return err } @@ -182,29 +174,33 @@ func validateBlobTx(tx *types.Transaction, head *types.Header, opts *ValidationO return validateBlobSidecarLegacy(sidecar, hashes) } -func validateBlobSidecarLegacy(sidecar *types.BlobTxSidecar, hashes []common.Hash) error { +func validateBlobSidecarLegacy(sidecar *types.BlobTxCellSidecar, hashes []common.Hash) error { if sidecar.Version != types.BlobSidecarVersion0 { return fmt.Errorf("invalid sidecar version pre-osaka: %v", sidecar.Version) } if len(sidecar.Proofs) != len(hashes) { return fmt.Errorf("invalid number of %d blob proofs expected %d", len(sidecar.Proofs), len(hashes)) } - for i := range sidecar.Blobs { - if err := kzg4844.VerifyBlobProof(&sidecar.Blobs[i], sidecar.Commitments[i], sidecar.Proofs[i]); err != nil { + blobs, err := kzg4844.RecoverBlobs(sidecar.Cells, sidecar.CellIndices.Indices()) + if err != nil { + return err + } + for i := range blobs { + if err := kzg4844.VerifyBlobProof(&blobs[i], sidecar.Commitments[i], sidecar.Proofs[i]); err != nil { return fmt.Errorf("invalid blob %d: %v", i, err) } } return nil } -func validateBlobSidecarOsaka(sidecar *types.BlobTxSidecar, hashes []common.Hash) error { +func validateBlobSidecarOsaka(sidecar *types.BlobTxCellSidecar, hashes []common.Hash) error { if sidecar.Version != types.BlobSidecarVersion1 { return fmt.Errorf("invalid sidecar version post-osaka: %v", sidecar.Version) } if len(sidecar.Proofs) != len(hashes)*kzg4844.CellProofsPerBlob { return fmt.Errorf("invalid number of %d blob proofs expected %d", len(sidecar.Proofs), len(hashes)*kzg4844.CellProofsPerBlob) } - return kzg4844.VerifyCellProofs(sidecar.Blobs, sidecar.Commitments, sidecar.Proofs) + return kzg4844.VerifyCells(sidecar.Cells, sidecar.Commitments, sidecar.Proofs, sidecar.CellIndices.Indices()) } // ValidationOptionsWithState define certain differences between stateful transaction diff --git a/core/types/custody_bitmap.go b/core/types/custody_bitmap.go new file mode 100644 index 0000000000..d7a0606c21 --- /dev/null +++ b/core/types/custody_bitmap.go @@ -0,0 +1,85 @@ +package types + +import ( + "errors" + "math/bits" +) + +// `CustodyBitmap` is a bitmap to represent which custody index to store (little endian) +type CustodyBitmap [16]byte + +const CustodySize = 128 + +func (b CustodyBitmap) IsSet(i uint) bool { + if i >= CustodySize { + return false + } + index := i / 8 + offset := i % 8 + return ((b[index] >> offset) & 1) == 1 +} + +// Set ith bit +func (b *CustodyBitmap) Set(i uint) error { + if i >= CustodySize { + return errors.New("bit index out of range") + } + index := i / 8 + offset := i % 8 + b[index] |= 1 << offset + return nil +} + +// Clear ith bit +func (b *CustodyBitmap) Clear(i uint) error { + if i >= CustodySize { + return errors.New("bit index out of range") + } + index := i / 8 + offset := i % 8 + b[index] &^= 1 << offset + return nil +} + +// Number of bits set to 1 +func (b CustodyBitmap) OneCount() int { + total := 0 + for _, byte := range b { + total += bits.OnesCount8(uint8(byte)) + } + return total +} + +// Return bit indices set to 1, ascending order +func (b CustodyBitmap) Indices() []uint64 { + out := make([]uint64, 0, b.OneCount()) + for byteIdx, val := range b { + v := val + for v != 0 { + tz := bits.TrailingZeros8(uint8(v)) // 0..7 + idx := uint64(byteIdx*8 + tz) + out = append(out, idx) + v &^= 1 << tz + } + } + return out +} + +func (b CustodyBitmap) SetIndices(indices []uint64) error { + for _, i := range indices { + if i >= CustodySize { + return errors.New("bit index out of range") + } + byteIdx := i / 8 + bitOff := i % 8 + b[byteIdx] |= 1 << bitOff + } + return nil +} + +func (b CustodyBitmap) SetAll() CustodyBitmap { + for i := 0; i < len(b); i++ { + b[i] = 0xFF + } + return b +} diff --git a/core/types/tx_blob.go b/core/types/tx_blob.go index bbfd3c98db..8242cd6146 100644 --- a/core/types/tx_blob.go +++ b/core/types/tx_blob.go @@ -176,6 +176,44 @@ func (sc *BlobTxSidecar) Copy() *BlobTxSidecar { } } +func (sc *BlobTxSidecar) ToBlobTxCellSidecar() *BlobTxCellSidecar { + cells, _ := kzg4844.ComputeCells(sc.Blobs) + return &BlobTxCellSidecar{ + Version: sc.Version, + Cells: cells, + Commitments: sc.Commitments, + Proofs: sc.Proofs, + CellIndices: CustodyBitmap{}.SetAll(), + } +} + +type BlobTxCellSidecar struct { + Version byte + Cells []kzg4844.Cell + Commitments []kzg4844.Commitment + Proofs []kzg4844.Proof + CellIndices CustodyBitmap +} + +func (c *BlobTxCellSidecar) ToBlobTxSidecar() *BlobTxSidecar { + blobs, _ := kzg4844.RecoverBlobs(c.Cells, c.CellIndices.Indices()) + return &BlobTxSidecar{ + Version: c.Version, + Blobs: blobs, + Commitments: c.Commitments, + Proofs: c.Proofs, + } +} + +// ValidateBlobCommitmentHashes checks whether the given hashes correspond to the +// commitments in the sidecar +func (c *BlobTxCellSidecar) ValidateBlobCommitmentHashes(hashes []common.Hash) error { + sc := BlobTxSidecar{ + Commitments: c.Commitments, + } + return sc.ValidateBlobCommitmentHashes(hashes) +} + // blobTxWithBlobs represents blob tx with its corresponding sidecar. // This is an interface because sidecars are versioned. type blobTxWithBlobs interface {