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

server/eth: Match fee rate = MaxFeeRate #1447

Merged
merged 6 commits into from
Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions client/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,6 @@ var (
dex.Simnet: 42, // see dex/testing/eth/harness.sh
}

minGasTipCap = dexeth.GweiToWei(2)

findRedemptionCoinID = []byte("FindRedemption Coin")
)

Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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))
}
Expand Down
10 changes: 5 additions & 5 deletions client/asset/eth/nodeclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion client/asset/eth/nodeclient_harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions dex/networks/eth/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
1 change: 1 addition & 0 deletions run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions server/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions server/asset/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions server/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
35 changes: 24 additions & 11 deletions server/asset/eth/coiner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Comment on lines +164 to +165
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a legacy transaction, which returns the same gas price for both GasFeeCap and GasTipCap, I think that the swapCoin.gasTipCap check in (*Backend).ValidateFeeRate is wrong or at least misleading.
Say we are debugging and we inspect a swapCoin, we'd see a gasTipCap == gasFeeCap (a huge gasTipCap).
I guess we can solve this with documentation spelling out the logic of why a legacy txn can be treated as a dynamic tx with a massive tip, but I do think we should spell that out in both methods (baseCoin construction and ValidateFeeRate)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some docs.

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.
Expand All @@ -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(),
Expand Down Expand Up @@ -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
}
48 changes: 28 additions & 20 deletions server/asset/eth/coiner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
}}
Expand Down Expand Up @@ -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)
}
}
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
}
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Loading