Skip to content

Commit

Permalink
Implement eth_sendRawTransactionConditional (#330)
Browse files Browse the repository at this point in the history
  • Loading branch information
hamdiallam authored and sebastianst committed Sep 17, 2024
1 parent 02dae7f commit 5f7ebba
Show file tree
Hide file tree
Showing 23 changed files with 1,194 additions and 10 deletions.
2 changes: 2 additions & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ var (
utils.GpoIgnoreGasPriceFlag,
utils.GpoMinSuggestedPriorityFeeFlag,
utils.RollupSequencerHTTPFlag,
utils.RollupSequencerEnableTxConditionalFlag,
utils.RollupSequencerTxConditionalRateLimitFlag,
utils.RollupHistoricalRPCFlag,
utils.RollupHistoricalRPCTimeoutFlag,
utils.RollupDisableTxPoolGossipFlag,
Expand Down
17 changes: 17 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,18 @@ var (
Category: flags.RollupCategory,
Value: true,
}
RollupSequencerEnableTxConditionalFlag = &cli.BoolFlag{
Name: "rollup.sequencerenabletxconditional",
Usage: "Serve the eth_sendRawTransactionConditional endpoint and apply the conditional constraints on mempool inclusion & block building",
Category: flags.RollupCategory,
Value: false,
}
RollupSequencerTxConditionalRateLimitFlag = &cli.IntFlag{
Name: "rollup.sequencertxconditionalratelimit",
Usage: "Maximum cost -- storage lookups -- allowed for conditional transactions in a given second",
Category: flags.RollupCategory,
Value: 5000,
}

// Metrics flags
MetricsEnabledFlag = &cli.BoolFlag{
Expand Down Expand Up @@ -1717,6 +1729,9 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) {
if ctx.IsSet(RollupComputePendingBlock.Name) {
cfg.RollupComputePendingBlock = ctx.Bool(RollupComputePendingBlock.Name)
}

// This flag has a default rate limit so always set
cfg.RollupTransactionConditionalRateLimit = ctx.Int(RollupSequencerTxConditionalRateLimitFlag.Name)
}

func setRequiredBlocks(ctx *cli.Context, cfg *ethconfig.Config) {
Expand Down Expand Up @@ -1955,6 +1970,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
cfg.RollupDisableTxPoolAdmission = cfg.RollupSequencerHTTP != "" && !ctx.Bool(RollupEnableTxPoolAdmissionFlag.Name)
cfg.RollupHaltOnIncompatibleProtocolVersion = ctx.String(RollupHaltOnIncompatibleProtocolVersionFlag.Name)
cfg.ApplySuperchainUpgrades = ctx.Bool(RollupSuperchainUpgradesFlag.Name)
cfg.RollupSequencerEnableTxConditional = ctx.Bool(RollupSequencerEnableTxConditionalFlag.Name)

// Override any default configs for hard coded networks.
switch {
case ctx.Bool(MainnetFlag.Name):
Expand Down
34 changes: 34 additions & 0 deletions core/state/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,40 @@ func (s *StateDB) HasSelfDestructed(addr common.Address) bool {
return false
}

// CheckTransactionConditional validates the account preconditions against the statedb.
//
// NOTE: A lock is not held on the db while the conditional is checked. The caller must
// ensure no state changes occur while this check is executed.
func (s *StateDB) CheckTransactionConditional(cond *types.TransactionConditional) error {
cost := cond.Cost()

// The max cost is an inclusive limit.
if cost > params.TransactionConditionalMaxCost {
return fmt.Errorf("conditional cost, %d, exceeded max: %d", cost, params.TransactionConditionalMaxCost)
}

for addr, acct := range cond.KnownAccounts {
if root, isRoot := acct.Root(); isRoot {
storageRoot := s.GetStorageRoot(addr)
if storageRoot == (common.Hash{}) { // if the root is not found, replace with the empty root hash
storageRoot = types.EmptyRootHash
}
if root != storageRoot {
return fmt.Errorf("failed account storage root constraint. Got %s, Expected %s", storageRoot, root)
}
}
if slots, isSlots := acct.Slots(); isSlots {
for key, state := range slots {
accState := s.GetState(addr, key)
if state != accState {
return fmt.Errorf("failed account storage slot key %s constraint. Got %s, Expected %s", key, accState, state)
}
}
}
}
return nil
}

/*
* SETTERS
*/
Expand Down
218 changes: 218 additions & 0 deletions core/state/statedb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1373,3 +1373,221 @@ func TestStorageDirtiness(t *testing.T) {
state.RevertToSnapshot(snap)
checkDirty(common.Hash{0x1}, common.Hash{0x1}, true)
}

func TestCheckTransactionConditional(t *testing.T) {
type preAction struct {
Account common.Address
Slots map[common.Hash]common.Hash
}

tests := []struct {
name string
preActions []preAction
cond types.TransactionConditional
valid bool
}{
{
// Clean Prestate, no defined cond
name: "clean prestate",
preActions: []preAction{},
cond: types.TransactionConditional{},
valid: true,
},
{
// Prestate:
// - address(1)
// - bytes32(0): bytes32(1)
// cond:
// - address(1)
// - bytes32(0): bytes32(1)
name: "matching storage slots",
preActions: []preAction{
{
Account: common.Address{19: 1},
Slots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 1},
},
},
},
cond: types.TransactionConditional{
KnownAccounts: map[common.Address]types.KnownAccount{
common.Address{19: 1}: types.KnownAccount{
StorageSlots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 1},
},
},
},
},
valid: true,
},
{
// Prestate:
// - address(1)
// - bytes32(0): bytes32(1)
// cond:
// - address(1)
// - bytes32(0): bytes32(2)
name: "mismatched storage slots",
preActions: []preAction{
{
Account: common.Address{19: 1},
Slots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 1},
},
},
},
cond: types.TransactionConditional{
KnownAccounts: map[common.Address]types.KnownAccount{
common.Address{19: 1}: types.KnownAccount{
StorageSlots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 2},
},
},
},
},
valid: false,
},
{
// Clean Prestate
// cond:
// - address(1)
// - emptyRoot
name: "matching storage root",
preActions: []preAction{},
cond: types.TransactionConditional{
KnownAccounts: map[common.Address]types.KnownAccount{
common.Address{19: 1}: types.KnownAccount{
StorageRoot: &types.EmptyRootHash,
},
},
},
valid: true,
},
{
// Prestate:
// - address(1)
// - bytes32(0): bytes32(1)
// cond:
// - address(1)
// - emptyRoot
name: "mismatched storage root",
preActions: []preAction{
{
Account: common.Address{19: 1},
Slots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 1},
},
},
},
cond: types.TransactionConditional{
KnownAccounts: map[common.Address]types.KnownAccount{
common.Address{19: 1}: types.KnownAccount{
StorageRoot: &types.EmptyRootHash,
},
},
},
valid: false,
},
{
// Prestate:
// - address(1)
// - bytes32(0): bytes32(1)
// - address(2)
// - bytes32(0): bytes32(2)
// cond:
// - address(1)
// - bytes32(0): bytes32(1)
// - address(2)
// - bytes32(0): bytes32(2)
name: "multiple matching",
preActions: []preAction{
{
Account: common.Address{19: 1},
Slots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 1},
},
},
{
Account: common.Address{19: 2},
Slots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 2},
},
},
},
cond: types.TransactionConditional{
KnownAccounts: map[common.Address]types.KnownAccount{
common.Address{19: 1}: types.KnownAccount{
StorageSlots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 1},
},
},
common.Address{19: 2}: types.KnownAccount{
StorageSlots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 2},
},
},
},
},
valid: true,
},
{
// Prestate:
// - address(1)
// - bytes32(0): bytes32(1)
// - address(2)
// - bytes32(0): bytes32(3)
// cond:
// - address(1)
// - bytes32(0): bytes32(1)
// - address(2)
// - bytes32(0): bytes32(2)
name: "multiple mismatch single",
preActions: []preAction{
{
Account: common.Address{19: 1},
Slots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 1},
},
},
{
Account: common.Address{19: 2},
Slots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 3},
},
},
},
cond: types.TransactionConditional{
KnownAccounts: map[common.Address]types.KnownAccount{
common.Address{19: 1}: types.KnownAccount{
StorageSlots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 1},
},
},
common.Address{19: 2}: types.KnownAccount{
StorageSlots: map[common.Hash]common.Hash{
common.Hash{}: common.Hash{31: 2},
},
},
},
},
valid: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
state, _ := New(types.EmptyRootHash, NewDatabase(rawdb.NewMemoryDatabase()), nil)
for _, action := range test.preActions {
for key, value := range action.Slots {
state.SetState(action.Account, key, value)
}
}

// write modifications to the trie
state.IntermediateRoot(false)
if err := state.CheckTransactionConditional(&test.cond); err == nil && !test.valid {
t.Errorf("Test %s got unvalid value: want %v, got err %v", test.name, test.valid, err)
}
})
}
}
14 changes: 12 additions & 2 deletions core/txpool/legacypool/legacypool.go
Original file line number Diff line number Diff line change
Expand Up @@ -1733,16 +1733,26 @@ func (pool *LegacyPool) demoteUnexecutables() {
}
pendingNofundsMeter.Mark(int64(len(drops)))

// Drop all transactions that were rejected by the miner
rejectedDrops := list.txs.Filter(func(tx *types.Transaction) bool {
return tx.Rejected()
})
for _, tx := range rejectedDrops {
hash := tx.Hash()
pool.all.Remove(hash)
log.Trace("Removed rejected transaction", "hash", hash)
}

for _, tx := range invalids {
hash := tx.Hash()
log.Trace("Demoting pending transaction", "hash", hash)

// Internal shuffle shouldn't touch the lookup set.
pool.enqueueTx(hash, tx, false, false)
}
pendingGauge.Dec(int64(len(olds) + len(drops) + len(invalids)))
pendingGauge.Dec(int64(len(olds) + len(drops) + len(invalids) + len(rejectedDrops)))
if pool.locals.contains(addr) {
localGauge.Dec(int64(len(olds) + len(drops) + len(invalids)))
localGauge.Dec(int64(len(olds) + len(drops) + len(invalids) + len(rejectedDrops)))
}
// If there's a gap in front, alert (should never happen) and postpone all transactions
if list.Len() > 0 && list.txs.Get(nonce) == nil {
Expand Down
33 changes: 33 additions & 0 deletions core/txpool/legacypool/legacypool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,39 @@ func TestDropping(t *testing.T) {
}
}

// Tests that transactions marked as reject (by the miner in practice)
// are removed from the pool
func TestRejectedDropping(t *testing.T) {
t.Parallel()

pool, key := setupPool()
defer pool.Close()

account := crypto.PubkeyToAddress(key.PublicKey)
testAddBalance(pool, account, big.NewInt(1000))

// create some txs. tx0 has a conditional
tx0, tx1 := transaction(0, 100, key), transaction(1, 200, key)

pool.all.Add(tx0, false)
pool.all.Add(tx1, false)
pool.promoteTx(account, tx0.Hash(), tx0)
pool.promoteTx(account, tx1.Hash(), tx1)

// pool state is unchanged
<-pool.requestReset(nil, nil)
if pool.all.Count() != 2 {
t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), 2)
}

// tx0 conditional is marked as rejected and should be removed
tx0.SetRejected()
<-pool.requestReset(nil, nil)
if pool.all.Count() != 1 {
t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), 1)
}
}

// Tests that if a transaction is dropped from the current pending pool (e.g. out
// of fund), all consecutive (still valid, but not executable) transactions are
// postponed back into the future queue to prevent broadcasting them.
Expand Down
Loading

0 comments on commit 5f7ebba

Please sign in to comment.