Skip to content

Commit

Permalink
Merge pull request #59 from siburu/price-bump
Browse files Browse the repository at this point in the history
Take price bump into consideration
  • Loading branch information
siburu authored Dec 26, 2024
2 parents ffd6071 + ad43f88 commit 30310f2
Show file tree
Hide file tree
Showing 8 changed files with 414 additions and 73 deletions.
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ require (
github.com/cosmos/ibc-go/modules/capability v1.0.0 // indirect
github.com/cosmos/ics23/go v0.10.0 // indirect
github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
Expand Down Expand Up @@ -96,6 +97,7 @@ require (
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/glog v1.2.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
Expand All @@ -114,6 +116,7 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/hashicorp/go-bexpr v0.1.10 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-getter v1.7.1 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
Expand All @@ -126,12 +129,15 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/holiman/uint256 v1.3.1 // indirect
github.com/huandu/skiplist v1.2.0 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/improbable-eng/grpc-web v0.15.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
Expand All @@ -149,6 +155,7 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/pointerstructure v1.2.0 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
Expand All @@ -168,6 +175,7 @@ require (
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/rs/cors v1.8.3 // indirect
github.com/rs/zerolog v1.32.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sasha-s/go-deadlock v0.3.1 // indirect
Expand All @@ -184,6 +192,8 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/urfave/cli/v2 v2.25.7 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/zondax/hid v0.9.2 // indirect
github.com/zondax/ledger-go v0.14.3 // indirect
go.etcd.io/bbolt v1.3.8 // indirect
Expand Down Expand Up @@ -214,6 +224,7 @@ require (
google.golang.org/grpc v1.62.0 // indirect
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect
Expand Down
4 changes: 1 addition & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,6 @@ github.com/cosmos/ics23/go v0.10.0 h1:iXqLLgp2Lp+EdpIuwXTYIQU+AiHj9mOC2X9ab++bZD
github.com/cosmos/ics23/go v0.10.0/go.mod h1:ZfJSmng/TBNTBkFemHHHj5YY7VAU/MBU980F4VU1NG0=
github.com/cosmos/ledger-cosmos-go v0.13.3 h1:7ehuBGuyIytsXbd4MP43mLeoN2LTOEnk5nvue4rK+yM=
github.com/cosmos/ledger-cosmos-go v0.13.3/go.mod h1:HENcEP+VtahZFw38HZ3+LS3Iv5XV6svsnkk9vdJtLr8=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
Expand Down Expand Up @@ -859,6 +858,7 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
Expand Down Expand Up @@ -1007,7 +1007,6 @@ github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
Expand Down Expand Up @@ -1105,7 +1104,6 @@ github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
Expand Down
111 changes: 111 additions & 0 deletions pkg/client/txpool/txpool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package txpool

import (
"context"
"math/big"
"slices"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)

// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction
type RPCTransaction struct {
BlockHash *common.Hash `json:"blockHash"`
BlockNumber *hexutil.Big `json:"blockNumber"`
From common.Address `json:"from"`
Gas hexutil.Uint64 `json:"gas"`
GasPrice *hexutil.Big `json:"gasPrice"`
GasFeeCap *hexutil.Big `json:"maxFeePerGas,omitempty"`
GasTipCap *hexutil.Big `json:"maxPriorityFeePerGas,omitempty"`
MaxFeePerBlobGas *hexutil.Big `json:"maxFeePerBlobGas,omitempty"`
Hash common.Hash `json:"hash"`
Input hexutil.Bytes `json:"input"`
Nonce hexutil.Uint64 `json:"nonce"`
To *common.Address `json:"to"`
TransactionIndex *hexutil.Uint64 `json:"transactionIndex"`
Value *hexutil.Big `json:"value"`
Type hexutil.Uint64 `json:"type"`
Accesses *types.AccessList `json:"accessList,omitempty"`
ChainID *hexutil.Big `json:"chainId,omitempty"`
BlobVersionedHashes []common.Hash `json:"blobVersionedHashes,omitempty"`
V *hexutil.Big `json:"v"`
R *hexutil.Big `json:"r"`
S *hexutil.Big `json:"s"`
YParity *hexutil.Uint64 `json:"yParity,omitempty"`
}

// ContentFrom calls `txpool_contentFrom` of the Ethereum RPC
func ContentFrom(ctx context.Context, client *ethclient.Client, address common.Address) (map[string]map[string]*RPCTransaction, error) {
var res map[string]map[string]*RPCTransaction
if err := client.Client().CallContext(ctx, &res, "txpool_contentFrom", address); err != nil {
return nil, err
}
return res, nil
}

// PendingTransactions returns pending txs sent from `address` sorted by nonce.
func PendingTransactions(ctx context.Context, client *ethclient.Client, address common.Address) ([]*RPCTransaction, error) {
txs, err := ContentFrom(ctx, client, address)
if err != nil {
return nil, err
}

pendingTxMap, found := txs["pending"]
if !found {
return nil, nil
}

var pendingTxs []*RPCTransaction
for _, pendingTx := range pendingTxMap {
pendingTxs = append(pendingTxs, pendingTx)
}

slices.SortFunc(pendingTxs, func(a, b *RPCTransaction) int {
if a.Nonce < b.Nonce {
return -1
} else if a.Nonce > b.Nonce {
return 1
} else {
return 0
}
})

return pendingTxs, nil
}

func inclByPercent(n *big.Int, percent uint64) {
n.Mul(n, big.NewInt(int64(100+percent)))
n.Div(n, big.NewInt(100))
}

// GetMinimumRequiredFee returns the minimum fee required to successfully send a transaction
func GetMinimumRequiredFee(ctx context.Context, client *ethclient.Client, address common.Address, nonce uint64, priceBump uint64) (*big.Int, *big.Int, error) {
pendingTxs, err := PendingTransactions(ctx, client, address)
if err != nil {
return nil, nil, err
} else if len(pendingTxs) == 0 {
return common.Big0, common.Big0, nil
}

var targetTx *RPCTransaction
for _, pendingTx := range pendingTxs {
if uint64(pendingTx.Nonce) == nonce {
targetTx = pendingTx
break
}
}
if targetTx == nil {
return common.Big0, common.Big0, nil
}

gasFeeCap := targetTx.GasFeeCap.ToInt()
gasTipCap := targetTx.GasTipCap.ToInt()

inclByPercent(gasFeeCap, priceBump)
inclByPercent(gasTipCap, priceBump)

return gasFeeCap, gasTipCap, nil
}
179 changes: 179 additions & 0 deletions pkg/client/txpool/txpool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package txpool

import (
"context"
"crypto/ecdsa"
"math/big"
"testing"
"unsafe"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/ethclient/simulated"
"github.com/ethereum/go-ethereum/node"
)

func makeGenesisAlloc(t *testing.T, n int) ([]common.Address, types.GenesisAlloc) {
var addrs []common.Address
alloc := make(types.GenesisAlloc)
for i := 0; i < n; i++ {
key, err := crypto.GenerateKey()
if err != nil {
t.Fatalf("failed to generate a private key: err=%v", err)
}

addr := crypto.PubkeyToAddress(key.PublicKey)

addrs = append(addrs, addr)
alloc[addr] = types.Account{
Balance: new(big.Int).Lsh(common.Big1, 250),
PrivateKey: crypto.FromECDSA(key),
}
}
return addrs, alloc
}

func getPrivateKey(t *testing.T, account types.Account) *ecdsa.PrivateKey {
key, err := crypto.ToECDSA(account.PrivateKey)
if err != nil {
t.Fatalf("failed to unmarshal the private key: err=%v", err)
}
return key
}

func getEthClient(sim *simulated.Backend) *ethclient.Client {
type simClient struct {
*ethclient.Client
}
ifceCli := sim.Client()
ptrCli := unsafe.Add(unsafe.Pointer(&ifceCli), unsafe.Sizeof(uintptr(0)))
return (*simClient)(ptrCli).Client
}

func transfer(t *testing.T, ctx context.Context, client *ethclient.Client, signer types.Signer, key *ecdsa.PrivateKey, nonce uint64, gasTipCap, gasFeeCap *big.Int, to common.Address, amount *big.Int) {
tx := types.NewTx(&types.DynamicFeeTx{
Nonce: nonce,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Gas: 21000,
To: &to,
Value: amount,
})

tx, err := types.SignTx(tx, signer, key)
if err != nil {
t.Fatalf("failed to sign tx: err=%v", err)
}

if err := client.SendTransaction(ctx, tx); err != nil {
t.Fatalf("failed to send tx: err=%v", err)
}
}

func replace(t *testing.T, ctx context.Context, client *ethclient.Client, signer types.Signer, key *ecdsa.PrivateKey, priceBump uint64, nonce uint64, gasTipCap, gasFeeCap *big.Int, to common.Address, amount *big.Int) {
addr := crypto.PubkeyToAddress(key.PublicKey)

if minFeeCap, minTipCap, err := GetMinimumRequiredFee(ctx, client, addr, nonce, priceBump); err != nil {
t.Fatalf("failed to get the minimum fee required to replace tx: err=%v", err)
} else if minFeeCap.Cmp(common.Big0) == 0 {
t.Fatalf("tx to replace not found")
} else {
t.Logf("minimum required fees: feeCap=%v, tipCap=%v", minFeeCap, minTipCap)
if gasFeeCap.Cmp(minFeeCap) < 0 {
t.Logf("gasFeeCap updated: %v => %v", gasFeeCap, minFeeCap)
gasFeeCap = minFeeCap
}
if gasTipCap.Cmp(minTipCap) < 0 {
t.Logf("gasTipCap updated: %v => %v", gasTipCap, minTipCap)
gasTipCap = minTipCap
}
}

transfer(t, ctx, client, signer, key, nonce, gasTipCap, gasFeeCap, to, amount)
}

func TestContentFrom(t *testing.T) {
ctx := context.Background()

addrs, alloc := makeGenesisAlloc(t, 2)
sender, receiver := addrs[0], addrs[1]
senderKey := getPrivateKey(t, alloc[sender])

priceBump := uint64(25)
sim := simulated.NewBackend(alloc, func(nodeConf *node.Config, ethConf *ethconfig.Config) {
t.Logf("original price bump is %v", ethConf.TxPool.PriceBump)
ethConf.TxPool.PriceBump = priceBump
})
defer sim.Close()

cli := getEthClient(sim)

// make signer
chainID, err := cli.ChainID(ctx)
if err != nil {
t.Fatalf("failed to get chain ID: err=%v", err)
}
signer := types.LatestSignerForChainID(chainID)

// we use small fee cap to prepend txs from being included
feeCap := big.NewInt(1_000_000)
tipCap := big.NewInt(1_000_000)
amount := big.NewInt(1_000_000_000) // 1 GWei

transfer(t, ctx, cli, signer, senderKey, 0, tipCap, feeCap, receiver, amount)
for i := 0; i < 10; i++ {
replace(t, ctx, cli, signer, senderKey, priceBump, 0, tipCap, feeCap, receiver, amount)
}

// check block info
block, err := cli.BlockByNumber(ctx, nil)
if err != nil {
t.Fatalf("failed to get block by number: err=%v", err)
} else {
t.Logf("bn=%v, basefee=%v", block.Number(), block.BaseFee())
}

// commit block
sim.Commit()

// check block info
block, err = cli.BlockByNumber(ctx, nil)
if err != nil {
t.Fatalf("failed to get block by number: err=%v", err)
} else {
t.Logf("bn=%v, basefee=%v", block.Number(), block.BaseFee())
}

// check that len(pendingTxs) == 1
if pendingTxs, err := PendingTransactions(ctx, cli, sender); err != nil {
t.Fatalf("failed to get pending transactions: err=%v", err)
} else if len(pendingTxs) != 1 {
t.Fatalf("unexpected pending txs: pendingTxs=%v", pendingTxs)
}

// update fee cap to allow the tx to be included
feeCap = block.BaseFee()

replace(t, ctx, cli, signer, senderKey, priceBump, 0, tipCap, feeCap, receiver, amount)

// commit block
sim.Commit()

// check block info
block, err = cli.BlockByNumber(ctx, nil)
if err != nil {
t.Fatalf("failed to get block by number: err=%v", err)
} else {
t.Logf("bn=%v, basefee=%v", block.Number(), block.BaseFee())
}

// check that len(pendingTxs) == 0
if pendingTxs, err := PendingTransactions(ctx, cli, sender); err != nil {
t.Fatalf("failed to get pending transactions: err=%v", err)
} else if len(pendingTxs) != 0 {
t.Fatalf("unexpected pending txs: pendingTxs=%v", pendingTxs)
}
}
Loading

0 comments on commit 30310f2

Please sign in to comment.