Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions consensus/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,16 +276,16 @@ func (c *Consensus) verifyBlock(blk *block.Block, state *state.State, blockConfl
return chain.HasTransaction(txid, txBlockRef)
}

for _, tx := range txs {
for _, trx := range txs {
// check if tx existed
if found, err := hasTx(tx.ID(), tx.BlockRef().Number()); err != nil {
if found, err := hasTx(trx.ID(), trx.BlockRef().Number()); err != nil {
return nil, nil, err
} else if found {
return nil, nil, consensusError("tx already exists")
}

// check depended tx
if dep := tx.DependsOn(); dep != nil {
if dep := trx.DependsOn(); dep != nil {
found, reverted, err := findDep(*dep)
if err != nil {
return nil, nil, err
Expand All @@ -299,7 +299,22 @@ func (c *Consensus) verifyBlock(blk *block.Block, state *state.State, blockConfl
}
}

receipt, err := rt.ExecuteTransaction(tx)
// Eth tx requires linear nonce growth (mismatch is a hard consensus error).
Comment thread
otherview marked this conversation as resolved.
if trx.Type() == tx.TypeEthDynamicFee {
origin, err := trx.Origin()
if err != nil {
return nil, nil, consensusError(fmt.Sprintf("tx signer unavailable: %v", err))
}
accNonce, err := state.GetNonce(origin)
if err != nil {
return nil, nil, err
}
if trx.Nonce() != accNonce {
return nil, nil, consensusError(fmt.Sprintf("tx nonce mismatch: want %v, have %v", accNonce, trx.Nonce()))
}
}

receipt, err := rt.ExecuteTransaction(trx)
if err != nil {
return nil, nil, err
}
Expand All @@ -310,7 +325,7 @@ func (c *Consensus) verifyBlock(blk *block.Block, state *state.State, blockConfl
}

receipts = append(receipts, receipt)
processedTxs[tx.ID()] = receipt.Reverted
processedTxs[trx.ID()] = receipt.Reverted
}

if header.GasUsed() != totalGasUsed {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.4.1
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad
github.com/holiman/uint256 v1.2.4
github.com/holiman/uint256 v1.3.2
github.com/huin/goupnp v0.0.0-20171109214107-dceda08e705b
github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458
github.com/mattn/go-isatty v0.0.3
Expand Down Expand Up @@ -69,4 +69,4 @@ require (

replace github.com/syndtr/goleveldb => github.com/vechain/goleveldb v1.0.1-0.20220809091043-51eb019c8655

replace github.com/ethereum/go-ethereum => github.com/vechain/go-ethereum v1.8.15-0.20260324060835-4fc778eca93e
replace github.com/ethereum/go-ethereum => github.com/vechain/go-ethereum v1.8.15-0.20260511103518-c6cd268fa5ce
10 changes: 4 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU=
github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
Expand Down Expand Up @@ -88,8 +86,8 @@ github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvK
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po=
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huin/goupnp v0.0.0-20171109214107-dceda08e705b h1:mvnS3LbcRgdM4nBLksEjecaLvASuBsg1mIJHc0l22iI=
github.com/huin/goupnp v0.0.0-20171109214107-dceda08e705b/go.mod h1:MZ2ZmwcBpvOoJ22IJsc7va19ZwoheaBk43rKg12SKag=
Expand Down Expand Up @@ -168,8 +166,8 @@ github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/vechain/go-ecvrf v0.0.0-20251211112124-5d5a3ef70fc9 h1:g6xRR8HD50ABVYhLhDv1MfEfDSWlDVV+GD/IKvTOQG8=
github.com/vechain/go-ecvrf v0.0.0-20251211112124-5d5a3ef70fc9/go.mod h1:Yoa6emaGryEaOlrvv6Eg6iX7vM7cqZAf0i9D1SCobnY=
github.com/vechain/go-ethereum v1.8.15-0.20260324060835-4fc778eca93e h1:0/g3bVEx1fFoYD6He8t7DgVNkgPNlZZl7h1WdvCd5K0=
github.com/vechain/go-ethereum v1.8.15-0.20260324060835-4fc778eca93e/go.mod h1:LVuf3xPnVtHmoIP5+mN7aPnIeRBgo0xVq/wVELtSeIA=
github.com/vechain/go-ethereum v1.8.15-0.20260511103518-c6cd268fa5ce h1:5N1rxNvAN8q2HXStzz41dGXTDL3MgLukeRH2MwB3YNI=
github.com/vechain/go-ethereum v1.8.15-0.20260511103518-c6cd268fa5ce/go.mod h1:WwsMAst71TTecDPsaZ/nj/MnA7uy+OEUjYEdZFD0gjA=
github.com/vechain/goleveldb v1.0.1-0.20220809091043-51eb019c8655 h1:CbHcWpCi7wOYfpoErRABh3Slyq9vO0Ay/EHN5GuJSXQ=
github.com/vechain/goleveldb v1.0.1-0.20220809091043-51eb019c8655/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down
14 changes: 14 additions & 0 deletions packer/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,20 @@ func (f *Flow) Adopt(t *tx.Transaction) error {
}
}

// Eth tx requires linear nonce growth.
if t.Type() == tx.TypeEthDynamicFee {
accNonce, err := f.runtime.State().GetNonce(origin)
if err != nil {
return err
}
if t.Nonce() < accNonce {
return badTxError{"nonce too low"}
}
if t.Nonce() > accNonce {
return errTxNotAdoptableNow
}
}

// check if tx already there
if found, err := f.hasTx(t.ID(), t.BlockRef().Number()); err != nil {
return err
Expand Down
75 changes: 75 additions & 0 deletions packer/flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,81 @@ func TestPackAfterGalacticaFork(t *testing.T) {
}
}

// TestEthDynFee_AdoptRejectedPreInterstellar verifies packer rejects 0x02
// txs before INTERSTELLAR activation (V2 statedb / 64-bit chainid not yet on).
func TestEthDynFee_AdoptRejectedPreInterstellar(t *testing.T) {
tc, err := testchain.NewWithFork(&thor.ForkConfig{GALACTICA: 0, HAYABUSA: math.MaxUint32, INTERSTELLAR: math.MaxUint32}, 180)
require.NoError(t, err)

validator, ok := tc.NextValidator()
require.True(t, ok)

pkr := packer.New(tc.Repo(), tc.Stater(), validator.Address, nil, tc.GetForkConfig(), 0)
best := tc.Repo().BestBlockSummary()
flow, err := pkr.Schedule(best, best.Header.Timestamp()+thor.BlockInterval())
require.NoError(t, err)

to := thor.BytesToAddress([]byte("to"))
trx := tx.NewBuilder(tx.TypeEthDynamicFee).
Gas(21000).
MaxFeePerGas(big.NewInt(thor.InitialBaseFee)).
MaxPriorityFeePerGas(big.NewInt(0)).
ChainID(tc.Repo().ChainID()).
Nonce(0).
To(&to).Value(big.NewInt(1)).
Build()
trx = tx.MustSign(trx, genesis.DevAccounts()[1].PrivateKey)

err = flow.Adopt(trx)
assert.Error(t, err)
assert.Equal(t, "bad tx: invalid tx type", err.Error())
}

// TestEthDynFee_AdoptNonceLinearGrowth verifies that packer rejects eth txs
// whose nonce doesn't match the on-state nonce: future nonce → not adoptable
// now (queued semantics); equal nonce → passes the nonce gate.
func TestEthDynFee_AdoptNonceLinearGrowth(t *testing.T) {
tc, err := testchain.NewWithFork(&thor.ForkConfig{GALACTICA: 0, HAYABUSA: math.MaxUint32, INTERSTELLAR: 0}, 180)
require.NoError(t, err)

validator, ok := tc.NextValidator()
require.True(t, ok)

pkr := packer.New(tc.Repo(), tc.Stater(), validator.Address, nil, tc.GetForkConfig(), 0)
best := tc.Repo().BestBlockSummary()
flow, err := pkr.Schedule(best, best.Header.Timestamp()+thor.BlockInterval())
require.NoError(t, err)

to := thor.BytesToAddress([]byte("to"))

// nonce > state.nonce → not adoptable now.
futureTx := tx.NewBuilder(tx.TypeEthDynamicFee).
Gas(21000).
MaxFeePerGas(big.NewInt(thor.InitialBaseFee)).
MaxPriorityFeePerGas(big.NewInt(0)).
ChainID(tc.Repo().ChainID()).
Nonce(99).
To(&to).Value(big.NewInt(1)).
Build()
futureTx = tx.MustSign(futureTx, genesis.DevAccounts()[1].PrivateKey)
err = flow.Adopt(futureTx)
assert.Error(t, err)
assert.Equal(t, "tx not adoptable now", err.Error())

// nonce == state.nonce → passes nonce gate.
okTx := tx.NewBuilder(tx.TypeEthDynamicFee).
Gas(21000).
MaxFeePerGas(big.NewInt(thor.InitialBaseFee)).
MaxPriorityFeePerGas(big.NewInt(0)).
ChainID(tc.Repo().ChainID()).
Nonce(0).
To(&to).Value(big.NewInt(1)).
Build()
okTx = tx.MustSign(okTx, genesis.DevAccounts()[2].PrivateKey)
err = flow.Adopt(okTx)
assert.Nil(t, err, "eth tx with matching nonce must adopt")
}

func TestAdoptErr(t *testing.T) {
db := muxdb.NewMem()
stater := state.NewStater(db)
Expand Down
2 changes: 1 addition & 1 deletion runtime/resolved_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ func (r *ResolvedTransaction) ToContext(
return nil, err
}
return &xenv.TransactionContext{
Type: r.tx.Type(),
ID: r.tx.ID(),
Origin: r.Origin,
GasPayer: gasPayer,
Expand All @@ -260,6 +261,5 @@ func (r *ResolvedTransaction) ToContext(
BlockRef: r.tx.BlockRef(),
Expiration: r.tx.Expiration(),
ClauseCount: uint32(len(r.Clauses)),
Type: r.tx.Type(),
}, nil
}
58 changes: 26 additions & 32 deletions runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,22 @@ func (rt *Runtime) Chain() *chain.Chain { return rt.chain }
func (rt *Runtime) State() *state.State { return rt.state }
func (rt *Runtime) Context() *xenv.BlockContext { return rt.ctx }

// newStateDB returns V2 for 0x02 txs, V1 otherwise.
func (rt *Runtime) newStateDB(txCtx *xenv.TransactionContext) statedb.StateDB {
if txCtx.Type == tx.TypeEthDynamicFee {
return statedb.NewV2(rt.state)
}
return statedb.New(rt.state)
}

// SetVMConfig config VM.
// Returns this runtime.
func (rt *Runtime) SetVMConfig(config vm.Config) *Runtime {
rt.vmConfig = config
return rt
}

func (rt *Runtime) newEVM(stateDB *statedb.StateDB, clauseIndex uint32, txCtx *xenv.TransactionContext) *vm.EVM {
func (rt *Runtime) newEVM(stateDB statedb.StateDB, clauseIndex uint32, txCtx *xenv.TransactionContext) *vm.EVM {
var (
lastNonNativeCallGas uint64
baseFee *big.Int
Expand Down Expand Up @@ -233,16 +241,10 @@ func (rt *Runtime) newEVM(stateDB *statedb.StateDB, clauseIndex uint32, txCtx *x
return common.Hash(id)
},
NewContractAddress: func(_ *vm.EVM, caller common.Address, counter uint32) common.Address {
switch txCtx.Type {
case tx.TypeEthDynamicFee:
// Ethereum formula: keccak256(rlp([caller, nonce])). counter is unused here —
// nonces play the equivalent role for Ethereum txs. With nonce tracking stubbed,
// stateDB.GetNonce always returns 0; sequential creates from the same caller
// will collide on the second call until real nonce tracking is implemented.
if txCtx.Type == tx.TypeEthDynamicFee {
return crypto.CreateAddress(caller, stateDB.GetNonce(caller))
default:
return common.Address(thor.CreateContractAddress(txCtx.ID, clauseIndex, counter))
}
return common.Address(thor.CreateContractAddress(txCtx.ID, clauseIndex, counter))
},
InterceptContractCall: func(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error, bool) {
if evm.Depth() < 2 {
Expand Down Expand Up @@ -390,7 +392,7 @@ func (rt *Runtime) PrepareClause(
txCtx *xenv.TransactionContext,
) (exec func() (output *Output, interrupted bool, err error), interrupt func()) {
var (
stateDB = statedb.New(rt.state, txCtx.Type)
stateDB = rt.newStateDB(txCtx)
evm = rt.newEVM(stateDB, clauseIndex, txCtx)
data []byte
leftOverGas uint64
Expand Down Expand Up @@ -419,6 +421,14 @@ func (rt *Runtime) PrepareClause(
data, caddr, leftOverGas, vmErr = evm.Create(vm.AccountRef(txCtx.Origin), clause.Data(), gas, clause.Value())
contractAddr = (*thor.Address)(&caddr)
} else {
if txCtx.Type == tx.TypeEthDynamicFee {
nonce := stateDB.GetNonce(common.Address(txCtx.Origin))
if nonce+1 < nonce {
return nil, false, errors.New("nonce has max value")
Comment thread
otherview marked this conversation as resolved.
}

stateDB.SetNonce(common.Address(txCtx.Origin), nonce+1)
}
data, leftOverGas, vmErr = evm.Call(vm.AccountRef(txCtx.Origin), common.Address(*clause.To()), clause.Data(), gas, clause.Value())
}

Expand Down Expand Up @@ -533,9 +543,12 @@ func (rt *Runtime) PrepareTransaction(trx *tx.Transaction) (*TransactionExecutor
leftOverGas += refund

if output.VMErr != nil {
// vm exception here
// revert all executed clauses
rt.state.RevertTo(checkpoint)
if txCtx.Type != tx.TypeEthDynamicFee {
// multi-clause: undo prior clauses' state.
// eth tx needs to preserve the nonce increment and EVM's internal
// RevertToSnapshot already cleaned the failed call.
rt.state.RevertTo(checkpoint)
}
reverted = true
txOutputs = nil
return
Expand Down Expand Up @@ -568,25 +581,6 @@ func (rt *Runtime) PrepareTransaction(trx *tx.Transaction) (*TransactionExecutor
return nil, err
}

// EIP-2: nonce is always consumed for EthereumTx, even if the tx reverts.
// For CALL txs the EVM never touches the nonce, so we always increment here.
// For CREATE txs that succeeded the EVM already incremented the nonce via
// stateDB.SetNonce before the inner snapshot; that survived the tx.
// For CREATE txs that reverted, rt.state.RevertTo(checkpoint) undid the EVM's
// increment, so we must re-apply it here.
if trx.Type() == tx.TypeEthDynamicFee {
isCreate := resolvedTx.Clauses[0].IsCreatingContract()
if !isCreate || reverted {
nonce, err := rt.state.GetNonce(txCtx.Origin)
if err != nil {
return nil, err
}
if err := rt.state.SetNonce(txCtx.Origin, nonce+1); err != nil {
return nil, err
}
}
}

if !thor.IsForked(rt.ctx.Number, rt.forkConfig.GALACTICA) {
provedWork, err := trx.ProvedWork(rt.ctx.Number-1, rt.chain.GetBlockID)
if err != nil {
Expand Down
Loading
Loading