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

eth/client: Swap #1248

Merged
merged 2 commits into from
Nov 29, 2021
Merged

eth/client: Swap #1248

merged 2 commits into from
Nov 29, 2021

Conversation

martonp
Copy link
Contributor

@martonp martonp commented Oct 23, 2021

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

GasTipCap: big.NewInt(GasTipCap),
Context: eth.ctx}

tx, err := eth.node.initiate(&opts, eth.networkID, initiations)
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

eth.lockFunds(asset.Coins{change})
}

feesUsed := swaps.FeeRate * initGas
Copy link
Member

@chappjc chappjc Nov 14, 2021

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.

Copy link
Member

@JoeGruffins JoeGruffins left a 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.

@@ -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)
Copy link
Member

Choose a reason for hiding this comment

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

netowrk -> network

return nil, errors.New("coin id must be amount coin id")
}

if !bytes.Equal(amountCoinID.Address.Bytes(), eth.acct.Address.Bytes()) {
Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

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)

Comment on lines 656 to 659
// 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.
Copy link
Member

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.

Copy link
Contributor Author

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.

Comment on lines 666 to 667
// 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
Copy link
Member

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.

Copy link
Contributor Author

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.

RefundTimestamp: big.NewInt(0).SetUint64(contract.LockTime),
SecretHash: secretHash,
Participant: address,
Value: big.NewInt(0).SetUint64(contract.Value),
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes this was wrong.

Comment on lines 749 to 750
Value: big.NewInt(0).SetUint64(totalContractValue),
GasFeeCap: big.NewInt(0).SetUint64(swaps.FeeRate),
Copy link
Member

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.

Copy link
Contributor Author

@martonp martonp Nov 25, 2021

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) {
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

@chappjc chappjc Nov 25, 2021

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.

Copy link
Contributor Author

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.

Copy link
Member

@buck54321 buck54321 left a 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.

Comment on lines +729 to +820
suggestedGasTipCap, err := eth.node.suggestGasTipCap(eth.ctx)
if err != nil {
return fail(fmt.Errorf("Swap: failed to get suggested gas tip cap %w", err))
}
Copy link
Member

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.

Copy link
Contributor Author

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.

Comment on lines +1118 to +1287
swaps = asset.Swaps{
Inputs: inputs,
Contracts: contracts,
FeeRate: 200,
LockChange: false,
}
testSwap("one contract, don't lock change", swaps, false)
Copy link
Member

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.

Comment on lines +230 to +239
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,
}
}

Copy link
Member

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.

Copy link
Contributor Author

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.

Comment on lines 638 to 732
// 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[:]),
}
}
Copy link
Member

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.


// Contract returns the swap's secret hash.
func (r *swapReceipt) Contract() dex.Bytes {
return dex.Bytes(r.secretHash[:])
Copy link
Member

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[:].


// 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)
Copy link
Member

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good to me.

}

// 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
Copy link
Member

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{}
Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

Issue: #1311

Comment on lines 919 to 922
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()))
}
Copy link
Member

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.

@chappjc chappjc added this to the 0.5 milestone Nov 28, 2021
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.
@chappjc chappjc merged commit 902495d into decred:master Nov 29, 2021
@chappjc chappjc removed this from the 0.5 milestone Aug 26, 2022
@martonp martonp deleted the ethSwap branch December 20, 2022 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants