diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index 501c07e5b5..7d4198e31f 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -100,8 +100,6 @@ var ( dex.Simnet: 42, // see dex/testing/eth/harness.sh } - minGasTipCap = dexeth.GweiToWei(2) - findRedemptionCoinID = []byte("FindRedemption Coin") ) @@ -697,9 +695,7 @@ func (eth *ExchangeWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin return nil, nil, 0, fmt.Errorf("unfunded contract. %d < %d", totalInputValue, totalSpend) } - // TODO: Fix fee rate. The current fee rate returned from the server - // will not allow this to be mined on simnet. - tx, err := eth.node.initiate(eth.ctx, swaps.Contracts, 200 /*swaps.FeeRate*/, swaps.AssetVersion) + tx, err := eth.node.initiate(eth.ctx, swaps.Contracts, swaps.FeeRate, swaps.AssetVersion) if err != nil { return fail(fmt.Errorf("Swap: initiate error: %w", err)) } @@ -789,9 +785,7 @@ func (eth *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Co // TODO: make sure the amount we locked for redemption is enough to cover the gas // fees. Also unlock coins. - // TODO: Fix fee rate. The current fee rate returned from the server - // will not allow this to be mined on simnet. - tx, err := eth.node.redeem(eth.ctx, form.Redemptions, 200 /*form.FeeSuggestion*/, contractVersion) + tx, err := eth.node.redeem(eth.ctx, form.Redemptions, form.FeeSuggestion, contractVersion) if err != nil { return fail(fmt.Errorf("Redeem: redeem error: %w", err)) } diff --git a/client/asset/eth/nodeclient.go b/client/asset/eth/nodeclient.go index e765b5d149..8c72a14b20 100644 --- a/client/asset/eth/nodeclient.go +++ b/client/asset/eth/nodeclient.go @@ -213,9 +213,8 @@ func (n *nodeClient) balance(ctx context.Context) (*Balance, error) { addFees := func(tx *types.Transaction) { gas := new(big.Int).SetUint64(tx.Gas()) - if gasPrice := tx.GasPrice(); gasPrice != nil && gasPrice.Cmp(zero) > 0 { - outgoing.Add(outgoing, new(big.Int).Mul(gas, gasPrice)) - } else if gasFeeCap := tx.GasFeeCap(); gasFeeCap != nil { + // For legacy transactions, GasFeeCap returns gas price + if gasFeeCap := tx.GasFeeCap(); gasFeeCap != nil { outgoing.Add(outgoing, new(big.Int).Mul(gas, gasFeeCap)) } else { n.log.Errorf("unable to calculate fees for tx %s", tx.Hash()) @@ -488,8 +487,9 @@ func (n *nodeClient) netFeeState(ctx context.Context) (baseFees, tipCap *big.Int return nil, nil, err } - if tip.Cmp(minGasTipCap) < 0 { - tip = new(big.Int).Set(minGasTipCap) + minGasTipCapWei := dexeth.GweiToWei(dexeth.MinGasTipCap) + if tip.Cmp(minGasTipCapWei) < 0 { + tip = new(big.Int).Set(minGasTipCapWei) } return base, tip, nil diff --git a/client/asset/eth/nodeclient_harness_test.go b/client/asset/eth/nodeclient_harness_test.go index d0a8f56eb8..d6387ab095 100644 --- a/client/asset/eth/nodeclient_harness_test.go +++ b/client/asset/eth/nodeclient_harness_test.go @@ -581,7 +581,9 @@ func feesAtBlk(ctx context.Context, n *nodeClient, blkNum int64) (fees *big.Int, if err != nil { return nil, err } - tip := new(big.Int).Set(minGasTipCap) + + minGasTipCapWei := dexeth.GweiToWei(dexeth.MinGasTipCap) + tip := new(big.Int).Set(minGasTipCapWei) return tip.Add(tip, hdr.BaseFee), nil } diff --git a/dex/networks/eth/params.go b/dex/networks/eth/params.go index 4ad5d1a9c5..f542a401d1 100644 --- a/dex/networks/eth/params.go +++ b/dex/networks/eth/params.go @@ -29,6 +29,7 @@ const ( // in over which we consider the chain to be out of sync. MaxBlockInterval = 180 EthBipID = 60 + MinGasTipCap = 2 //gwei ) var ( diff --git a/run_tests.sh b/run_tests.sh index a0970b98de..83a7acc00b 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -57,6 +57,7 @@ go test "${dumptags[@]}" dcrlive ./server/asset/dcr go test "${dumptags[@]}" btclive ./server/asset/btc go test "${dumptags[@]}" ltclive ./server/asset/ltc go test "${dumptags[@]}" bchlive ./server/asset/bch +go test "${dumptags[@]}" harness,lgpl ./server/asset/eth go test "${dumptags[@]}" pgonline ./server/db/driver/pg # Return to initial directory. diff --git a/server/asset/btc/btc.go b/server/asset/btc/btc.go index d04424e068..205b26458c 100644 --- a/server/asset/btc/btc.go +++ b/server/asset/btc/btc.go @@ -493,6 +493,17 @@ func (btc *Backend) FeeRate(_ context.Context) (uint64, error) { return btc.estimateFee(btc.node) } +// Info provides some general information about the backend. +func (*Backend) Info() *asset.BackendInfo { + return &asset.BackendInfo{} +} + +// ValidateFeeRate checks that the transaction fees used to initiate the +// contract are sufficient. +func (*Backend) ValidateFeeRate(contract *asset.Contract, reqFeeRate uint64) bool { + return contract.FeeRate() >= reqFeeRate +} + // CheckAddress checks that the given address is parseable. func (btc *Backend) CheckAddress(addr string) bool { _, err := btc.decodeAddr(addr, btc.chainParams) diff --git a/server/asset/common.go b/server/asset/common.go index 31d7b5ee48..a880d93ac2 100644 --- a/server/asset/common.go +++ b/server/asset/common.go @@ -22,6 +22,11 @@ type KeyIndexer interface { SetKeyIndex(idx uint32, xpub string) error } +// BackendInfo provides auxillary information about a backend. +type BackendInfo struct { + SupportsDynamicTxFee bool +} + // CoinNotFoundError is to be returned from Contract, Redemption, and // FundingCoin when the specified transaction cannot be found. Used by the // server to handle network latency. @@ -75,6 +80,11 @@ type Backend interface { // Synced should return true when the blockchain is synced and ready for // fee rate estimation. Synced() (bool, error) + // Info provides auxillary information about a backend. + Info() *BackendInfo + // ValidateFeeRate checks that the transaction fees used to initiate the + // contract are sufficient. + ValidateFeeRate(contract *Contract, reqFeeRate uint64) bool } // OutputTracker is implemented by backends for UTXO-based blockchains. diff --git a/server/asset/dcr/dcr.go b/server/asset/dcr/dcr.go index a125a06ed5..8a833779a9 100644 --- a/server/asset/dcr/dcr.go +++ b/server/asset/dcr/dcr.go @@ -300,6 +300,17 @@ func (dcr *Backend) FeeRate(ctx context.Context) (uint64, error) { return atomsPerB, nil } +// Info provides some general information about the backend. +func (*Backend) Info() *asset.BackendInfo { + return &asset.BackendInfo{} +} + +// ValidateFeeRate checks that the transaction fees used to initiate the +// contract are sufficient. +func (*Backend) ValidateFeeRate(contract *asset.Contract, reqFeeRate uint64) bool { + return contract.FeeRate() >= reqFeeRate +} + // BlockChannel creates and returns a new channel on which to receive block // updates. If the returned channel is ever blocking, there will be no error // logged from the dcr package. Part of the asset.Backend interface. diff --git a/server/asset/eth/coiner.go b/server/asset/eth/coiner.go index 4f58b2c1e8..1284b0fcca 100644 --- a/server/asset/eth/coiner.go +++ b/server/asset/eth/coiner.go @@ -24,7 +24,8 @@ var _ asset.Coin = (*redeemCoin)(nil) type baseCoin struct { backend *Backend secretHash [32]byte - gasPrice uint64 + gasFeeCap uint64 + gasTipCap uint64 txHash common.Hash value uint64 txData []byte @@ -144,18 +145,29 @@ func (eth *Backend) baseCoin(coinID []byte, contractData []byte) (*baseCoin, err // initialization transaction could take a long time to be mined. A // transaction with a very low gas price may need to be resent with a // higher price. + // + // Although we only retrieve the GasFeeCap and GasTipCap here, legacy + // transaction are also supported. In legacy transactions, the full + // gas price that is specified will be used no matter what, so the + // values returned from GasFeeCap and GasTipCap will both be equal to the + // gas price. zero := new(big.Int) - rate := tx.GasPrice() - if rate == nil || rate.Cmp(zero) <= 0 { - rate = tx.GasFeeCap() - if rate == nil || rate.Cmp(zero) <= 0 { - return nil, fmt.Errorf("Failed to parse gas price from tx %s", txHash) - } + gasFeeCap := tx.GasFeeCap() + if gasFeeCap == nil || gasFeeCap.Cmp(zero) <= 0 { + return nil, fmt.Errorf("Failed to parse gas fee cap from tx %s", txHash) + } + gasFeeCapGwei, err := dexeth.WeiToGweiUint64(gasFeeCap) + if err != nil { + return nil, fmt.Errorf("unable to convert gas fee cap: %v", err) } - gasPrice, err := dexeth.WeiToGweiUint64(rate) + gasTipCap := tx.GasTipCap() + if gasTipCap == nil || gasTipCap.Cmp(zero) <= 0 { + return nil, fmt.Errorf("Failed to parse gas tip cap from tx %s", txHash) + } + gasTipCapGwei, err := dexeth.WeiToGweiUint64(gasTipCap) if err != nil { - return nil, fmt.Errorf("unable to convert gas price: %v", err) + return nil, fmt.Errorf("unable to convert gas tip cap: %v", err) } // Value is stored in the swap with the initialization transaction. @@ -167,7 +179,8 @@ func (eth *Backend) baseCoin(coinID []byte, contractData []byte) (*baseCoin, err return &baseCoin{ backend: eth, secretHash: secretHash, - gasPrice: gasPrice, + gasFeeCap: gasFeeCapGwei, + gasTipCap: gasTipCapGwei, txHash: txHash, value: value, txData: tx.Data(), @@ -283,5 +296,5 @@ func (c *baseCoin) Value() uint64 { // FeeRate returns the gas rate, in gwei/gas. It is set in initialization of // the swapCoin. func (c *baseCoin) FeeRate() uint64 { - return c.gasPrice + return c.gasFeeCap } diff --git a/server/asset/eth/coiner_test.go b/server/asset/eth/coiner_test.go index 8ae20422c2..6f2dcc8731 100644 --- a/server/asset/eth/coiner_test.go +++ b/server/asset/eth/coiner_test.go @@ -32,8 +32,10 @@ func TestNewRedeemCoin(t *testing.T) { copy(secretHash[:], redeemSecretHashB) contract := dexeth.EncodeContractData(0, secretHash) const gasPrice = 30 + const gasTipCap = 2 const value = 5e9 const wantGas = 30 + const wantGasTipCap = 2 tests := []struct { name string contract []byte @@ -42,21 +44,21 @@ func TestNewRedeemCoin(t *testing.T) { wantErr bool }{{ name: "ok redeem", - tx: tTx(gasPrice, 0, contractAddr, redeemCalldata), + tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), contract: contract, }, { name: "non zero value with redeem", - tx: tTx(gasPrice, value, contractAddr, redeemCalldata), + tx: tTx(gasPrice, gasTipCap, value, contractAddr, redeemCalldata), contract: contract, wantErr: true, }, { name: "unable to decode redeem data, must be redeem for redeem coin type", - tx: tTx(gasPrice, 0, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, 0, contractAddr, initCalldata), contract: contract, wantErr: true, }, { name: "tx coin id for redeem - contract not in tx", - tx: tTx(gasPrice, value, contractAddr, redeemCalldata), + tx: tTx(gasPrice, gasTipCap, value, contractAddr, redeemCalldata), contract: encode.RandomBytes(32), wantErr: true, }} @@ -84,8 +86,8 @@ func TestNewRedeemCoin(t *testing.T) { if rc.secretHash != secretHash || rc.secret != secret || rc.value != 0 || - rc.gasPrice != wantGas { - + rc.gasFeeCap != wantGas || + rc.gasTipCap != wantGasTipCap { t.Fatalf("returns do not match expected for test %q / %v", test.name, rc) } } @@ -101,6 +103,7 @@ func TestNewSwapCoin(t *testing.T) { badCoinIDBytes := encode.RandomBytes(39) const gasPrice = 30 const value = 5e9 + const gasTipCap = 2 wantGas, err := dexeth.WeiToGweiUint64(big.NewInt(3e10)) if err != nil { t.Fatal(err) @@ -109,6 +112,10 @@ func TestNewSwapCoin(t *testing.T) { if err != nil { t.Fatal(err) } + wantGasTipCap, err := dexeth.WeiToGweiUint64(big.NewInt(2e9)) + if err != nil { + t.Fatal(err) + } tests := []struct { name string coinID []byte @@ -118,61 +125,61 @@ func TestNewSwapCoin(t *testing.T) { wantErr bool }{{ name: "ok init", - tx: tTx(gasPrice, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), coinID: txCoinIDBytes, contract: dexeth.EncodeContractData(0, secretHash), }, { name: "contract incorrect length", - tx: tTx(gasPrice, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), coinID: txCoinIDBytes, contract: initSecretHashA[:31], wantErr: true, }, { name: "tx has no data", - tx: tTx(gasPrice, value, contractAddr, nil), + tx: tTx(gasPrice, gasTipCap, value, contractAddr, nil), coinID: txCoinIDBytes, contract: initSecretHashA, wantErr: true, }, { name: "unable to decode init data, must be init for init coin type", - tx: tTx(gasPrice, value, contractAddr, redeemCalldata), + tx: tTx(gasPrice, gasTipCap, value, contractAddr, redeemCalldata), coinID: txCoinIDBytes, contract: initSecretHashA, wantErr: true, }, { name: "unable to decode CoinID", - tx: tTx(gasPrice, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), contract: initSecretHashA, wantErr: true, }, { name: "invalid coinID", - tx: tTx(gasPrice, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), coinID: badCoinIDBytes, contract: initSecretHashA, wantErr: true, }, { name: "transaction error", - tx: tTx(gasPrice, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), coinID: txCoinIDBytes, contract: initSecretHashA, txErr: errors.New(""), wantErr: true, }, { name: "transaction not found error", - tx: tTx(gasPrice, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), coinID: txCoinIDBytes, contract: initSecretHashA, txErr: ethereum.NotFound, wantErr: true, }, { name: "wrong contract", - tx: tTx(gasPrice, value, randomAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, value, randomAddr, initCalldata), coinID: txCoinIDBytes, contract: initSecretHashA, wantErr: true, }, { name: "tx coin id for swap - contract not in tx", - tx: tTx(gasPrice, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), coinID: txCoinIDBytes, contract: encode.RandomBytes(32), wantErr: true, @@ -202,9 +209,9 @@ func TestNewSwapCoin(t *testing.T) { if sc.init.Participant != initParticipantAddr || sc.secretHash != secretHash || sc.value != wantVal || - sc.gasPrice != wantGas || + sc.gasFeeCap != wantGas || + sc.gasTipCap != wantGasTipCap || sc.init.LockTime.Unix() != initLocktime { - t.Fatalf("returns do not match expected for test %q / %v", test.name, sc) } } @@ -223,6 +230,7 @@ func TestConfirmations(t *testing.T) { copy(secret[:], redeemSecretB) copy(secretHash[:], redeemSecretHashB) const gasPrice = 30 + const gasTipCap = 2 const swapVal = 25e8 const txVal = swapVal * 2 const oneGweiMore = swapVal + 1 @@ -314,10 +322,10 @@ func TestConfirmations(t *testing.T) { var confirmer Confirmer var err error if test.redeem { - node.tx = tTx(gasPrice, test.value, contractAddr, redeemCalldata) + node.tx = tTx(gasPrice, gasTipCap, test.value, contractAddr, redeemCalldata) confirmer, err = eth.newRedeemCoin(txHash[:], swapData) } else { - node.tx = tTx(gasPrice, test.value, contractAddr, initCalldata) + node.tx = tTx(gasPrice, gasTipCap, test.value, contractAddr, initCalldata) confirmer, err = eth.newSwapCoin(txHash[:], swapData) } if err != nil { diff --git a/server/asset/eth/eth.go b/server/asset/eth/eth.go index a1b97b2996..208d038eb0 100644 --- a/server/asset/eth/eth.go +++ b/server/asset/eth/eth.go @@ -40,6 +40,10 @@ const ( ethContractVersion = 0 ) +var backendInfo = &asset.BackendInfo{ + SupportsDynamicTxFee: true, +} + var _ asset.Driver = (*Driver)(nil) // Driver implements asset.Driver. @@ -83,7 +87,7 @@ type ethFetcher interface { headerByHeight(ctx context.Context, height uint64) (*types.Header, error) connect(ctx context.Context, ipc string, contractAddr *common.Address) error shutdown() - suggestGasPrice(ctx context.Context) (*big.Int, error) + suggestGasTipCap(ctx context.Context) (*big.Int, error) syncProgress(ctx context.Context) (*ethereum.SyncProgress, error) swap(ctx context.Context, secretHash [32]byte) (*dexeth.SwapState, error) transaction(ctx context.Context, hash common.Hash) (tx *types.Transaction, isMempool bool, err error) @@ -231,11 +235,55 @@ func (eth *Backend) InitTxSizeBase() uint32 { // FeeRate returns the current optimal fee rate in gwei / gas. func (eth *Backend) FeeRate(ctx context.Context) (uint64, error) { - bigGP, err := eth.node.suggestGasPrice(ctx) + hdr, err := eth.node.bestHeader(ctx) + if err != nil { + return 0, fmt.Errorf("error getting best header: %w", err) + } + + if hdr.BaseFee == nil { + return 0, errors.New("eth block header does not contain base fee") + } + + suggestedGasTipCap, err := eth.node.suggestGasTipCap(ctx) + if err != nil { + return 0, fmt.Errorf("error getting suggested gas tip cap: %w", err) + } + + feeRate := new(big.Int).Add( + suggestedGasTipCap, + new(big.Int).Mul(hdr.BaseFee, big.NewInt(2))) + + feeRateGwei, err := dexeth.WeiToGweiUint64(feeRate) if err != nil { - return 0, err + return 0, fmt.Errorf("failed to convert wei to gwei: %w", err) } - return dexeth.WeiToGweiUint64(bigGP) + + return feeRateGwei, nil +} + +// Info provides some general information about the backend. +func (*Backend) Info() *asset.BackendInfo { + return backendInfo +} + +// ValidateFeeRate checks that the transaction fees used to initiate the +// contract are sufficient. For most assets only the contract.FeeRate() cannot +// be less than reqFeeRate, but for Eth, the gasTipCap must also be checked. +func (eth *Backend) ValidateFeeRate(contract *asset.Contract, reqFeeRate uint64) bool { + coin := contract.Coin + sc, ok := coin.(*swapCoin) + if !ok { + eth.log.Error("eth contract coin type must be a swapCoin but got %T", sc) + return false + } + + // Legacy transactions are also supported. In a legacy transaction, the + // gas tip cap will be equal to the gas price. + if sc.gasTipCap < dexeth.MinGasTipCap { + return false + } + + return contract.FeeRate() >= reqFeeRate } // BlockChannel creates and returns a new channel on which to receive block diff --git a/server/asset/eth/eth_test.go b/server/asset/eth/eth_test.go index 5aa65c8b4d..21b5140ea7 100644 --- a/server/asset/eth/eth_test.go +++ b/server/asset/eth/eth_test.go @@ -89,24 +89,24 @@ func mustParseHex(s string) []byte { } type testNode struct { - connectErr error - bestHdr *types.Header - bestHdrErr error - hdrByHeight *types.Header - hdrByHeightErr error - blkNum uint64 - blkNumErr error - syncProg *ethereum.SyncProgress - syncProgErr error - sugGasPrice *big.Int - sugGasPriceErr error - swp *dexeth.SwapState - swpErr error - tx *types.Transaction - txIsMempool bool - txErr error - acctBal *big.Int - acctBalErr error + connectErr error + bestHdr *types.Header + bestHdrErr error + hdrByHeight *types.Header + hdrByHeightErr error + blkNum uint64 + blkNumErr error + syncProg *ethereum.SyncProgress + syncProgErr error + suggGasTipCap *big.Int + suggGasTipCapErr error + swp *dexeth.SwapState + swpErr error + tx *types.Transaction + txIsMempool bool + txErr error + acctBal *big.Int + acctBalErr error } func (n *testNode) connect(ctx context.Context, ipc string, contractAddr *common.Address) error { @@ -131,8 +131,8 @@ func (n *testNode) syncProgress(ctx context.Context) (*ethereum.SyncProgress, er return n.syncProg, n.syncProgErr } -func (n *testNode) suggestGasPrice(ctx context.Context) (*big.Int, error) { - return n.sugGasPrice, n.sugGasPriceErr +func (n *testNode) suggestGasTipCap(ctx context.Context) (*big.Int, error) { + return n.suggGasTipCap, n.suggGasTipCapErr } func (n *testNode) swap(ctx context.Context, secretHash [32]byte) (*dexeth.SwapState, error) { @@ -278,43 +278,60 @@ func TestFeeRate(t *testing.T) { overMaxWei := new(big.Int).Set(maxWei) overMaxWei.Add(overMaxWei, gweiFactorBig) tests := []struct { - name string - gas *big.Int - gasErr error - wantFee uint64 - wantErr bool + name string + hdrBaseFee *big.Int + hdrErr error + suggGasTipCap *big.Int + suggGasTipCapErr error + wantFee uint64 + wantErr bool }{{ - name: "ok zero", - gas: new(big.Int), - wantFee: 0, - }, { - name: "ok rounded down", - gas: big.NewInt(dexeth.GweiFactor - 1), - wantFee: 0, - }, { - name: "ok one", - gas: big.NewInt(dexeth.GweiFactor), - wantFee: 1, - }, { - name: "ok max int", - gas: maxWei, - wantFee: maxInt, - }, { - name: "over max int", - gas: overMaxWei, - wantErr: true, - }, { - name: "node suggest gas fee error", - gas: new(big.Int), - gasErr: errors.New(""), - wantErr: true, + name: "ok zero", + hdrBaseFee: new(big.Int), + suggGasTipCap: new(big.Int), + wantFee: 0, + }, { + name: "ok rounded down", + hdrBaseFee: big.NewInt(dexeth.GweiFactor - 1), + suggGasTipCap: new(big.Int), + wantFee: 1, + }, { + name: "ok 100, 2", + hdrBaseFee: big.NewInt(dexeth.GweiFactor * 100), + suggGasTipCap: big.NewInt(dexeth.GweiFactor * 2), + wantFee: 202, + }, { + name: "over max int", + hdrBaseFee: overMaxWei, + suggGasTipCap: big.NewInt(dexeth.GweiFactor * 2), + wantErr: true, + }, { + name: "node header err", + hdrBaseFee: new(big.Int), + hdrErr: errors.New(""), + suggGasTipCap: new(big.Int), + wantErr: true, + }, { + name: "nil base fee error", + hdrBaseFee: nil, + suggGasTipCap: new(big.Int), + wantErr: true, + }, { + name: "node suggest gas tip cap err", + hdrBaseFee: new(big.Int), + suggGasTipCapErr: errors.New(""), + wantErr: true, }} for _, test := range tests { ctx, cancel := context.WithCancel(context.Background()) node := &testNode{ - sugGasPrice: test.gas, - sugGasPriceErr: test.gasErr, + bestHdr: &types.Header{ + BaseFee: test.hdrBaseFee, + }, + bestHdrErr: test.hdrErr, + suggGasTipCap: test.suggGasTipCap, + suggGasTipCapErr: test.suggGasTipCapErr, } eth := &Backend{ node: node, @@ -423,12 +440,13 @@ func TestRequiredOrderFunds(t *testing.T) { } } -func tTx(gasPrice, value uint64, to *common.Address, data []byte) *types.Transaction { - return types.NewTx(&types.LegacyTx{ - GasPrice: dexeth.GweiToWei(gasPrice), - To: to, - Value: dexeth.GweiToWei(value), - Data: data, +func tTx(gasFeeCap, gasTipCap, value uint64, to *common.Address, data []byte) *types.Transaction { + return types.NewTx(&types.DynamicFeeTx{ + GasFeeCap: dexeth.GweiToWei(gasFeeCap), + GasTipCap: dexeth.GweiToWei(gasTipCap), + To: to, + Value: dexeth.GweiToWei(value), + Data: data, }) } @@ -439,6 +457,7 @@ func TestContract(t *testing.T) { var txHash [32]byte copy(txHash[:], encode.RandomBytes(32)) const gasPrice = 30 + const gasTipCap = 2 const swapVal = 25e8 const txVal = 5e9 var secret, secretHash [32]byte @@ -454,20 +473,20 @@ func TestContract(t *testing.T) { wantErr bool }{{ name: "ok", - tx: tTx(gasPrice, txVal, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), contract: dexeth.EncodeContractData(0, secretHash), swap: tSwap(97, initLocktime, swapVal, secret, dexeth.SSInitiated, &initParticipantAddr), coinID: txHash[:], }, { name: "new coiner error, wrong tx type", - tx: tTx(gasPrice, txVal, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), contract: dexeth.EncodeContractData(0, secretHash), swap: tSwap(97, initLocktime, swapVal, secret, dexeth.SSInitiated, &initParticipantAddr), coinID: txHash[1:], wantErr: true, }, { name: "confirmations error, swap error", - tx: tTx(gasPrice, txVal, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), contract: dexeth.EncodeContractData(0, secretHash), coinID: txHash[:], swapErr: errors.New(""), @@ -503,6 +522,36 @@ func TestContract(t *testing.T) { } } +func TestValidateFeeRate(t *testing.T) { + swapCoin := swapCoin{ + baseCoin: &baseCoin{ + gasFeeCap: 100, + gasTipCap: 2, + }, + } + + contract := &asset.Contract{ + Coin: &swapCoin, + } + + eth := &Backend{ + log: tLogger, + } + + if !eth.ValidateFeeRate(contract, 100) { + t.Fatalf("expected valid fee rate, but was not valid") + } + + if eth.ValidateFeeRate(contract, 101) { + t.Fatalf("expected invalid fee rate, but was valid") + } + + swapCoin.gasTipCap = dexeth.MinGasTipCap - 1 + if eth.ValidateFeeRate(contract, 100) { + t.Fatalf("expected invalid fee rate, but was valid") + } +} + func TestValidateSecret(t *testing.T) { secret, blankHash := [32]byte{}, [32]byte{} copy(secret[:], encode.RandomBytes(32)) @@ -541,6 +590,7 @@ func TestRedemption(t *testing.T) { copy(secretHash[:], redeemSecretHashB) copy(txHash[:], encode.RandomBytes(32)) const gasPrice = 30 + const gasTipCap = 2 tests := []struct { name string coinID, contractID []byte @@ -551,26 +601,26 @@ func TestRedemption(t *testing.T) { wantErr bool }{{ name: "ok", - tx: tTx(gasPrice, 0, contractAddr, redeemCalldata), + tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), contractID: dexeth.EncodeContractData(0, secretHash), coinID: txHash[:], swp: tSwap(0, 0, 0, secret, dexeth.SSRedeemed, receiverAddr), }, { name: "new coiner error, wrong tx type", - tx: tTx(gasPrice, 0, contractAddr, redeemCalldata), + tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), contractID: dexeth.EncodeContractData(0, secretHash), coinID: txHash[1:], wantErr: true, }, { name: "confirmations error, swap wrong state", - tx: tTx(gasPrice, 0, contractAddr, redeemCalldata), + tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), contractID: dexeth.EncodeContractData(0, secretHash), swp: tSwap(0, 0, 0, secret, dexeth.SSRefunded, receiverAddr), coinID: txHash[:], wantErr: true, }, { name: "validate redeem error", - tx: tTx(gasPrice, 0, contractAddr, redeemCalldata), + tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), contractID: secretHash[:31], coinID: txHash[:], swp: tSwap(0, 0, 0, secret, dexeth.SSRedeemed, receiverAddr), @@ -608,10 +658,11 @@ func TestTxData(t *testing.T) { node: node, } const gasPrice = 30 + const gasTipCap = 2 const value = 5e9 addr := randomAddress() data := encode.RandomBytes(5) - tx := tTx(gasPrice, value, addr, data) + tx := tTx(gasPrice, gasTipCap, value, addr, data) goodCoinID, _ := hex.DecodeString("09c3bed75b35c6cf0549b0636c9511161b18765c019ef371e2a9f01e4b4a1487") node.tx = tx diff --git a/server/asset/eth/rpcclient.go b/server/asset/eth/rpcclient.go index 52c1315a4d..e2da997c5d 100644 --- a/server/asset/eth/rpcclient.go +++ b/server/asset/eth/rpcclient.go @@ -75,10 +75,10 @@ func (c *rpcclient) headerByHeight(ctx context.Context, height uint64) (*types.H return c.ec.HeaderByNumber(ctx, big.NewInt(int64(height))) } -// suggestGasPrice retrieves the currently suggested gas price to allow a timely -// execution of a transaction. -func (c *rpcclient) suggestGasPrice(ctx context.Context) (sgp *big.Int, err error) { - return c.ec.SuggestGasPrice(ctx) +// suggestGasTipCap retrieves the currently suggested priority fee to allow a +// timely execution of a transaction. +func (c *rpcclient) suggestGasTipCap(ctx context.Context) (*big.Int, error) { + return c.ec.SuggestGasTipCap(ctx) } // syncProgress return the current sync progress. Returns no error and nil when not syncing. diff --git a/server/asset/eth/rpcclient_harness_test.go b/server/asset/eth/rpcclient_harness_test.go index fce53fd110..e8cbf1afad 100644 --- a/server/asset/eth/rpcclient_harness_test.go +++ b/server/asset/eth/rpcclient_harness_test.go @@ -98,8 +98,8 @@ func TestSyncProgress(t *testing.T) { } } -func TestSuggestGasPrice(t *testing.T) { - _, err := ethClient.suggestGasPrice(ctx) +func TestSuggestGasTipCap(t *testing.T) { + _, err := ethClient.suggestGasTipCap(ctx) if err != nil { t.Fatal(err) } diff --git a/server/dex/feemgr.go b/server/dex/feemgr.go index fc73384923..97e106ede6 100644 --- a/server/dex/feemgr.go +++ b/server/dex/feemgr.go @@ -104,3 +104,13 @@ func (f *feeFetcher) LastRate() uint64 { func (f *feeFetcher) MaxFeeRate() uint64 { return f.Asset.MaxFeeRate } + +// SwapFeeRate returns the tx fee that needs to be used to initiate a swap. +// This fee will be the max fee rate if the asset supports dynamic tx fees, +// and otherwise it will be the current market fee rate. +func (f *feeFetcher) SwapFeeRate(ctx context.Context) uint64 { + if f.Backend.Info().SupportsDynamicTxFee { + return f.MaxFeeRate() + } + return f.FeeRate(ctx) +} diff --git a/server/market/market.go b/server/market/market.go index 91cb282a33..0f513e3b25 100644 --- a/server/market/market.go +++ b/server/market/market.go @@ -70,6 +70,7 @@ type DataCollector interface { // with FeeFetcher fairly frequently. type FeeFetcher interface { FeeRate(context.Context) uint64 + SwapFeeRate(context.Context) uint64 LastRate() uint64 MaxFeeRate() uint64 } @@ -2258,7 +2259,7 @@ func (m *Market) getFeeRate(assetID uint32, f FeeFetcher) uint64 { // Do not block indefinitely waiting for fetcher. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - rate := f.FeeRate(ctx) + rate := f.SwapFeeRate(ctx) if ctx.Err() != nil { // timeout, try last known rate rate = f.LastRate() log.Warnf("Failed to get latest fee rate for %v. Using last known rate %d.", diff --git a/server/market/market_test.go b/server/market/market_test.go index ec32dec98e..6d281ceb07 100644 --- a/server/market/market_test.go +++ b/server/market/market_test.go @@ -241,6 +241,10 @@ func (f *tFeeFetcher) LastRate() uint64 { return 10 } +func (f *tFeeFetcher) SwapFeeRate(context.Context) uint64 { + return 10 +} + type tBalancer struct { reqs map[string]int } diff --git a/server/market/routers_test.go b/server/market/routers_test.go index 9f27ab110a..16f457476b 100644 --- a/server/market/routers_test.go +++ b/server/market/routers_test.go @@ -428,6 +428,12 @@ func (b *TBackend) Synced() (bool, error) { } func (b *TBackend) TxData([]byte) ([]byte, error) { return nil, nil } +func (*TBackend) Info() *asset.BackendInfo { + return &asset.BackendInfo{} +} +func (*TBackend) ValidateFeeRate(*asset.Contract, uint64) bool { + return true +} type tUTXOBackend struct { *TBackend diff --git a/server/swap/swap.go b/server/swap/swap.go index 6f8488c589..b71ac9e1c1 100644 --- a/server/swap/swap.go +++ b/server/swap/swap.go @@ -1386,8 +1386,8 @@ func (s *Swapper) processInit(msg *msgjson.Message, params *msgjson.Init, stepIn if stepInfo.isBaseAsset { reqFeeRate = stepInfo.match.FeeRateBase } - if contract.FeeRate() < reqFeeRate { - // TODO: test this case + + if !chain.ValidateFeeRate(contract, reqFeeRate) { s.respondError(msg.ID, actor.user, msgjson.ContractError, "low tx fee") return wait.DontTryAgain } diff --git a/server/swap/swap_test.go b/server/swap/swap_test.go index a944cd36e2..05812ce2b1 100644 --- a/server/swap/swap_test.go +++ b/server/swap/swap_test.go @@ -344,14 +344,15 @@ type redeemKey struct { // This stub satisfies asset.Backend. type TBackend struct { - mtx sync.RWMutex - contracts map[string]*asset.Contract - contractErr error - fundsErr error - redemptions map[redeemKey]asset.Coin - redemptionErr error - bChan chan *asset.BlockUpdate // to trigger processBlock and eventually (after up to BroadcastTimeout) checkInaction depending on block time - lbl string + mtx sync.RWMutex + contracts map[string]*asset.Contract + contractErr error + fundsErr error + redemptions map[redeemKey]asset.Coin + redemptionErr error + bChan chan *asset.BlockUpdate // to trigger processBlock and eventually (after up to BroadcastTimeout) checkInaction depending on block time + lbl string + invalidFeeRate bool } func newTBackend(lbl string) TBackend { @@ -442,6 +443,12 @@ func (a *TBackend) setRedemption(redeem asset.Coin, cpSwap asset.Coin, resetErr } a.mtx.Unlock() } +func (*TBackend) Info() *asset.BackendInfo { + return &asset.BackendInfo{} +} +func (a *TBackend) ValidateFeeRate(*asset.Contract, uint64) bool { + return !a.invalidFeeRate +} type TUTXOBackend struct { TBackend @@ -1477,6 +1484,34 @@ func TestSwaps(t *testing.T) { } } +func TestInvalidFeeRate(t *testing.T) { + set := tPerfectLimitLimit(uint64(1e8), uint64(1e8), true) + matchInfo := set.matchInfos[0] + rig, cleanup := tNewTestRig(matchInfo) + defer cleanup() + + rig.auth.newReq = make(chan struct{}, 2) // Negotiate sends 2 requests, one for each party assuming different users matched + + rig.swapper.Negotiate([]*order.MatchSet{set.matchSet}) + + rig.abcNode.invalidFeeRate = true + + // The error will be generated by the chainWaiter thread, so will need to + // check the response. + if err := rig.sendSwap_maker(false); err != nil { + t.Fatal(err) + } + timeOutMempool() + // Should have an rpc error. + msg, resp := rig.auth.getResp(matchInfo.maker.acct) + if msg == nil { + t.Fatalf("no response for missing tx after timeout") + } + if resp.Error == nil { + t.Fatalf("no rpc error for erroneous maker swap %v", resp.Error) + } +} + func TestTxWaiters(t *testing.T) { set := tPerfectLimitLimit(uint64(1e8), uint64(1e8), true) matchInfo := set.matchInfos[0]