-
Notifications
You must be signed in to change notification settings - Fork 92
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
eth/client: Swap #1248
eth/client: Swap #1248
Conversation
GasTipCap: big.NewInt(GasTipCap), | ||
Context: eth.ctx} | ||
|
||
tx, err := eth.node.initiate(&opts, eth.networkID, initiations) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have a bigger problem with EIP-1559. If we lock value (the "change" amount) based on getInitGas
and swaps.FeeRate
(the GasFeeCap
), then tx
gets mined using some unknown lower gas cost, the actual amount available is something else.
I suppose this just means we'll have more locked than is necessary until the order is fully executed and the change is returned?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's right. We could just use a legacy transaction and all the funds could be used up and this issue wouldn't exist, but I think we might as well let the user save a little bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, we'll use an eip 1559 txn, but let's doc the unknown actual cost issue somewhere in this function.
client/asset/eth/eth.go
Outdated
eth.lockFunds(asset.Coins{change}) | ||
} | ||
|
||
feesUsed := swaps.FeeRate * initGas |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have initGas * swaps.FeeRate
above when calculating totalUsedValue
, so how about we assign it to a variable like maxPossibleFee
to convey it's meaning better in both places.
4019cab
to
d4600fe
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Could you make the commit messages longer? Maybe comment on the need to remove getInitGas and some other stuff this does.
client/asset/eth/eth.go
Outdated
@@ -273,13 +272,18 @@ func NewWallet(assetCFG *asset.WalletConfig, logger dex.Logger, network dex.Netw | |||
len(accounts)) | |||
} | |||
|
|||
networkID, err := getNetworkID(network) | |||
if err != nil { | |||
return nil, fmt.Errorf("NewWallet: unable to get netowrk ID: %w", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
netowrk -> network
client/asset/eth/eth.go
Outdated
return nil, errors.New("coin id must be amount coin id") | ||
} | ||
|
||
if !bytes.Equal(amountCoinID.Address.Bytes(), eth.acct.Address.Bytes()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you can compare the common.Address
type with ==
@@ -506,14 +453,18 @@ func (*ExchangeWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, er | |||
|
|||
// coin implements the asset.Coin interface for ETH | |||
type coin struct { | |||
id srveth.AmountCoinID | |||
id dex.Bytes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just my opinion but I think srveth.CoinID would be more appropriate, because it's easy to type check if this is meant to hold different types now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll leave this part for now, the previous agreement was to just send the transaction hash, but I'm thinking it would be better to send the entire TxCoinID
including the index. I commented over here:
#1154 (comment)
client/asset/eth/eth.go
Outdated
// SignedRefund returns an empty byte array. ETH does not support a pre-signed | ||
// redeem script becuase the nonce needed in the transaction can not be previously | ||
// determined. We will need to come up with a different way to support manual | ||
// refunds in ETH. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add the TODO:
tag to this somewhere? That makes it easier to find all the todo's later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just made in issue instead.
client/asset/eth/eth.go
Outdated
// Swap sends the swaps in a single transaction. The Receipts returned can | ||
// be used to refund a failed transaction. The fees used returned are the max |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the Receipts comment is wrong for eth. They cannot be used to refund, because the the signature will need to include the next nonce for the account to be accepted by the network. Right? And those must be in ascending order incrementing by one for each mined transaction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's right, the comment is wrong.
client/asset/eth/eth.go
Outdated
RefundTimestamp: big.NewInt(0).SetUint64(contract.LockTime), | ||
SecretHash: secretHash, | ||
Participant: address, | ||
Value: big.NewInt(0).SetUint64(contract.Value), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this need to be converted to wei from gwei here correct? I believe all uint64 values the server deals with are gwei, and the actual contract wants the basic unit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this was wrong.
client/asset/eth/eth.go
Outdated
Value: big.NewInt(0).SetUint64(totalContractValue), | ||
GasFeeCap: big.NewInt(0).SetUint64(swaps.FeeRate), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't these also need to be converted from gwei to wei? I could be wrong, just let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes they do.
@@ -108,16 +119,34 @@ func (n *testNode) syncProgress(ctx context.Context) (*ethereum.SyncProgress, er | |||
func (n *testNode) pendingTransactions(ctx context.Context) ([]*types.Transaction, error) { | |||
return nil, nil | |||
} | |||
func (n *testNode) initiate(opts *bind.TransactOpts, netID int64, initiations []swap.ETHSwapInitiation) (*types.Transaction, error) { | |||
return nil, nil | |||
func (n *testNode) initiate(opts *bind.TransactOpts, netID int64, initiations []dexeth.ETHSwapInitiation) (*types.Transaction, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe worth noting that this is not concurrent safe because it sets a few values, although the real initiate probably is. It's probably fine that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah in the real one only the locked funds are updated, and those are protected by a mutex. The tests aren't expected to be concurrent safe are they?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That depends if they are used concurrently. :p
To ensure the product code does not have data races, which have security implications in addition to the potential to cause incorrect behavior and even to panic the app (e.g. concurrent map writes), we run tests with the race detector. If there is no concurrent use of the method or the mutated fields, it doesn't need a sync mechanism, but otherwise it would.
As an aside, having the race detector on with tests helps us be acutely aware of the access patterns of the product code using these stubs. In many cases we're better off rethinking design than slapping a mutex on everything. In particular, I'd think these methods don't need synchronization mechanisms (under what circumstances would they be called concurrently or the mutated fields accessed concurrently?). A common anti-pattern we face is each test case not cleaning up after itself, and subsequent tests causing data races. Meddling in internals too much is another one. It's easy to ignore these issue without the race detector. I know you weren't suggesting to turn it off though.
I'm always in favor of designing code, both product and test, in such a way that it is lock-free. That's obv not always possible, just saying.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. As long as we're not reusing a testNode
between tests, we'll be good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Obviously a lot going to conflict with #1301, but this was created first, so I can handle the rebase if this is ready before.
suggestedGasTipCap, err := eth.node.suggestGasTipCap(eth.ctx) | ||
if err != nil { | ||
return fail(fmt.Errorf("Swap: failed to get suggested gas tip cap %w", err)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still haven't figured out how exactly this tip is evaluated, but my assumption is that we'll need to do some math with the dex.Asset.MaxFeeRate
in order to guarantee that the transaction comes in under target.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GasFeeCap
will always be the maximum fee used. The actual gas used will be BaseFee + Tip
. The tip will only be less than the the GasTipCap
if for example BaseFee=99
, GasFeeCap=100
, and GasTipCap=2
. In this case, only 1 gwei will be spent for the tip.
My plan here was to have 2 be the default GasTipCap
and the server can validate that clients put at least 2. Generally all wallets just use 1 for the GasTipCap
though, 2 is for more safety and expediency. If the node recommends that the GasTipCap
should be higher, then a higher one is used, but the dex.Asset.MaxFeeRate
will still never be exceeded.
swaps = asset.Swaps{ | ||
Inputs: inputs, | ||
Contracts: contracts, | ||
FeeRate: 200, | ||
LockChange: false, | ||
} | ||
testSwap("one contract, don't lock change", swaps, false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably OK, but we really shouldn't need a change coin if LockChange
is false
. core shouldn't require it. It does look like we are returning the change coin for other assets. I don't think that's on purpose.
func CreateAmountCoinID(address common.Address, amount uint64) *AmountCoinID { | ||
var nonce [8]byte | ||
copy(nonce[:], encode.RandomBytes(8)) | ||
return &AmountCoinID{ | ||
Address: address, | ||
Amount: amount, | ||
Nonce: nonce, | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per convo elsewhere, we will probably only use this client-side. Can move and unexport here or separately. Up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll do it separately.
client/asset/eth/eth.go
Outdated
// Coin returns the coin used to fund the swap. | ||
func (r *swapReceipt) Coin() asset.Coin { | ||
return &coin{ | ||
value: r.value, | ||
id: dex.Bytes(r.txHash[:]), | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably just the txHash
here.
client/asset/eth/eth.go
Outdated
|
||
// Contract returns the swap's secret hash. | ||
func (r *swapReceipt) Contract() dex.Bytes { | ||
return dex.Bytes(r.secretHash[:]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[]byte
is about the only thing in Go that you can cast implicitly. Just return r.secretHash[:]
.
client/asset/eth/eth.go
Outdated
|
||
// String returns a string representation of the swapReceipt. | ||
func (r *swapReceipt) String() string { | ||
return fmt.Sprintf("tx hash: %x, secret hash: %x", r.txHash, r.secretHash) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you feel about wrapping the output in a delimiter?
return fmt.Sprintf("{ tx hash: %x, secret hash: %x }", r.txHash, r.secretHash)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me.
client/asset/eth/eth.go
Outdated
} | ||
|
||
// SignedRefund returns an empty byte array. ETH does not support a pre-signed | ||
// redeem script becuase the nonce needed in the transaction cannot be previously |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because
// redeem script becuase the nonce needed in the transaction cannot be previously | ||
// determined. | ||
func (*swapReceipt) SignedRefund() dex.Bytes { | ||
return dex.Bytes{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll have to do something, but we can punt for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issue: #1311
client/asset/eth/eth_test.go
Outdated
if len(receipt.Contract()) != srveth.SecretHashSize { | ||
t.Fatalf("%v: expected length of contract to be %v but got %v", | ||
testName, srveth.SecretHashSize, len(receipt.Contract())) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Length check is implicit in bytes.Equal
.
This diff implements the Swap function in the eth client wallet. In addition, the eth client wallet no longer does gas estimations using the eth client and uses hardcoded values instead. Gas estimations do not vary unless the eth client is updated, and these changed will be caught by the harness tests, so it is safe to just use the hardcoded values.
This diff implements the Swap function in the eth client wallet. In addition, the eth client wallet no longer does gas estimations using the eth client and uses hardcoded values instead. Gas estimations do not vary unless the eth client is updated, and these changed will be caught by the harness tests, so it is safe to just use the hardcoded values.
Part of #1154