The binding sends a tx's fee cap and tip, as ABI 7's CGpuTx carries them

go_bridge.h's CGpuTx has max_fee_per_gas and max_priority_fee_per_gas in
place of gas_price, 120 bytes on LP64. cevm.Transaction has GasFeeCap and
GasTipCap for them, both the gas price of a legacy or access-list tx, and
buildTxs sets both; the size check reads 120. cevm charges min(cap, base
fee + tip), as the EVM does, where it charged the cap whole, and declines
a tip above the cap.

The executor shapes tx.GasFeeCap() and tx.GasTipCap(), each held to 64
bits. The device test sends a transfer with its tip above its cap, which
the device declines.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-23 13:45:07 -07:00
co-authored by Hanzo Dev
parent ace7fa1e3f
commit 1d08088efd
7 changed files with 96 additions and 60 deletions
+19 -11
View File
@@ -131,18 +131,26 @@ func (b Backend) String() string {
// declined (ErrDeclined), because gpu_execute_block passes no host and would
// run that code as a message, on its whole limit, which is not the tx's gas.
//
// Value and GasPrice are 64-bit on the wire. A transaction whose value or
// price does not fit is the caller's to run; it never belongs in a batch.
// GasFeeCap and GasTipCap are the tx's maxFeePerGas and maxPriorityFeePerGas,
// both its gas price for a legacy or access-list tx. It pays per gas the
// block's base fee and its tip, capped at its fee cap, and its sender must
// hold its limit at its fee cap. cevm declines a batch with a tx whose tip is
// above its fee cap or whose fee cap is under the base fee.
//
// Value, GasFeeCap and GasTipCap are 64-bit on the wire. A transaction whose
// value or fee does not fit is the caller's to run; it never belongs in a
// batch.
type Transaction struct {
From [20]byte
To [20]byte
HasTo bool
Data []byte // Calldata
Code []byte // The recipient's code; any makes cevm decline the batch
GasLimit uint64
Value uint64
Nonce uint64
GasPrice uint64
From [20]byte
To [20]byte
HasTo bool
Data []byte // Calldata
Code []byte // The recipient's code; any makes cevm decline the batch
GasLimit uint64
Value uint64
Nonce uint64
GasFeeCap uint64
GasTipCap uint64
}
// TxStatus is a per-transaction execution outcome from the V2 ABI.
+9 -2
View File
@@ -6,8 +6,10 @@ import "testing"
// The block context reaches the device path and is read there: the same
// funded transfer that a device runs is declined when the block's base fee is
// above its price, and when the block's gas limit cannot hold its limit.
// above its fee cap, and when the block's gas limit cannot hold its limit.
// Either would be run as a valid transfer by a path that dropped the context.
// So is the transfer with its tip above its fee cap, which luxfi/geth refuses
// (ErrTipAboveFeeCap): before ABI 7 carried the tip, the device never saw it.
func TestTheBlockContextReachesTheDevice(t *testing.T) {
for _, b := range deviceLanes(t) {
t.Run(BackendName(b), func(t *testing.T) {
@@ -16,10 +18,15 @@ func TestTheBlockContextReachesTheDevice(t *testing.T) {
ranTransfers(t, r, err, 1)
priced := ctx
priced.BaseFee = txs[0].GasPrice + 1
priced.BaseFee = txs[0].GasFeeCap + 1
r, err = ExecuteBlock(b, 0, txs, &priced, state)
declined(t, r, err)
tipped := append([]Transaction(nil), txs...)
tipped[0].GasTipCap = tipped[0].GasFeeCap + 1
r, err = ExecuteBlock(b, 0, tipped, &ctx, state)
declined(t, r, err)
full := ctx
full.GasLimit = txs[0].GasLimit - 1
r, err = ExecuteBlock(b, 0, txs, &full, state)
+5 -4
View File
@@ -77,8 +77,8 @@ var (
// is how storage_root went unset. Each pair fails when the size is larger
// (the first) or smaller (the second).
var (
_ [unsafe.Sizeof(C.CGpuTx{}) - 112]struct{}
_ [112 - unsafe.Sizeof(C.CGpuTx{})]struct{}
_ [unsafe.Sizeof(C.CGpuTx{}) - 120]struct{}
_ [120 - unsafe.Sizeof(C.CGpuTx{})]struct{}
_ [unsafe.Sizeof(C.CGpuStateAccount{}) - 136]struct{}
_ [136 - unsafe.Sizeof(C.CGpuStateAccount{})]struct{}
_ [unsafe.Sizeof(C.CGpuBlockResult{}) - 88]struct{}
@@ -122,7 +122,8 @@ func buildTxs(txs []Transaction, pinner *runtime.Pinner) []C.CGpuTx {
ctxs[i].gas_limit = C.uint64_t(t.GasLimit)
ctxs[i].value = C.uint64_t(t.Value)
ctxs[i].nonce = C.uint64_t(t.Nonce)
ctxs[i].gas_price = C.uint64_t(t.GasPrice)
ctxs[i].max_fee_per_gas = C.uint64_t(t.GasFeeCap)
ctxs[i].max_priority_fee_per_gas = C.uint64_t(t.GasTipCap)
if t.HasTo {
ctxs[i].has_to = 1
}
@@ -363,7 +364,7 @@ func healthBattery() []healthProbe {
}
return []healthProbe{{
name: "transfer",
txs: []Transaction{{From: from, To: to, HasTo: true, GasLimit: 30000, Value: 1, GasPrice: 1}},
txs: []Transaction{{From: from, To: to, HasTo: true, GasLimit: 30000, Value: 1, GasFeeCap: 1, GasTipCap: 1}},
ctx: BlockContext{GasLimit: 30_000_000, ChainID: 1, BaseFee: 1, Coinbase: coinbase},
state: []StateAccount{row(from, 1_000_000), row(to, 0), row(coinbase, 0)},
wantGas: 21000,
+8 -7
View File
@@ -281,7 +281,7 @@ func transfers(n int) ([]Transaction, BlockContext, []StateAccount) {
for i := range txs {
from := [20]byte{0xA0, byte(i >> 8), byte(i)}
to := [20]byte{0xB0, byte(i >> 8), byte(i)}
txs[i] = Transaction{From: from, To: to, HasTo: true, GasLimit: 30_000, Value: 1, GasPrice: 1}
txs[i] = Transaction{From: from, To: to, HasTo: true, GasLimit: 30_000, Value: 1, GasFeeCap: 1, GasTipCap: 1}
state = append(state, account(from, 1_000_000), account(to, 0))
}
state = append(state, account(ctx.Coinbase, 0))
@@ -396,12 +396,13 @@ func bytecodeTx(i uint64, code []byte) Transaction {
from[19] = byte(i)
from[18] = byte(i >> 8)
return Transaction{
From: from,
HasTo: true,
Code: code,
GasLimit: 1_000_000,
Nonce: i,
GasPrice: 1,
From: from,
HasTo: true,
Code: code,
GasLimit: 1_000_000,
Nonce: i,
GasFeeCap: 1,
GasTipCap: 1,
}
}
+25 -14
View File
@@ -281,15 +281,17 @@ func TestShapeCarriesEachTransactionsOwnFields(t *testing.T) {
GasPrice: big.NewInt(5), Data: []byte{0xAB},
}),
types.NewTx(&types.LegacyTx{Nonce: 8, Value: big.NewInt(1), Gas: 53000, GasPrice: big.NewInt(9)}),
types.NewTx(&types.DynamicFeeTx{Nonce: 9, To: &to, Gas: 21000, GasFeeCap: big.NewInt(30), GasTipCap: big.NewInt(2)}),
}
senders := []common.Address{{0x11}, {0x33}}
senders := []common.Address{{0x11}, {0x33}, {0x44}}
got, i := shape(txs, senders, sdb)
if i != len(txs) {
t.Fatalf("shape declined at index %d; every transaction here is representable", i)
}
if got[0].Nonce != 7 || got[0].Value != 1234 || got[0].GasLimit != 21000 || got[0].GasPrice != 5 {
if got[0].Nonce != 7 || got[0].Value != 1234 || got[0].GasLimit != 21000 ||
got[0].GasFeeCap != 5 || got[0].GasTipCap != 5 {
t.Errorf("tx 0 shaped as %+v", got[0])
}
if !got[0].HasTo || common.Address(got[0].To) != to {
@@ -315,12 +317,19 @@ func TestShapeCarriesEachTransactionsOwnFields(t *testing.T) {
if common.Address(got[1].From) != senders[1] {
t.Errorf("tx 1 sender = %x, want %x", got[1].From, senders[1])
}
// A dynamic-fee tx carries its fee cap and its tip: it pays the base fee
// and the tip, capped at the cap. Before, the wire had one price, the cap,
// and cevm charged it whole.
if got[2].GasFeeCap != 30 || got[2].GasTipCap != 2 {
t.Errorf("tx 2 fee cap/tip = %d/%d, want 30/2", got[2].GasFeeCap, got[2].GasTipCap)
}
}
// cevm.Transaction holds Value and GasPrice as uint64. A value or price above
// 2^64-1 is rare but legal, and truncating it would execute a transaction
// other than the one that was signed — so the block is declined at the index
// that cannot be carried.
// cevm.Transaction holds Value, GasFeeCap and GasTipCap as uint64. A value or
// fee above 2^64-1 is rare but legal, and truncating it would execute a
// transaction other than the one that was signed — so the block is declined at
// the index that cannot be carried.
func TestAValueOrPriceTooLargeForTheWireIsDeclinedAtItsIndex(t *testing.T) {
huge := new(big.Int).Lsh(big.NewInt(1), 64) // 2^64: one past what fits
max := new(big.Int).Sub(huge, big.NewInt(1))
@@ -356,17 +365,19 @@ func TestAValueOrPriceTooLargeForTheWireIsDeclinedAtItsIndex(t *testing.T) {
if i != 2 {
t.Fatalf("shape declined 2^64-1 at index %d; it is exactly representable", i)
}
if got[1].Value != tc.exact.Value().Uint64() || got[1].GasPrice != tc.exact.GasPrice().Uint64() {
t.Errorf("shaped value/price = %d/%d, want %d/%d", got[1].Value, got[1].GasPrice,
tc.exact.Value().Uint64(), tc.exact.GasPrice().Uint64())
if got[1].Value != tc.exact.Value().Uint64() || got[1].GasFeeCap != tc.exact.GasFeeCap().Uint64() ||
got[1].GasTipCap != tc.exact.GasTipCap().Uint64() {
t.Errorf("shaped value/fee cap/tip = %d/%d/%d, want %d/%d/%d", got[1].Value, got[1].GasFeeCap,
got[1].GasTipCap, tc.exact.Value().Uint64(), tc.exact.GasFeeCap().Uint64(),
tc.exact.GasTipCap().Uint64())
}
})
}
}
// CGpuTx has one price and no access list, blob hashes, authorizations or tip.
// A tx that carries any of them is charged for them, or invalid by them, where
// cevm would not see it: it is declined at its index.
// CGpuTx has no access list, blob hashes or authorizations. A tx that carries
// any of them is charged for them where cevm would not see it, and one whose
// tip is above its fee cap is invalid: it is declined at its index.
func TestATransactionTheWireDropsPartOfIsDeclinedAtItsIndex(t *testing.T) {
to := common.Address{0x11}
plain := types.NewTx(&types.LegacyTx{To: &to, Value: big.NewInt(1), Gas: 21000, GasPrice: big.NewInt(1)})
@@ -403,8 +414,8 @@ func TestANilGasPriceIsZeroAndNotAPanic(t *testing.T) {
if i != 1 {
t.Fatalf("shape declined at %d", i)
}
if got[0].GasPrice != 0 {
t.Errorf("gas price = %d, want 0", got[0].GasPrice)
if got[0].GasFeeCap != 0 || got[0].GasTipCap != 0 {
t.Errorf("fee cap/tip = %d/%d, want 0/0", got[0].GasFeeCap, got[0].GasTipCap)
}
}
+23 -16
View File
@@ -214,28 +214,35 @@ func (e *Executor) Backend() cevm.Backend { return e.CevmBackend }
// paired with the recovered senders.
//
// It returns the index of the first transaction that cannot be represented, or
// len(txs) when every one can. cevm.Transaction carries Value and GasPrice as
// uint64; a value or price above 2^64-1 is rare but legal, and truncating it
// would execute a transaction other than the one that was signed. The price is
// tx.GasPrice(), the fee cap of a dynamic-fee tx: what the EVM's buy-gas
// balance check charges, and what cevm checks against the base fee. A tx the
// wire drops part of (see carried) is not representable either.
// len(txs) when every one can. cevm.Transaction carries Value, GasFeeCap and
// GasTipCap as uint64; a value or fee above 2^64-1 is rare but legal, and
// truncating it would execute a transaction other than the one that was
// signed. The fee cap and tip are tx.GasFeeCap() and tx.GasTipCap(), both the
// gas price of a legacy or access-list tx: cevm charges the base fee and the
// tip, capped at the fee cap, as the EVM does, and checks the sender's balance
// at the fee cap. A tx the wire drops part of (see carried) is not
// representable either.
func shape(txs types.Transactions, senders []common.Address, statedb *state.StateDB) ([]cevm.Transaction, int) {
out := make([]cevm.Transaction, len(txs))
for i, tx := range txs {
if !carried(tx) || !tx.Value().IsUint64() {
return out, i
}
price, ok := fits(tx.GasPrice())
feeCap, ok := fits(tx.GasFeeCap())
if !ok {
return out, i
}
tipCap, ok := fits(tx.GasTipCap())
if !ok {
return out, i
}
ct := cevm.Transaction{
GasLimit: tx.Gas(),
Nonce: tx.Nonce(),
Data: tx.Data(),
Value: tx.Value().Uint64(),
GasPrice: price,
GasLimit: tx.Gas(),
Nonce: tx.Nonce(),
Data: tx.Data(),
Value: tx.Value().Uint64(),
GasFeeCap: feeCap,
GasTipCap: tipCap,
}
copy(ct.From[:], senders[i].Bytes())
if to := tx.To(); to != nil {
@@ -252,10 +259,10 @@ func shape(txs types.Transactions, senders []common.Address, statedb *state.Stat
}
// carried reports whether cevm's wire carries everything tx's gas and status
// depend on. CGpuTx has one price and no access list, blob hashes,
// authorizations or tip: a tx with an access list is charged for it, a blob
// or set-code tx for what it carries, and one whose tip exceeds its fee cap is
// invalid, which only the Go EVM would see.
// depend on. CGpuTx has no access list, blob hashes or authorizations: a tx
// with an access list is charged for it, and a blob or set-code tx for what it
// carries. One whose tip exceeds its fee cap is invalid (ErrTipAboveFeeCap):
// cevm declines it too, and the Go EVM says why.
func carried(tx *types.Transaction) bool {
switch tx.Type() {
case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType:
+7 -6
View File
@@ -29,12 +29,13 @@ import (
// no result.
func TestABatchWithCodeIsDeclinedOnEveryBackend(t *testing.T) {
tx := cevm.Transaction{
From: [20]byte{0x11},
To: [20]byte{0x22},
HasTo: true,
Code: []byte{0x60, 0x01, 0x60, 0x01, 0x01, 0x50, 0x00}, // 1+1, POP, STOP
GasLimit: 100_000,
GasPrice: 1,
From: [20]byte{0x11},
To: [20]byte{0x22},
HasTo: true,
Code: []byte{0x60, 0x01, 0x60, 0x01, 0x01, 0x50, 0x00}, // 1+1, POP, STOP
GasLimit: 100_000,
GasFeeCap: 1,
GasTipCap: 1,
}
for _, b := range cevm.AvailableBackends() {
t.Run(cevm.BackendName(b), func(t *testing.T) {