-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #59 from siburu/price-bump
Take price bump into consideration
- Loading branch information
Showing
8 changed files
with
414 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.