Skip to content

Commit 6b6363c

Browse files
committed
Add early validation of bundles
1 parent 8e5ff63 commit 6b6363c

3 files changed

Lines changed: 196 additions & 5 deletions

File tree

evmcore/tx_pool.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ var (
106106
// ErrSponsoredTransactionsDisabled is returned when validating a sponsorship
107107
// request if gas subsidies are disabled in the current network rules.
108108
ErrSponsoredTransactionsDisabled = errors.New("sponsored transactions are disabled")
109+
110+
// ErrBundleTransactionsDisabled is returned when validating a transaction
111+
// bundle if transaction bundles are disabled in the current network rules.
112+
ErrBundleTransactionsDisabled = errors.New("bundled transactions are disabled")
113+
114+
// ErrBundleTransactionInvalid is returned when a bundle envelope is ill-formed
115+
ErrBundleTransactionInvalid = errors.New("invalid bundle transaction")
109116
)
110117

111118
var (
@@ -711,8 +718,9 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
711718
eip7623: pool.eip7623,
712719
eip7702: pool.eip7702,
713720

714-
gasSubsidies: pool.chain.CurrentRules().Upgrades.GasSubsidies,
715-
brio: pool.chain.CurrentRules().Upgrades.Brio,
721+
gasSubsidies: pool.chain.CurrentRules().Upgrades.GasSubsidies,
722+
brio: pool.chain.CurrentRules().Upgrades.Brio,
723+
transactionBundles: pool.chain.CurrentRules().Upgrades.TransactionBundles,
716724
}
717725

718726
subsidiesChecker := pool.createSubsidiesChecker()

evmcore/tx_validation.go

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
package evmcore
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"math"
2223
"math/big"
2324

25+
"github.com/0xsoniclabs/sonic/gossip/blockproc/bundle"
2426
"github.com/0xsoniclabs/sonic/gossip/blockproc/subsidies"
2527
"github.com/0xsoniclabs/sonic/gossip/gasprice/gaspricelimits"
2628
"github.com/0xsoniclabs/sonic/inter/state"
@@ -59,8 +61,9 @@ type NetworkRules struct {
5961
eip7623 bool // Fork indicator whether we are using EIP-7623 floor gas validation.
6062
eip7702 bool // Fork indicator whether we are using EIP-7702 set code transactions.
6163

62-
gasSubsidies bool // Indicator whether gas subsidies are active.
63-
brio bool // Indicator whether Brio revision is active
64+
brio bool // Indicator whether Brio revision is active
65+
gasSubsidies bool // Indicator whether gas subsidies are active.
66+
transactionBundles bool // Indicator whether transaction bundles are active.
6467
}
6568

6669
// Signer wraps types.Signer to allow mocking it in tests.
@@ -107,6 +110,10 @@ func validateTx(
107110
return err
108111
}
109112

113+
if err := validateBundleTransactions(tx, netRules, chain, state, signer); err != nil {
114+
return err
115+
}
116+
110117
return nil
111118
}
112119

@@ -251,7 +258,9 @@ func ValidateTxForBlock(tx *types.Transaction, netRules NetworkRules, chain Stat
251258

252259
// Ensure Sonic-specific hard bounds
253260
isSponsorRequest := netRules.gasSubsidies && subsidies.IsSponsorshipRequest(tx)
254-
if baseFee := chain.CurrentBaseFee(); !isSponsorRequest && baseFee != nil {
261+
isBundle := netRules.transactionBundles && bundle.IsEnvelope(tx)
262+
baseFee := chain.CurrentBaseFee()
263+
if !isSponsorRequest && !isBundle {
255264
limit := gaspricelimits.GetMinimumFeeCapForTransactionPool(baseFee)
256265
if tx.GasFeeCapIntCmp(limit) < 0 {
257266
log.Trace("Rejecting underpriced tx: minimumBaseFee", "minimumBaseFee", baseFee, "limit", limit, "tx.GasFeeCap", tx.GasFeeCap())
@@ -348,6 +357,11 @@ func validateSponsoredTransactions(
348357
netRules NetworkRules,
349358
SubsidiesChecker subsidiesChecker,
350359
) error {
360+
// Transaction Bundles are identified as sponsorship requests, but they are
361+
// checked independently.
362+
if bundle.IsEnvelope(tx) {
363+
return nil
364+
}
351365

352366
// No check is conducted if gas subsidies are not active.
353367
if !netRules.gasSubsidies {
@@ -369,3 +383,59 @@ func validateSponsoredTransactions(
369383

370384
return nil
371385
}
386+
387+
// validateBundleTransactions checks if a transaction is a bundle transaction and if so,
388+
// validates the bundle structure and the validity of each transaction in the bundle.
389+
// if the bundle is malformed or any bundle-only transactions is invalid,
390+
// it returns an error rejecting the transaction.
391+
func validateBundleTransactions(
392+
tx *types.Transaction,
393+
netRules NetworkRules,
394+
chainState StateReader,
395+
// Although state can be retrieved from chain, it is passed explicitly to avoid extra db-pool accesses
396+
stateDb state.StateDB,
397+
signer types.Signer,
398+
) error {
399+
return validateBundleTransactionsInternal(
400+
tx,
401+
netRules,
402+
chainState,
403+
stateDb,
404+
signer,
405+
)
406+
}
407+
408+
func validateBundleTransactionsInternal(
409+
tx *types.Transaction,
410+
netRules NetworkRules,
411+
chainState StateReader,
412+
// Although state can be retrieved from chain, it is passed explicitly to avoid extra db-pool accesses
413+
stateDb state.StateDB,
414+
signer types.Signer,
415+
) error {
416+
// This check only covers bundle transactions, ignore the rest.
417+
if !bundle.IsEnvelope(tx) {
418+
return nil
419+
}
420+
421+
// If transaction bundles are not active, reject the transaction.
422+
if !netRules.brio {
423+
return nil
424+
}
425+
if !netRules.transactionBundles {
426+
return ErrBundleTransactionsDisabled
427+
}
428+
429+
// If the transaction is a bundle, validate its structure and content.
430+
_, _, err := bundle.ValidateEnvelope(signer, tx)
431+
if err != nil {
432+
return errors.Join(ErrBundleTransactionInvalid, err)
433+
}
434+
435+
// Check that the bundle is runnable.
436+
// TODO: this requires integration of `GetBundleState`
437+
_ = stateDb
438+
_ = chainState
439+
440+
return nil
441+
}

evmcore/tx_validation_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"math/big"
2323
"testing"
2424

25+
"github.com/0xsoniclabs/sonic/gossip/blockproc/bundle"
2526
"github.com/0xsoniclabs/sonic/inter/state"
2627
"github.com/0xsoniclabs/sonic/opera"
2728
"github.com/ethereum/go-ethereum/common"
@@ -1288,6 +1289,44 @@ func TestValidateTx_RejectsTx_WhenStateValidationFails(t *testing.T) {
12881289
}
12891290
}
12901291

1292+
func TestValidateTx_RejectsTx_WhenBundleTransactionValidationFails(t *testing.T) {
1293+
require := require.New(t)
1294+
ctrl := gomock.NewController(t)
1295+
signer := NewMockSigner(ctrl)
1296+
signer.EXPECT().Sender(gomock.Any()).AnyTimes()
1297+
signer.EXPECT().Equal(gomock.Any()).AnyTimes()
1298+
1299+
chain := NewMockStateReader(ctrl)
1300+
chain.EXPECT().CurrentBaseFee().Return(big.NewInt(5)).AnyTimes()
1301+
chain.EXPECT().CurrentMaxGasLimit().Return(uint64(100_000)).AnyTimes()
1302+
1303+
state := state.NewMockStateDB(ctrl)
1304+
state.EXPECT().GetNonce(gomock.Any()).Return(uint64(0)).AnyTimes()
1305+
state.EXPECT().GetBalance(gomock.Any()).Return(uint256.NewInt(0)).AnyTimes()
1306+
state.EXPECT().GetCode(gomock.Any()).Return(nil).AnyTimes()
1307+
1308+
invalidBundle := types.NewTx(&types.LegacyTx{
1309+
To: &bundle.BundleProcessor,
1310+
Gas: 100_000,
1311+
})
1312+
1313+
require.True(bundle.IsEnvelope(invalidBundle))
1314+
require.ErrorIs(validateTx(
1315+
invalidBundle,
1316+
poolOptions{
1317+
isLocal: true,
1318+
},
1319+
NetworkRules{
1320+
brio: true,
1321+
transactionBundles: true,
1322+
},
1323+
chain,
1324+
state,
1325+
nil,
1326+
signer,
1327+
), ErrBundleTransactionInvalid)
1328+
}
1329+
12911330
func TestValidateTx_AcceptsZeroGasPriceTransactions_WhenSubsidiesAreEnabled(t *testing.T) {
12921331
tests := []types.TxData{
12931332
&types.LegacyTx{
@@ -1440,6 +1479,80 @@ func Test_validateSponsoredTransactions_RejectsSponsoredTransactions(t *testing.
14401479
}
14411480
}
14421481

1482+
func Test_validateBundleTransactions_AcceptNonBundleTransactions(t *testing.T) {
1483+
tests := map[string]*types.Transaction{
1484+
"legacy tx": types.NewTx(&types.LegacyTx{}),
1485+
"access list tx": types.NewTx(&types.AccessListTx{}),
1486+
"dynamic fee tx": types.NewTx(&types.DynamicFeeTx{}),
1487+
"blob tx": types.NewTx(&types.BlobTx{}),
1488+
}
1489+
1490+
for name, tx := range tests {
1491+
t.Run(name, func(t *testing.T) {
1492+
require := require.New(t)
1493+
require.False(bundle.IsEnvelope(tx))
1494+
require.NoError(validateBundleTransactions(tx, NetworkRules{}, nil, nil, nil))
1495+
})
1496+
}
1497+
}
1498+
1499+
func Test_validateBundleTransactions_RespectNetworkRules(t *testing.T) {
1500+
signer := types.LatestSignerForChainID(big.NewInt(1))
1501+
bundle := bundle.NewBuilder(signer).Build()
1502+
1503+
tests := map[string]struct {
1504+
rules NetworkRules
1505+
expectedError error
1506+
}{
1507+
"bundle transactions disabled pre brio": {
1508+
rules: NetworkRules{},
1509+
expectedError: nil,
1510+
},
1511+
"bundle transactions enabled pre brio": {
1512+
rules: NetworkRules{transactionBundles: true},
1513+
expectedError: nil,
1514+
},
1515+
"bundle transactions disabled post brio": {
1516+
rules: NetworkRules{brio: true},
1517+
expectedError: ErrBundleTransactionsDisabled,
1518+
},
1519+
"bundle transactions enabled post brio": {
1520+
rules: NetworkRules{brio: true, transactionBundles: true},
1521+
expectedError: nil,
1522+
},
1523+
}
1524+
1525+
for name, test := range tests {
1526+
t.Run(name, func(t *testing.T) {
1527+
require := require.New(t)
1528+
err := validateBundleTransactions(bundle, test.rules, nil, nil, nil)
1529+
if test.expectedError != nil {
1530+
require.ErrorIs(err, test.expectedError)
1531+
} else {
1532+
require.NoError(err)
1533+
}
1534+
})
1535+
}
1536+
}
1537+
1538+
func Test_validateBundleTransactions_ReturnsErrorWithMalformedEnvelope(t *testing.T) {
1539+
malformedBundle := types.NewTx(&types.LegacyTx{
1540+
To: &bundle.BundleProcessor,
1541+
Gas: 100_000,
1542+
})
1543+
1544+
require := require.New(t)
1545+
require.True(bundle.IsEnvelope(malformedBundle))
1546+
1547+
bundlesEnabled := NetworkRules{
1548+
brio: true,
1549+
transactionBundles: true,
1550+
}
1551+
1552+
err := validateBundleTransactions(malformedBundle, bundlesEnabled, nil, nil, nil)
1553+
require.ErrorIs(err, ErrBundleTransactionInvalid)
1554+
}
1555+
14431556
func TestValidateTx_AllowsSponsoredZeroGasPriceTransactions_WhenSubsidiesAreFunded(t *testing.T) {
14441557

14451558
tests := map[string]struct {

0 commit comments

Comments
 (0)