add: validation logic with new type BlobTxCellSidecar

This commit is contained in:
healthykim 2025-09-04 19:29:37 +09:00
parent da8a204517
commit 86df16d6e6
5 changed files with 166 additions and 31 deletions

View file

@ -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)

View file

@ -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()

View file

@ -63,9 +63,6 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
if opts.Accept&(1<<tx.Type()) == 0 {
return fmt.Errorf("%w: tx type %v not supported by this pool", core.ErrTxTypeNotSupported, tx.Type())
}
if blobCount := len(tx.BlobHashes()); blobCount > 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

View file

@ -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
}

View file

@ -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 {