Skip to content

Commit 30310f2

Browse files
authored
Merge pull request #59 from siburu/price-bump
Take price bump into consideration
2 parents ffd6071 + ad43f88 commit 30310f2

File tree

8 files changed

+414
-73
lines changed

8 files changed

+414
-73
lines changed

go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ require (
6767
github.com/cosmos/ibc-go/modules/capability v1.0.0 // indirect
6868
github.com/cosmos/ics23/go v0.10.0 // indirect
6969
github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect
70+
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
7071
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect
7172
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
7273
github.com/danieljoos/wincred v1.1.2 // indirect
@@ -96,6 +97,7 @@ require (
9697
github.com/gofrs/flock v0.8.1 // indirect
9798
github.com/gogo/googleapis v1.4.1 // indirect
9899
github.com/gogo/protobuf v1.3.2 // indirect
100+
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
99101
github.com/golang/glog v1.2.0 // indirect
100102
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
101103
github.com/golang/mock v1.6.0 // indirect
@@ -114,6 +116,7 @@ require (
114116
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
115117
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
116118
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
119+
github.com/hashicorp/go-bexpr v0.1.10 // indirect
117120
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
118121
github.com/hashicorp/go-getter v1.7.1 // indirect
119122
github.com/hashicorp/go-hclog v1.5.0 // indirect
@@ -126,12 +129,15 @@ require (
126129
github.com/hashicorp/hcl v1.0.0 // indirect
127130
github.com/hashicorp/yamux v0.1.1 // indirect
128131
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
132+
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 // indirect
129133
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
130134
github.com/holiman/uint256 v1.3.1 // indirect
131135
github.com/huandu/skiplist v1.2.0 // indirect
136+
github.com/huin/goupnp v1.3.0 // indirect
132137
github.com/iancoleman/strcase v0.3.0 // indirect
133138
github.com/improbable-eng/grpc-web v0.15.0 // indirect
134139
github.com/inconshreveable/mousetrap v1.1.0 // indirect
140+
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
135141
github.com/jmespath/go-jmespath v0.4.0 // indirect
136142
github.com/jmhodges/levigo v1.0.0 // indirect
137143
github.com/klauspost/compress v1.17.9 // indirect
@@ -149,6 +155,7 @@ require (
149155
github.com/mitchellh/go-homedir v1.1.0 // indirect
150156
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
151157
github.com/mitchellh/mapstructure v1.5.0 // indirect
158+
github.com/mitchellh/pointerstructure v1.2.0 // indirect
152159
github.com/mmcloughlin/addchain v0.4.0 // indirect
153160
github.com/mtibben/percent v0.2.1 // indirect
154161
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -168,6 +175,7 @@ require (
168175
github.com/rogpeppe/go-internal v1.13.1 // indirect
169176
github.com/rs/cors v1.8.3 // indirect
170177
github.com/rs/zerolog v1.32.0 // indirect
178+
github.com/russross/blackfriday/v2 v2.1.0 // indirect
171179
github.com/sagikazarmark/locafero v0.4.0 // indirect
172180
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
173181
github.com/sasha-s/go-deadlock v0.3.1 // indirect
@@ -184,6 +192,8 @@ require (
184192
github.com/tklauser/go-sysconf v0.3.12 // indirect
185193
github.com/tklauser/numcpus v0.6.1 // indirect
186194
github.com/ulikunitz/xz v0.5.11 // indirect
195+
github.com/urfave/cli/v2 v2.25.7 // indirect
196+
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
187197
github.com/zondax/hid v0.9.2 // indirect
188198
github.com/zondax/ledger-go v0.14.3 // indirect
189199
go.etcd.io/bbolt v1.3.8 // indirect
@@ -214,6 +224,7 @@ require (
214224
google.golang.org/grpc v1.62.0 // indirect
215225
google.golang.org/protobuf v1.35.2 // indirect
216226
gopkg.in/ini.v1 v1.67.0 // indirect
227+
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
217228
gopkg.in/yaml.v2 v2.4.0 // indirect
218229
gopkg.in/yaml.v3 v3.0.1 // indirect
219230
gotest.tools/v3 v3.5.1 // indirect

go.sum

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,6 @@ github.com/cosmos/ics23/go v0.10.0 h1:iXqLLgp2Lp+EdpIuwXTYIQU+AiHj9mOC2X9ab++bZD
385385
github.com/cosmos/ics23/go v0.10.0/go.mod h1:ZfJSmng/TBNTBkFemHHHj5YY7VAU/MBU980F4VU1NG0=
386386
github.com/cosmos/ledger-cosmos-go v0.13.3 h1:7ehuBGuyIytsXbd4MP43mLeoN2LTOEnk5nvue4rK+yM=
387387
github.com/cosmos/ledger-cosmos-go v0.13.3/go.mod h1:HENcEP+VtahZFw38HZ3+LS3Iv5XV6svsnkk9vdJtLr8=
388-
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
389388
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
390389
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
391390
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
@@ -859,6 +858,7 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
859858
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
860859
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
861860
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
861+
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
862862
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
863863
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
864864
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
@@ -1007,7 +1007,6 @@ github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
10071007
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
10081008
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
10091009
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
1010-
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
10111010
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
10121011
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
10131012
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -1105,7 +1104,6 @@ github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o
11051104
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
11061105
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
11071106
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
1108-
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
11091107
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
11101108
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
11111109
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=

pkg/client/txpool/txpool.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package txpool
2+
3+
import (
4+
"context"
5+
"math/big"
6+
"slices"
7+
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/ethereum/go-ethereum/common/hexutil"
10+
"github.com/ethereum/go-ethereum/core/types"
11+
"github.com/ethereum/go-ethereum/ethclient"
12+
)
13+
14+
// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction
15+
type RPCTransaction struct {
16+
BlockHash *common.Hash `json:"blockHash"`
17+
BlockNumber *hexutil.Big `json:"blockNumber"`
18+
From common.Address `json:"from"`
19+
Gas hexutil.Uint64 `json:"gas"`
20+
GasPrice *hexutil.Big `json:"gasPrice"`
21+
GasFeeCap *hexutil.Big `json:"maxFeePerGas,omitempty"`
22+
GasTipCap *hexutil.Big `json:"maxPriorityFeePerGas,omitempty"`
23+
MaxFeePerBlobGas *hexutil.Big `json:"maxFeePerBlobGas,omitempty"`
24+
Hash common.Hash `json:"hash"`
25+
Input hexutil.Bytes `json:"input"`
26+
Nonce hexutil.Uint64 `json:"nonce"`
27+
To *common.Address `json:"to"`
28+
TransactionIndex *hexutil.Uint64 `json:"transactionIndex"`
29+
Value *hexutil.Big `json:"value"`
30+
Type hexutil.Uint64 `json:"type"`
31+
Accesses *types.AccessList `json:"accessList,omitempty"`
32+
ChainID *hexutil.Big `json:"chainId,omitempty"`
33+
BlobVersionedHashes []common.Hash `json:"blobVersionedHashes,omitempty"`
34+
V *hexutil.Big `json:"v"`
35+
R *hexutil.Big `json:"r"`
36+
S *hexutil.Big `json:"s"`
37+
YParity *hexutil.Uint64 `json:"yParity,omitempty"`
38+
}
39+
40+
// ContentFrom calls `txpool_contentFrom` of the Ethereum RPC
41+
func ContentFrom(ctx context.Context, client *ethclient.Client, address common.Address) (map[string]map[string]*RPCTransaction, error) {
42+
var res map[string]map[string]*RPCTransaction
43+
if err := client.Client().CallContext(ctx, &res, "txpool_contentFrom", address); err != nil {
44+
return nil, err
45+
}
46+
return res, nil
47+
}
48+
49+
// PendingTransactions returns pending txs sent from `address` sorted by nonce.
50+
func PendingTransactions(ctx context.Context, client *ethclient.Client, address common.Address) ([]*RPCTransaction, error) {
51+
txs, err := ContentFrom(ctx, client, address)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
pendingTxMap, found := txs["pending"]
57+
if !found {
58+
return nil, nil
59+
}
60+
61+
var pendingTxs []*RPCTransaction
62+
for _, pendingTx := range pendingTxMap {
63+
pendingTxs = append(pendingTxs, pendingTx)
64+
}
65+
66+
slices.SortFunc(pendingTxs, func(a, b *RPCTransaction) int {
67+
if a.Nonce < b.Nonce {
68+
return -1
69+
} else if a.Nonce > b.Nonce {
70+
return 1
71+
} else {
72+
return 0
73+
}
74+
})
75+
76+
return pendingTxs, nil
77+
}
78+
79+
func inclByPercent(n *big.Int, percent uint64) {
80+
n.Mul(n, big.NewInt(int64(100+percent)))
81+
n.Div(n, big.NewInt(100))
82+
}
83+
84+
// GetMinimumRequiredFee returns the minimum fee required to successfully send a transaction
85+
func GetMinimumRequiredFee(ctx context.Context, client *ethclient.Client, address common.Address, nonce uint64, priceBump uint64) (*big.Int, *big.Int, error) {
86+
pendingTxs, err := PendingTransactions(ctx, client, address)
87+
if err != nil {
88+
return nil, nil, err
89+
} else if len(pendingTxs) == 0 {
90+
return common.Big0, common.Big0, nil
91+
}
92+
93+
var targetTx *RPCTransaction
94+
for _, pendingTx := range pendingTxs {
95+
if uint64(pendingTx.Nonce) == nonce {
96+
targetTx = pendingTx
97+
break
98+
}
99+
}
100+
if targetTx == nil {
101+
return common.Big0, common.Big0, nil
102+
}
103+
104+
gasFeeCap := targetTx.GasFeeCap.ToInt()
105+
gasTipCap := targetTx.GasTipCap.ToInt()
106+
107+
inclByPercent(gasFeeCap, priceBump)
108+
inclByPercent(gasTipCap, priceBump)
109+
110+
return gasFeeCap, gasTipCap, nil
111+
}

pkg/client/txpool/txpool_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package txpool
2+
3+
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"math/big"
7+
"testing"
8+
"unsafe"
9+
10+
"github.com/ethereum/go-ethereum/common"
11+
"github.com/ethereum/go-ethereum/core/types"
12+
"github.com/ethereum/go-ethereum/crypto"
13+
"github.com/ethereum/go-ethereum/eth/ethconfig"
14+
"github.com/ethereum/go-ethereum/ethclient"
15+
"github.com/ethereum/go-ethereum/ethclient/simulated"
16+
"github.com/ethereum/go-ethereum/node"
17+
)
18+
19+
func makeGenesisAlloc(t *testing.T, n int) ([]common.Address, types.GenesisAlloc) {
20+
var addrs []common.Address
21+
alloc := make(types.GenesisAlloc)
22+
for i := 0; i < n; i++ {
23+
key, err := crypto.GenerateKey()
24+
if err != nil {
25+
t.Fatalf("failed to generate a private key: err=%v", err)
26+
}
27+
28+
addr := crypto.PubkeyToAddress(key.PublicKey)
29+
30+
addrs = append(addrs, addr)
31+
alloc[addr] = types.Account{
32+
Balance: new(big.Int).Lsh(common.Big1, 250),
33+
PrivateKey: crypto.FromECDSA(key),
34+
}
35+
}
36+
return addrs, alloc
37+
}
38+
39+
func getPrivateKey(t *testing.T, account types.Account) *ecdsa.PrivateKey {
40+
key, err := crypto.ToECDSA(account.PrivateKey)
41+
if err != nil {
42+
t.Fatalf("failed to unmarshal the private key: err=%v", err)
43+
}
44+
return key
45+
}
46+
47+
func getEthClient(sim *simulated.Backend) *ethclient.Client {
48+
type simClient struct {
49+
*ethclient.Client
50+
}
51+
ifceCli := sim.Client()
52+
ptrCli := unsafe.Add(unsafe.Pointer(&ifceCli), unsafe.Sizeof(uintptr(0)))
53+
return (*simClient)(ptrCli).Client
54+
}
55+
56+
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) {
57+
tx := types.NewTx(&types.DynamicFeeTx{
58+
Nonce: nonce,
59+
GasTipCap: gasTipCap,
60+
GasFeeCap: gasFeeCap,
61+
Gas: 21000,
62+
To: &to,
63+
Value: amount,
64+
})
65+
66+
tx, err := types.SignTx(tx, signer, key)
67+
if err != nil {
68+
t.Fatalf("failed to sign tx: err=%v", err)
69+
}
70+
71+
if err := client.SendTransaction(ctx, tx); err != nil {
72+
t.Fatalf("failed to send tx: err=%v", err)
73+
}
74+
}
75+
76+
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) {
77+
addr := crypto.PubkeyToAddress(key.PublicKey)
78+
79+
if minFeeCap, minTipCap, err := GetMinimumRequiredFee(ctx, client, addr, nonce, priceBump); err != nil {
80+
t.Fatalf("failed to get the minimum fee required to replace tx: err=%v", err)
81+
} else if minFeeCap.Cmp(common.Big0) == 0 {
82+
t.Fatalf("tx to replace not found")
83+
} else {
84+
t.Logf("minimum required fees: feeCap=%v, tipCap=%v", minFeeCap, minTipCap)
85+
if gasFeeCap.Cmp(minFeeCap) < 0 {
86+
t.Logf("gasFeeCap updated: %v => %v", gasFeeCap, minFeeCap)
87+
gasFeeCap = minFeeCap
88+
}
89+
if gasTipCap.Cmp(minTipCap) < 0 {
90+
t.Logf("gasTipCap updated: %v => %v", gasTipCap, minTipCap)
91+
gasTipCap = minTipCap
92+
}
93+
}
94+
95+
transfer(t, ctx, client, signer, key, nonce, gasTipCap, gasFeeCap, to, amount)
96+
}
97+
98+
func TestContentFrom(t *testing.T) {
99+
ctx := context.Background()
100+
101+
addrs, alloc := makeGenesisAlloc(t, 2)
102+
sender, receiver := addrs[0], addrs[1]
103+
senderKey := getPrivateKey(t, alloc[sender])
104+
105+
priceBump := uint64(25)
106+
sim := simulated.NewBackend(alloc, func(nodeConf *node.Config, ethConf *ethconfig.Config) {
107+
t.Logf("original price bump is %v", ethConf.TxPool.PriceBump)
108+
ethConf.TxPool.PriceBump = priceBump
109+
})
110+
defer sim.Close()
111+
112+
cli := getEthClient(sim)
113+
114+
// make signer
115+
chainID, err := cli.ChainID(ctx)
116+
if err != nil {
117+
t.Fatalf("failed to get chain ID: err=%v", err)
118+
}
119+
signer := types.LatestSignerForChainID(chainID)
120+
121+
// we use small fee cap to prepend txs from being included
122+
feeCap := big.NewInt(1_000_000)
123+
tipCap := big.NewInt(1_000_000)
124+
amount := big.NewInt(1_000_000_000) // 1 GWei
125+
126+
transfer(t, ctx, cli, signer, senderKey, 0, tipCap, feeCap, receiver, amount)
127+
for i := 0; i < 10; i++ {
128+
replace(t, ctx, cli, signer, senderKey, priceBump, 0, tipCap, feeCap, receiver, amount)
129+
}
130+
131+
// check block info
132+
block, err := cli.BlockByNumber(ctx, nil)
133+
if err != nil {
134+
t.Fatalf("failed to get block by number: err=%v", err)
135+
} else {
136+
t.Logf("bn=%v, basefee=%v", block.Number(), block.BaseFee())
137+
}
138+
139+
// commit block
140+
sim.Commit()
141+
142+
// check block info
143+
block, err = cli.BlockByNumber(ctx, nil)
144+
if err != nil {
145+
t.Fatalf("failed to get block by number: err=%v", err)
146+
} else {
147+
t.Logf("bn=%v, basefee=%v", block.Number(), block.BaseFee())
148+
}
149+
150+
// check that len(pendingTxs) == 1
151+
if pendingTxs, err := PendingTransactions(ctx, cli, sender); err != nil {
152+
t.Fatalf("failed to get pending transactions: err=%v", err)
153+
} else if len(pendingTxs) != 1 {
154+
t.Fatalf("unexpected pending txs: pendingTxs=%v", pendingTxs)
155+
}
156+
157+
// update fee cap to allow the tx to be included
158+
feeCap = block.BaseFee()
159+
160+
replace(t, ctx, cli, signer, senderKey, priceBump, 0, tipCap, feeCap, receiver, amount)
161+
162+
// commit block
163+
sim.Commit()
164+
165+
// check block info
166+
block, err = cli.BlockByNumber(ctx, nil)
167+
if err != nil {
168+
t.Fatalf("failed to get block by number: err=%v", err)
169+
} else {
170+
t.Logf("bn=%v, basefee=%v", block.Number(), block.BaseFee())
171+
}
172+
173+
// check that len(pendingTxs) == 0
174+
if pendingTxs, err := PendingTransactions(ctx, cli, sender); err != nil {
175+
t.Fatalf("failed to get pending transactions: err=%v", err)
176+
} else if len(pendingTxs) != 0 {
177+
t.Fatalf("unexpected pending txs: pendingTxs=%v", pendingTxs)
178+
}
179+
}

0 commit comments

Comments
 (0)