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

Test fee and tip caps NIT-2110 #2200

Merged
merged 11 commits into from
Mar 25, 2024
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)
}

}
Loading