Skip to content

Commit

Permalink
Merge pull request #2200 from OffchainLabs/test-feeAndTipCaps
Browse files Browse the repository at this point in the history
Test fee and tip caps NIT-2110
  • Loading branch information
Tristan-Wilson committed Mar 25, 2024
2 parents 703c626 + 9be62f6 commit 0399e76
Show file tree
Hide file tree
Showing 2 changed files with 309 additions and 7 deletions.
21 changes: 14 additions & 7 deletions arbnode/dataposter/data_poster.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,13 +469,10 @@ func (p *DataPoster) evalMaxFeeCapExpr(backlogOfBatches uint64, elapsed time.Dur
var big4 = big.NewInt(4)

// The dataPosterBacklog argument should *not* include extraBacklog (it's added in in this function)
func (p *DataPoster) feeAndTipCaps(ctx context.Context, nonce uint64, gasLimit uint64, numBlobs uint64, lastTx *types.Transaction, dataCreatedAt time.Time, dataPosterBacklog uint64) (*big.Int, *big.Int, *big.Int, error) {
func (p *DataPoster) feeAndTipCaps(ctx context.Context, nonce uint64, gasLimit uint64, numBlobs uint64, lastTx *types.Transaction, dataCreatedAt time.Time, dataPosterBacklog uint64, latestHeader *types.Header) (*big.Int, *big.Int, *big.Int, error) {
config := p.config()
dataPosterBacklog += p.extraBacklog()
latestHeader, err := p.headerReader.LastHeader(ctx)
if err != nil {
return nil, nil, nil, err
}

if latestHeader.BaseFee == nil {
return nil, nil, nil, fmt.Errorf("latest parent chain block %v missing BaseFee (either the parent chain does not have EIP-1559 or the parent chain node is not synced)", latestHeader.Number)
}
Expand Down Expand Up @@ -692,7 +689,12 @@ func (p *DataPoster) PostTransaction(ctx context.Context, dataCreatedAt time.Tim
return nil, fmt.Errorf("failed to update data poster balance: %w", err)
}

feeCap, tipCap, blobFeeCap, err := p.feeAndTipCaps(ctx, nonce, gasLimit, uint64(len(kzgBlobs)), nil, dataCreatedAt, 0)
latestHeader, err := p.headerReader.LastHeader(ctx)
if err != nil {
return nil, err
}

feeCap, tipCap, blobFeeCap, err := p.feeAndTipCaps(ctx, nonce, gasLimit, uint64(len(kzgBlobs)), nil, dataCreatedAt, 0, latestHeader)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -877,7 +879,12 @@ func updateGasCaps(tx *types.Transaction, newFeeCap, newTipCap, newBlobFeeCap *b

// The mutex must be held by the caller.
func (p *DataPoster) replaceTx(ctx context.Context, prevTx *storage.QueuedTransaction, backlogWeight uint64) error {
newFeeCap, newTipCap, newBlobFeeCap, err := p.feeAndTipCaps(ctx, prevTx.FullTx.Nonce(), prevTx.FullTx.Gas(), uint64(len(prevTx.FullTx.BlobHashes())), prevTx.FullTx, prevTx.Created, backlogWeight)
latestHeader, err := p.headerReader.LastHeader(ctx)
if err != nil {
return err
}

newFeeCap, newTipCap, newBlobFeeCap, err := p.feeAndTipCaps(ctx, prevTx.FullTx.Nonce(), prevTx.FullTx.Gas(), uint64(len(prevTx.FullTx.BlobHashes())), prevTx.FullTx, prevTx.Created, backlogWeight, latestHeader)
if err != nil {
return err
}
Expand Down
295 changes: 295 additions & 0 deletions arbnode/dataposter/dataposter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ import (
"time"

"github.com/Knetic/govaluate"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/google/go-cmp/cmp"
"github.com/holiman/uint256"
"github.com/offchainlabs/nitro/arbnode/dataposter/externalsigner"
"github.com/offchainlabs/nitro/arbnode/dataposter/externalsignertest"
"github.com/offchainlabs/nitro/util/arbmath"
)

func TestParseReplacementTimes(t *testing.T) {
Expand Down Expand Up @@ -187,3 +192,293 @@ func TestMaxFeeCapFormulaCalculation(t *testing.T) {
t.Fatalf("Unexpected result. Got: %d, want: >0", result)
}
}

type stubL1Client struct {
senderNonce uint64
suggestedGasTipCap *big.Int

// Define most of the required methods that aren't used by feeAndTipCaps
backends.SimulatedBackend
}

func (c *stubL1Client) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) {
return c.senderNonce, nil
}

func (c *stubL1Client) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
return c.suggestedGasTipCap, nil
}

// Not used but we need to define
func (c *stubL1Client) BlockNumber(ctx context.Context) (uint64, error) {
return 0, nil
}

func (c *stubL1Client) CallContractAtHash(ctx context.Context, msg ethereum.CallMsg, blockHash common.Hash) ([]byte, error) {
return []byte{}, nil
}

func (c *stubL1Client) ChainID(ctx context.Context) (*big.Int, error) {
return nil, nil
}

func (c *stubL1Client) Client() rpc.ClientInterface {
return nil
}

func (c *stubL1Client) TransactionSender(ctx context.Context, tx *types.Transaction, block common.Hash, index uint) (common.Address, error) {
return common.Address{}, nil
}

func TestFeeAndTipCaps_EnoughBalance_NoBacklog_NoUnconfirmed_BlobTx(t *testing.T) {
conf := func() *DataPosterConfig {
// Set only the fields that are used by feeAndTipCaps
// Start with defaults, maybe change for test.
return &DataPosterConfig{
MaxMempoolTransactions: 18,
MaxMempoolWeight: 18,
MinTipCapGwei: 0.05,
MinBlobTxTipCapGwei: 1,
MaxTipCapGwei: 5,
MaxBlobTxTipCapGwei: 10,
MaxFeeBidMultipleBips: arbmath.OneInBips * 10,
AllocateMempoolBalance: true,

UrgencyGwei: 2.,
ElapsedTimeBase: 10 * time.Minute,
ElapsedTimeImportance: 10,
TargetPriceGwei: 60.,
}
}
expression, err := govaluate.NewEvaluableExpression(DefaultDataPosterConfig.MaxFeeCapFormula)
if err != nil {
t.Fatalf("error creating govaluate evaluable expression: %v", err)
}

p := DataPoster{
config: conf,
extraBacklog: func() uint64 { return 0 },
balance: big.NewInt(0).Mul(big.NewInt(params.Ether), big.NewInt(10)),
usingNoOpStorage: false,
client: &stubL1Client{
senderNonce: 1,
suggestedGasTipCap: big.NewInt(2 * params.GWei),
},
auth: &bind.TransactOpts{
From: common.Address{},
},
maxFeeCapExpression: expression,
}

ctx := context.Background()
var nonce uint64 = 1
var gasLimit uint64 = 300_000 // reasonable upper bound for mainnet blob batches
var numBlobs uint64 = 6
var lastTx *types.Transaction // PostTransaction leaves this nil, used when replacing
dataCreatedAt := time.Now()
var dataPosterBacklog uint64 = 0 // Zero backlog for PostTransaction
var blobGasUsed uint64 = 0xc0000 // 6 blobs of gas
var excessBlobGas uint64 = 0 // typical current mainnet conditions
latestHeader := types.Header{
Number: big.NewInt(1),
BaseFee: big.NewInt(1_000_000_000),
BlobGasUsed: &blobGasUsed,
ExcessBlobGas: &excessBlobGas,
}

newGasFeeCap, newTipCap, newBlobFeeCap, err := p.feeAndTipCaps(ctx, nonce, gasLimit, numBlobs, lastTx, dataCreatedAt, dataPosterBacklog, &latestHeader)
if err != nil {
t.Fatalf("%s", err)
}

// There is no backlog and almost no time elapses since the batch data was
// created to when it was posted so the maxNormalizedFeeCap is ~60.01 gwei.
// This is multiplied with the normalizedGas to get targetMaxCost.
// This is greatly in excess of currentTotalCost * MaxFeeBidMultipleBips,
// so targetMaxCost is reduced to the current base fee + suggested tip cap +
// current blob fee multipled by MaxFeeBidMultipleBips (factor of 10).
// The blob and non blob factors are then proportionally split out and so
// the newGasFeeCap is set to (current base fee + suggested tip cap) * 10
// and newBlobFeeCap is set to current blob gas base fee (1 wei
// since there is no excess blob gas) * 10.
expectedGasFeeCap := big.NewInt(30 * params.GWei)
expectedBlobFeeCap := big.NewInt(10)
if !arbmath.BigEquals(expectedGasFeeCap, newGasFeeCap) {
t.Fatalf("feeAndTipCaps didn't return expected gas fee cap. Was: %d, expected: %d", expectedGasFeeCap, newGasFeeCap)
}
if !arbmath.BigEquals(expectedBlobFeeCap, newBlobFeeCap) {
t.Fatalf("feeAndTipCaps didn't return expected blob gas fee cap. Was: %d, expected: %d", expectedBlobFeeCap, newBlobFeeCap)
}

// 2 gwei is the amount suggested by the L1 client, so that is the value
// returned because it doesn't exceed the configured bounds, there is no
// lastTx to scale against with rbf, and it is not bigger than the computed
// gasFeeCap.
expectedTipCap := big.NewInt(2 * params.GWei)
if !arbmath.BigEquals(expectedTipCap, newTipCap) {
t.Fatalf("feeAndTipCaps didn't return expected tip cap. Was: %d, expected: %d", expectedTipCap, newTipCap)
}

lastBlobTx := &types.BlobTx{}
err = updateTxDataGasCaps(lastBlobTx, newGasFeeCap, newTipCap, newBlobFeeCap)
if err != nil {
t.Fatal(err)
}
lastTx = types.NewTx(lastBlobTx)
// Make creation time go backwards so elapsed time increases
retconnedCreationTime := dataCreatedAt.Add(-time.Minute)
// Base fee needs to have increased to simulate conditions to not include prev tx
latestHeader = types.Header{
Number: big.NewInt(2),
BaseFee: big.NewInt(32_000_000_000),
BlobGasUsed: &blobGasUsed,
ExcessBlobGas: &excessBlobGas,
}

newGasFeeCap, newTipCap, newBlobFeeCap, err = p.feeAndTipCaps(ctx, nonce, gasLimit, numBlobs, lastTx, retconnedCreationTime, dataPosterBacklog, &latestHeader)
_, _, _, _ = newGasFeeCap, newTipCap, newBlobFeeCap, err
/*
// I think we expect an increase by *2 due to rbf rules for blob txs,
// currently appears to be broken since the increase exceeds the
// current cost (based on current basefees and tip) * config.MaxFeeBidMultipleBips
// since the previous attempt to send the tx was already using the current cost scaled by
// the multiple (* 10 bips).
expectedGasFeeCap = expectedGasFeeCap.Mul(expectedGasFeeCap, big.NewInt(2))
expectedBlobFeeCap = expectedBlobFeeCap.Mul(expectedBlobFeeCap, big.NewInt(2))
expectedTipCap = expectedTipCap.Mul(expectedTipCap, big.NewInt(2))
t.Log("newGasFeeCap", newGasFeeCap, "newTipCap", newTipCap, "newBlobFeeCap", newBlobFeeCap, "err", err)
if !arbmath.BigEquals(expectedGasFeeCap, newGasFeeCap) {
t.Fatalf("feeAndTipCaps didn't return expected gas fee cap. Was: %d, expected: %d", expectedGasFeeCap, newGasFeeCap)
}
if !arbmath.BigEquals(expectedBlobFeeCap, newBlobFeeCap) {
t.Fatalf("feeAndTipCaps didn't return expected blob gas fee cap. Was: %d, expected: %d", expectedBlobFeeCap, newBlobFeeCap)
}
if !arbmath.BigEquals(expectedTipCap, newTipCap) {
t.Fatalf("feeAndTipCaps didn't return expected tip cap. Was: %d, expected: %d", expectedTipCap, newTipCap)
}
*/

}

func TestFeeAndTipCaps_RBF_RisingBlobFee_FallingBaseFee(t *testing.T) {
conf := func() *DataPosterConfig {
// Set only the fields that are used by feeAndTipCaps
// Start with defaults, maybe change for test.
return &DataPosterConfig{
MaxMempoolTransactions: 18,
MaxMempoolWeight: 18,
MinTipCapGwei: 0.05,
MinBlobTxTipCapGwei: 1,
MaxTipCapGwei: 5,
MaxBlobTxTipCapGwei: 10,
MaxFeeBidMultipleBips: arbmath.OneInBips * 10,
AllocateMempoolBalance: true,

UrgencyGwei: 2.,
ElapsedTimeBase: 10 * time.Minute,
ElapsedTimeImportance: 10,
TargetPriceGwei: 60.,
}
}
expression, err := govaluate.NewEvaluableExpression(DefaultDataPosterConfig.MaxFeeCapFormula)
if err != nil {
t.Fatalf("error creating govaluate evaluable expression: %v", err)
}

p := DataPoster{
config: conf,
extraBacklog: func() uint64 { return 0 },
balance: big.NewInt(0).Mul(big.NewInt(params.Ether), big.NewInt(10)),
usingNoOpStorage: false,
client: &stubL1Client{
senderNonce: 1,
suggestedGasTipCap: big.NewInt(2 * params.GWei),
},
auth: &bind.TransactOpts{
From: common.Address{},
},
maxFeeCapExpression: expression,
}

ctx := context.Background()
var nonce uint64 = 1
var gasLimit uint64 = 300_000 // reasonable upper bound for mainnet blob batches
var numBlobs uint64 = 6
var lastTx *types.Transaction // PostTransaction leaves this nil, used when replacing
dataCreatedAt := time.Now()
var dataPosterBacklog uint64 = 0 // Zero backlog for PostTransaction
var blobGasUsed uint64 = 0xc0000 // 6 blobs of gas
var excessBlobGas uint64 = 0 // typical current mainnet conditions
latestHeader := types.Header{
Number: big.NewInt(1),
BaseFee: big.NewInt(1_000_000_000),
BlobGasUsed: &blobGasUsed,
ExcessBlobGas: &excessBlobGas,
}

newGasFeeCap, newTipCap, newBlobFeeCap, err := p.feeAndTipCaps(ctx, nonce, gasLimit, numBlobs, lastTx, dataCreatedAt, dataPosterBacklog, &latestHeader)
if err != nil {
t.Fatalf("%s", err)
}

// There is no backlog and almost no time elapses since the batch data was
// created to when it was posted so the maxNormalizedFeeCap is ~60.01 gwei.
// This is multiplied with the normalizedGas to get targetMaxCost.
// This is greatly in excess of currentTotalCost * MaxFeeBidMultipleBips,
// so targetMaxCost is reduced to the current base fee + suggested tip cap +
// current blob fee multipled by MaxFeeBidMultipleBips (factor of 10).
// The blob and non blob factors are then proportionally split out and so
// the newGasFeeCap is set to (current base fee + suggested tip cap) * 10
// and newBlobFeeCap is set to current blob gas base fee (1 wei
// since there is no excess blob gas) * 10.
expectedGasFeeCap := big.NewInt(30 * params.GWei)
expectedBlobFeeCap := big.NewInt(10)
if !arbmath.BigEquals(expectedGasFeeCap, newGasFeeCap) {
t.Fatalf("feeAndTipCaps didn't return expected gas fee cap. Was: %d, expected: %d", expectedGasFeeCap, newGasFeeCap)
}
if !arbmath.BigEquals(expectedBlobFeeCap, newBlobFeeCap) {
t.Fatalf("feeAndTipCaps didn't return expected blob gas fee cap. Was: %d, expected: %d", expectedBlobFeeCap, newBlobFeeCap)
}

// 2 gwei is the amount suggested by the L1 client, so that is the value
// returned because it doesn't exceed the configured bounds, there is no
// lastTx to scale against with rbf, and it is not bigger than the computed
// gasFeeCap.
expectedTipCap := big.NewInt(2 * params.GWei)
if !arbmath.BigEquals(expectedTipCap, newTipCap) {
t.Fatalf("feeAndTipCaps didn't return expected tip cap. Was: %d, expected: %d", expectedTipCap, newTipCap)
}

lastBlobTx := &types.BlobTx{}
err = updateTxDataGasCaps(lastBlobTx, newGasFeeCap, newTipCap, newBlobFeeCap)
if err != nil {
t.Fatal(err)
}
lastTx = types.NewTx(lastBlobTx)
// Make creation time go backwards so elapsed time increases
retconnedCreationTime := dataCreatedAt.Add(-time.Minute)
// Base fee has decreased but blob fee has increased
blobGasUsed = 0xc0000 // 6 blobs of gas
excessBlobGas = 8295804 // this should set blob fee to 12 wei
latestHeader = types.Header{
Number: big.NewInt(2),
BaseFee: big.NewInt(100_000_000),
BlobGasUsed: &blobGasUsed,
ExcessBlobGas: &excessBlobGas,
}

newGasFeeCap, newTipCap, newBlobFeeCap, err = p.feeAndTipCaps(ctx, nonce, gasLimit, numBlobs, lastTx, retconnedCreationTime, dataPosterBacklog, &latestHeader)

t.Log("newGasFeeCap", newGasFeeCap, "newTipCap", newTipCap, "newBlobFeeCap", newBlobFeeCap, "err", err)
if arbmath.BigEquals(expectedGasFeeCap, newGasFeeCap) {
t.Fatalf("feeAndTipCaps didn't return expected gas fee cap. Was: %d, expected NOT: %d", expectedGasFeeCap, newGasFeeCap)
}
if arbmath.BigEquals(expectedBlobFeeCap, newBlobFeeCap) {
t.Fatalf("feeAndTipCaps didn't return expected blob gas fee cap. Was: %d, expected NOT: %d", expectedBlobFeeCap, newBlobFeeCap)
}
if arbmath.BigEquals(expectedTipCap, newTipCap) {
t.Fatalf("feeAndTipCaps didn't return expected tip cap. Was: %d, expected NOT: %d", expectedTipCap, newTipCap)
}

}

0 comments on commit 0399e76

Please sign in to comment.