Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix EIP-7623 gas calculation and tests #217

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions blockchain/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ var (
// than required to start the invocation.
ErrIntrinsicGas = errors.New("intrinsic gas too low")

// ErrDataFloorGas is returned if the transaction is specified to use less gas
// ErrFloorDataGas is returned if the transaction is specified to use less gas
// than required for the data floor cost.
ErrDataFloorGas = errors.New("insufficient gas for data floor cost")
ErrFloorDataGas = errors.New("insufficient gas for floor data gas cost")

// ErrGasLimit is returned if a transaction's requested gas limit exceeds the
// maximum allowance of the current block.
Expand Down
60 changes: 36 additions & 24 deletions blockchain/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,16 @@ type StateTransition struct {
// Message represents a message sent to a contract.
type Message interface {
// ValidatedSender returns the sender of the transaction.
// The returned sender should be derived by calling AsMessageAccountKeyPicker().
// It should be set by calling AsMessageAccountKeyPicker().
ValidatedSender() common.Address

// ValidatedFeePayer returns the fee payer of the transaction.
// The returned fee payer should be derived by calling AsMessageAccountKeyPicker().
// It should be set by calling AsMessageAccountKeyPicker().
ValidatedFeePayer() common.Address

// ValidatedIntrinsicGas returns the intrinsic gas of the transaction.
// The returned intrinsic gas should be derived by calling AsMessageAccountKeyPicker().
ValidatedIntrinsicGas() *types.ValidatedIntrinsicGas
// ValidatedGas holds the intrinsic gas, sig validation gas, and number of data tokens for the transaction.
// It should be set by calling AsMessageAccountKeyPicker().
ValidatedGas() *types.ValidatedGas

// FeeRatio returns a ratio of tx fee paid by the fee payer in percentage.
// For example, if it is 30, 30% of tx fee will be paid by the fee payer.
Expand Down Expand Up @@ -346,21 +346,22 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
}

// Check clauses 4-5, subtract intrinsic gas if everything is correct
validatedGas := msg.ValidatedIntrinsicGas()
if st.gas < validatedGas.Gas {
validatedGas := msg.ValidatedGas()
hyunsooda marked this conversation as resolved.
Show resolved Hide resolved
if st.gas < validatedGas.IntrinsicGas {
return nil, ErrIntrinsicGas
}
rules := st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber)
if rules.IsPrague {
floorGas, err := FloorDataGas(validatedGas.Tokens)
floorGas, err := FloorDataGas(st.msg.Type(), validatedGas.Tokens, validatedGas.SigValidateGas)
if err != nil {
return nil, err
}
if st.gas < floorGas {
return nil, fmt.Errorf("%w: have %d, want %d", ErrDataFloorGas, st.gas, floorGas)
return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, st.gas, floorGas)
}
}
st.gas -= validatedGas.Gas
// SigValidationGas is already inclduded in IntrinsicGas
st.gas -= validatedGas.IntrinsicGas

// Check clause 6
if msg.Value().Sign() > 0 && !st.evm.Context.CanTransfer(st.state, msg.ValidatedSender(), msg.Value()) {
Expand Down Expand Up @@ -429,22 +430,25 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
return nil, vm.ErrTotalTimeLimitReached
}

var gasRefund uint64
if rules.IsKore {
// After EIP-3529: refunds are capped to gasUsed / 5
gasRefund = st.refundAmount(params.RefundQuotientEIP3529)
} else {
// Before EIP-3529: refunds were capped to gasUsed / 2
gasRefund = st.refundAmount(params.RefundQuotient)
}
st.gas += gasRefund

ulbqb marked this conversation as resolved.
Show resolved Hide resolved
if rules.IsPrague {
// After EIP-7623: Data-heavy transactions pay the floor gas.
// Overflow error has already been checked and can be ignored here.
floorGas, _ := FloorDataGas(validatedGas.Tokens)
floorGas, _ := FloorDataGas(st.msg.Type(), validatedGas.Tokens, validatedGas.SigValidateGas)
if st.gasUsed() < floorGas {
st.gas = st.initialGas - floorGas
}
}

if rules.IsKore {
// After EIP-3529: refunds are capped to gasUsed / 5
st.refundGas(params.RefundQuotientEIP3529)
} else {
// Before EIP-3529: refunds were capped to gasUsed / 2
st.refundGas(params.RefundQuotient)
}
st.returnGas()

// Defer transferring Tx fee when DeferredTxFee is true
// DeferredTxFee has never been voted, so it's ok to use the genesis value instead of the latest value from governance.
Expand Down Expand Up @@ -592,14 +596,16 @@ func (st *StateTransition) applyAuthorization(auth *types.SetCodeAuthorization,
return nil
}

func (st *StateTransition) refundGas(refundQuotient uint64) {
func (st *StateTransition) refundAmount(refundQuotient uint64) uint64 {
// Apply refund counter, capped a refund quotient
refund := st.gasUsed() / refundQuotient
if refund > st.state.GetRefund() {
refund = st.state.GetRefund()
}
st.gas += refund
return refund
}

func (st *StateTransition) returnGas() {
// Return KAIA for remaining gas, exchanged at the original rate.
remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gas), st.gasPrice)

Expand All @@ -624,10 +630,16 @@ func (st *StateTransition) gasUsed() uint64 {

// FloorDataGas calculates the minimum gas required for a transaction
// based on its data tokens (EIP-7623).
func FloorDataGas(tokens uint64) (uint64, error) {
func FloorDataGas(txType types.TxType, tokens, sigValidateGas uint64) (uint64, error) {
// Check for overflow
if (math.MaxUint64-params.TxGas)/params.CostFloorPerToken7623 < tokens {
// Instead of using parmas.TxGas, we should consider the tx type
// because Kaia tx type has different tx gas (e.g., fee delegated tx).
txGas, err := types.GetTxGasForTxType(txType)
if err != nil {
return 0, err
}
if (math.MaxUint64-txGas)/params.CostFloorPerToken7623 < tokens {
return 0, types.ErrGasUintOverflow
}
return params.TxGas + tokens*params.CostFloorPerToken7623, nil
return txGas + tokens*params.CostFloorPerToken7623 + sigValidateGas, nil
}
90 changes: 90 additions & 0 deletions blockchain/state_transition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package blockchain

import (
"crypto/ecdsa"
"errors"
"fmt"
"math/big"
Expand All @@ -31,6 +32,7 @@ import (
mock_vm "github.com/kaiachain/kaia/blockchain/vm/mocks"
"github.com/kaiachain/kaia/common"
"github.com/kaiachain/kaia/crypto"
"github.com/kaiachain/kaia/fork"
"github.com/kaiachain/kaia/params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -418,3 +420,91 @@ func TestStateTransition_applyAuthorization(t *testing.T) {
})
}
}

func TestStateTransition_EIP7623(t *testing.T) {
// Prague fork block at 10
config := params.TestChainConfig.Copy()
config.IstanbulCompatibleBlock = common.Big0
config.LondonCompatibleBlock = common.Big0
config.EthTxTypeCompatibleBlock = common.Big0
config.MagmaCompatibleBlock = common.Big0
config.KoreCompatibleBlock = common.Big0
config.ShanghaiCompatibleBlock = common.Big0
config.CancunCompatibleBlock = common.Big0
config.KaiaCompatibleBlock = common.Big0
config.PragueCompatibleBlock = big.NewInt(10)
config.Governance = params.GetDefaultGovernanceConfig()
config.Governance.KIP71.LowerBoundBaseFee = 0
// Apply chain config to fork
fork.SetHardForkBlockNumberConfig(config)

var (
key, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
addr = crypto.PubkeyToAddress(key.PublicKey)
amount = big.NewInt(1000)
data = []byte{1, 2, 3, 4, 0, 0, 0, 0} // 4 non-zero bytes, 4 zero bytes
signer = types.LatestSigner(config)
gaslimit1 = uint64(21800) // 21000 + 100 * 8 (100 per byte)
gaslimit2 = uint64(21200) // 21000 + 10*4*4 + 10*4 (10 per token, 4 tokens per non-zero byte, 1 token per zero byte)
)

mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockStateDB := mock_vm.NewMockStateDB(mockCtrl)
mockStateDB.EXPECT().GetBalance(gomock.Any()).Return(big.NewInt(params.KAIA)).AnyTimes()
mockStateDB.EXPECT().SubBalance(gomock.Any(), gomock.Any()).Return().AnyTimes()
mockStateDB.EXPECT().GetKey(gomock.Any()).Return(accountkey.NewAccountKeyLegacy()).AnyTimes()
mockStateDB.EXPECT().GetNonce(gomock.Any()).Return(uint64(0)).AnyTimes()
mockStateDB.EXPECT().Prepare(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockStateDB.EXPECT().IncNonce(gomock.Any()).Return().AnyTimes()
mockStateDB.EXPECT().Snapshot().Return(1).AnyTimes()
mockStateDB.EXPECT().Exist(gomock.Any()).Return(false).AnyTimes()
mockStateDB.EXPECT().GetRefund().Return(uint64(0)).AnyTimes()
mockStateDB.EXPECT().AddBalance(gomock.Any(), gomock.Any()).Return().AnyTimes()
mockStateDB.EXPECT().CreateEOA(gomock.Any(), gomock.Any(), gomock.Any()).Return().AnyTimes()
mockStateDB.EXPECT().GetVmVersion(gomock.Any()).Return(params.VmVersion0, false).AnyTimes()
mockStateDB.EXPECT().IsProgramAccount(gomock.Any()).Return(false).AnyTimes()

var (
header *types.Header
blockContext vm.BlockContext
txContext vm.TxContext
evm *vm.EVM
res *ExecutionResult
err error
tx *types.Transaction
)

// Generate tx before Prague
tx = types.NewTransaction(0, addr, amount, gaslimit1, big.NewInt(1), data)
err = tx.SignWithKeys(signer, []*ecdsa.PrivateKey{key})
assert.NoError(t, err)
tx, err = tx.AsMessageWithAccountKeyPicker(signer, mockStateDB, 0)
assert.NoError(t, err)

header = &types.Header{Number: big.NewInt(0), Time: big.NewInt(0), BlockScore: big.NewInt(0)}
blockContext = NewEVMBlockContext(header, nil, &common.Address{})
txContext = NewEVMTxContext(tx, header, config)
evm = vm.NewEVM(blockContext, txContext, mockStateDB, config, &vm.Config{})

res, err = NewStateTransition(evm, tx).TransitionDb()
assert.NoError(t, err)
assert.Equal(t, gaslimit1, res.UsedGas)

// Generate tx after Prague
tx = types.NewTransaction(0, addr, amount, gaslimit2, big.NewInt(1), data)
err = tx.SignWithKeys(signer, []*ecdsa.PrivateKey{key})
assert.NoError(t, err)
tx, err = tx.AsMessageWithAccountKeyPicker(signer, mockStateDB, 20)
assert.NoError(t, err)

header = &types.Header{Number: big.NewInt(20), Time: big.NewInt(0), BlockScore: big.NewInt(0)}
blockContext = NewEVMBlockContext(header, nil, &common.Address{})
txContext = NewEVMTxContext(tx, header, config)
evm = vm.NewEVM(blockContext, txContext, mockStateDB, config, &vm.Config{})

res, err = NewStateTransition(evm, tx).TransitionDb()
assert.NoError(t, err)
assert.Equal(t, gaslimit2, res.UsedGas)
}
8 changes: 4 additions & 4 deletions blockchain/tx_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -837,21 +837,21 @@ func (pool *TxPool) validateTx(tx *types.Transaction) error {
}

intrGas, dataTokens, err := tx.IntrinsicGas(pool.currentBlockNumber)
intrGas += gasFrom + gasFeePayer
sigValGas := gasFrom + gasFeePayer
if err != nil {
return err
}
if tx.Gas() < intrGas {
if tx.Gas() < intrGas+sigValGas {
return ErrIntrinsicGas
}
// Ensure the transaction can cover floor data gas.
if pool.rules.IsPrague {
floorGas, err := FloorDataGas(dataTokens)
floorGas, err := FloorDataGas(tx.Type(), dataTokens, sigValGas)
if err != nil {
return err
}
if tx.Gas() < floorGas {
return fmt.Errorf("%w: gas %v, minimum needed %v", ErrDataFloorGas, tx.Gas(), floorGas)
return fmt.Errorf("%w: gas %v, minimum needed %v", ErrFloorDataGas, tx.Gas(), floorGas)
}
}

Expand Down
36 changes: 24 additions & 12 deletions blockchain/types/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,18 @@ func ErrFeePayer(err error) error {
return fmt.Errorf("invalid fee payer: %s", err)
}

type ValidatedIntrinsicGas struct {
Gas uint64
Tokens uint64
// ValidatedGas holds the intrinsic gas, sig validation gas, and data tokens.
// - Intrinsic gas is the gas for the tx type + signature validation + data.
// After Prague, floor gas would be used if intrinsic gas < floor gas.
// Note that SigValidationGas is already included in IntrinsicGas.
// - Sig validation gas is the gas for validating sender and feePayer.
// It is related to Kaia-specific tx types, so it is not part of the floor gas comparison.
// - Tokens is the number of tokens for the data.
// It is used for the floor gas calculation.
type ValidatedGas struct {
IntrinsicGas uint64
SigValidateGas uint64
Tokens uint64
}

type Transaction struct {
Expand All @@ -93,9 +102,9 @@ type Transaction struct {
// validatedFeePayer represents the fee payer of the transaction to be used for ApplyTransaction().
// This value is set in AsMessageWithAccountKeyPicker().
validatedFeePayer common.Address
// validatedIntrinsicGas represents intrinsic gas of the transaction to be used for ApplyTransaction().
// validatedGas holds intrinsic gas, sig validation gas, and number of tokens for the transaction to be used for ApplyTransaction().
// This value is set in AsMessageWithAccountKeyPicker().
validatedIntrinsicGas *ValidatedIntrinsicGas
validatedGas *ValidatedGas
// The account's nonce is checked only if `checkNonce` is true.
checkNonce bool
// This value is set when the tx is invalidated in block tx validation, and is used to remove pending tx in txPool.
Expand Down Expand Up @@ -390,10 +399,10 @@ func (tx *Transaction) ValidatedFeePayer() common.Address {
return tx.validatedFeePayer
}

func (tx *Transaction) ValidatedIntrinsicGas() *ValidatedIntrinsicGas {
func (tx *Transaction) ValidatedGas() *ValidatedGas {
tx.mu.RLock()
defer tx.mu.RUnlock()
return tx.validatedIntrinsicGas
return tx.validatedGas
}
func (tx *Transaction) MakeRPCOutput() map[string]interface{} { return tx.data.MakeRPCOutput() }
func (tx *Transaction) GetTxInternalData() TxInternalData { return tx.data }
Expand Down Expand Up @@ -611,8 +620,11 @@ func (tx *Transaction) AsMessageWithAccountKeyPicker(s Signer, picker AccountKey
}
}

sigValidationGas := gasFrom + gasFeePayer
intrinsicGas = intrinsicGas + sigValidationGas

tx.mu.Lock()
tx.validatedIntrinsicGas = &ValidatedIntrinsicGas{Gas: intrinsicGas + gasFrom + gasFeePayer, Tokens: dataTokens}
tx.validatedGas = &ValidatedGas{IntrinsicGas: intrinsicGas, SigValidateGas: sigValidationGas, Tokens: dataTokens}
tx.mu.Unlock()

return tx, err
Expand Down Expand Up @@ -1086,10 +1098,10 @@ func (t *TransactionsByPriceAndNonce) Clear() {
// NewMessage returns a `*Transaction` object with the given arguments.
func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *big.Int, gasLimit uint64, gasPrice, gasFeeCap, gasTipCap *big.Int, data []byte, checkNonce bool, intrinsicGas uint64, dataTokens uint64, list AccessList, auth []SetCodeAuthorization) *Transaction {
transaction := &Transaction{
validatedIntrinsicGas: &ValidatedIntrinsicGas{Gas: intrinsicGas, Tokens: dataTokens},
validatedFeePayer: from,
validatedSender: from,
checkNonce: checkNonce,
validatedGas: &ValidatedGas{IntrinsicGas: intrinsicGas, SigValidateGas: 0, Tokens: dataTokens},
validatedFeePayer: from,
validatedSender: from,
checkNonce: checkNonce,
}

// Call supports EthereumAccessList, EthereumSetCode and Legacy txTypes only.
Expand Down
37 changes: 37 additions & 0 deletions blockchain/types/tx_internal_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"bytes"
"crypto/ecdsa"
"errors"
"fmt"
"math"
"math/big"

Expand Down Expand Up @@ -697,6 +698,42 @@ func IntrinsicGas(data []byte, accessList AccessList, authorizationList []SetCod
return gasPayloadWithGas, tokens, nil
}

var txTypeToGasMap = map[TxType]uint64{
TxTypeLegacyTransaction: params.TxGas,
TxTypeValueTransfer: params.TxGasValueTransfer,
TxTypeFeeDelegatedValueTransfer: params.TxGasValueTransfer + params.TxGasFeeDelegated,
TxTypeFeeDelegatedValueTransferWithRatio: params.TxGasValueTransfer + params.TxGasFeeDelegatedWithRatio,
TxTypeValueTransferMemo: params.TxGasValueTransfer,
TxTypeFeeDelegatedValueTransferMemo: params.TxGasValueTransfer + params.TxGasFeeDelegated,
TxTypeFeeDelegatedValueTransferMemoWithRatio: params.TxGasValueTransfer + params.TxGasFeeDelegatedWithRatio,
TxTypeAccountCreation: params.TxGasAccountCreation,
TxTypeAccountUpdate: params.TxGasAccountUpdate,
TxTypeFeeDelegatedAccountUpdate: params.TxGasAccountUpdate + params.TxGasFeeDelegated,
TxTypeFeeDelegatedAccountUpdateWithRatio: params.TxGasAccountUpdate + params.TxGasFeeDelegatedWithRatio,
TxTypeSmartContractDeploy: params.TxGasContractCreation,
TxTypeFeeDelegatedSmartContractDeploy: params.TxGasContractCreation + params.TxGasFeeDelegated,
TxTypeFeeDelegatedSmartContractDeployWithRatio: params.TxGasContractCreation + params.TxGasFeeDelegatedWithRatio,
TxTypeSmartContractExecution: params.TxGasContractExecution,
TxTypeFeeDelegatedSmartContractExecution: params.TxGasContractExecution + params.TxGasFeeDelegated,
TxTypeFeeDelegatedSmartContractExecutionWithRatio: params.TxGasContractExecution + params.TxGasFeeDelegatedWithRatio,
TxTypeCancel: params.TxGasCancel,
TxTypeFeeDelegatedCancel: params.TxGasCancel + params.TxGasFeeDelegated,
TxTypeFeeDelegatedCancelWithRatio: params.TxGasCancel + params.TxGasFeeDelegatedWithRatio,
TxTypeChainDataAnchoring: params.TxChainDataAnchoringGas,
TxTypeFeeDelegatedChainDataAnchoring: params.TxChainDataAnchoringGas + params.TxGasFeeDelegated,
TxTypeFeeDelegatedChainDataAnchoringWithRatio: params.TxChainDataAnchoringGas + params.TxGasFeeDelegatedWithRatio,
TxTypeEthereumAccessList: params.TxGas,
TxTypeEthereumDynamicFee: params.TxGas,
TxTypeEthereumSetCode: params.TxGas,
}

func GetTxGasForTxType(txType TxType) (uint64, error) {
if gas, exists := txTypeToGasMap[txType]; exists {
return gas, nil
}
return 0, fmt.Errorf("cannot find txGas for txType %s", txType.String())
}

// CalcFeeWithRatio returns feePayer's fee and sender's fee based on feeRatio.
// For example, if fee = 100 and feeRatio = 30, feePayer = 30 and feeSender = 70.
func CalcFeeWithRatio(feeRatio FeeRatio, fee *big.Int) (*big.Int, *big.Int) {
Expand Down
Loading
Loading