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

client/eth: refactor eth client #1301

Merged
merged 1 commit into from
Nov 29, 2021

Conversation

buck54321
Copy link
Member

@buck54321 buck54321 commented Nov 16, 2021

The client/asset/eth package was using go-ethereum like an RPC client. That's
not crazy, considering we have still an *rpc.Client, but in reality,
there is no RPC server, and the client is just a shortcut to RPC handlers
that call functions that we already have access to through e.g.
*les.LightEthereum or *keystore.KeyStore. This PR removes all
CallContext calls in favor of calling the underlying methods directly.
There are probably more *rpc.Client calls we could bypass as well.

Moves some common types to dex/networks/eth. As a general rule, if
the client/asset/ imports from server/asset, we should just move it
to dex/networks.

Creates the Contractor interface, which serves as a clear boundary
between dex-space and abigen-space. Implements contract versions.

Switches to EIP1559 transactions and removes hard-coded base fee rate
from harness. Due to this change, some live tests are still busted because
balance changes need to be fixes. Leaving in draft until that's done.

I realize this a big change in an area where we have a lot of hands right now,
but I'd rather do it earlier than later.

Comment on lines 204 to 306
nonce, err := n.ec.NonceAt(ctx, n.creds.addr, nil)
if err != nil {
return nil, fmt.Errorf("error getting nonce: %v", err)
}
Copy link
Member Author

Choose a reason for hiding this comment

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

It's not impossible that this is insufficient to guarantee a unique nonce. I haven't verified that this is incremented before the transaction is mined.

@buck54321
Copy link
Member Author

This PR removes all CallContext calls in favor of calling the underlying methods directly. There are probably more *rpc.Client calls we could bypass as well.

I went ahead and removed the rest of the ethclient.Client => rcp.Client calls. No more pointless encode->decodes. We could get rid of the ethclient.Client altogether if it wasn't for a single function EstimateGas in an internal package that I really don't want to copy.

return b, weiToGwei(tip), nil
}

// getCodeAt retrieves the runtime bytecode at a certain address.
func (n *nodeClient) getCodeAt(ctx context.Context, contractAddr *common.Address) ([]byte, error) {
bn, err := n.ec.BlockNumber(ctx)
func (n *nodeClient) getCodeAt(ctx context.Context, contractAddr common.Address) ([]byte, error) {
Copy link
Member Author

@buck54321 buck54321 Nov 16, 2021

Choose a reason for hiding this comment

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

This one's busted too and I don't know why yet. It's busted if I use the ethclient.Client method too, but that's not ruling out some other mistake I made.

Copy link
Contributor

Choose a reason for hiding this comment

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

Works fine for me.. maybe try restarting the eth harness?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm. I'm still failing. What version of geth are you running?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nevermind. I found the problem.

Copy link
Contributor

Choose a reason for hiding this comment

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

What was wrong?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I've forgotten now. Sorry I missed this question.

Copy link
Contributor

@martonp martonp left a comment

Choose a reason for hiding this comment

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

This looks great.
I say let's get the #1262 and #1274 in first since that would finalize the contract, and then put this one before any other ones.

client/asset/eth/eth.go Outdated Show resolved Hide resolved
client/asset/eth/eth.go Outdated Show resolved Hide resolved
client/asset/eth/nodeclient.go Outdated Show resolved Hide resolved
client/asset/eth/nodeclient.go Outdated Show resolved Hide resolved
// called and the gas used for each. (◍•﹏•)
InitGas = 158000 // gas

RedeemGas = 63000 // gas
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the contracts are versioned now, these gas estimates will also need to be versioned.

Comment on lines 797 to 780
func (eth *ExchangeWallet) Withdraw(addr string, value, _ uint64) (asset.Coin, error) {
_, err := eth.sendToAddr(common.HexToAddress(addr), value)
Copy link
Member Author

Choose a reason for hiding this comment

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

We should revisit this at some point. If a user tries to withdraw their entire balance, it will fail and they'll lose the tx fees, I believe.

Copy link
Member

@chappjc chappjc Nov 17, 2021

Choose a reason for hiding this comment

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

Yikes, how does one sweep an ETH wallet completely, leaving nothing left if gas doesn't eat up whatever was allocated to gas?

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe the wallet won't allow the transaction to be sent the way I've described. I'm not certain.

Copy link
Member

@chappjc chappjc Nov 17, 2021

Choose a reason for hiding this comment

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

for ref: https://www.reddit.com/r/ethdev/comments/pits6s/sweep_all_eth_using_dynamic_fees_eip1559_and/
EIP-1559 does make this a headache if not impossible to do without leaving dust

Out of curiosity I tried out a popular eth wallet and despite being eip-1559 aware, it's support for sweep is non-existent. A tx draft wavers between leaving dust and having insufficient funds, while the base fee fluctuates.

Copy link
Contributor

Choose a reason for hiding this comment

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

Haha, basically the way to sweep it is to give the dust to the miners.

For the original issue of the failing transaction, I would think that the node would have a check that refuses to submit a transaction where the gasLimit*gasPrice + value > balance, but I haven't tried it out yet.


state := dexeth.SwapStep(swap.State)
if test.success && state != dexeth.SSInitiated {
t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSInitiated, state)
Copy link
Member Author

@buck54321 buck54321 Nov 17, 2021

Choose a reason for hiding this comment

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

I cannot figure out why this isn't passing. I'm verifying the secret hash matches thoughout. There are no errors. I can see the transaction with eth.getTransaction afterwards, and I can see the secret hash in the tx data, but the ETHSwapSwap is completely zeroed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've been trying to figure it out what's wrong.. haven't had any luck yet.

I also saw that you removed the addSignerToOpts calls from the functions that submit transactions (initiate, redeem, etc.) and you're calling them from the test code, but in some cases you forgot to call it. What's wrong with this going into intiate and redeem?

Copy link
Member Author

@buck54321 buck54321 Nov 18, 2021

Choose a reason for hiding this comment

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

Here's a clue.

https://github.com/decred/dcrdex/compare/master...buck54321:fail-init-real-balance?expand=1

That fails too.

Nevermind. That failed because I wasn't setting the TransactOpts value

Copy link
Member Author

@buck54321 buck54321 Nov 18, 2021

Choose a reason for hiding this comment

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

This branch is failing for > 1 swap. Maybe another manifestation of #1304?

Copy link
Member Author

Choose a reason for hiding this comment

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

Update: I've got everything passing except for TestReplayAttack.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nice, what was wrong? I was getting a headache from that.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I was handling a *big.Int wrong. Not making a copy when I should have or something.

@buck54321 buck54321 force-pushed the eth-contract-versioning branch 2 times, most recently from 196a375 to 145481a Compare November 17, 2021 17:23
@buck54321
Copy link
Member Author

Rebased and un-renamed the rpcclient*.go files back to the original names in an attempt to provide a usable diff for review. Will rename again later.

I'm still banging my head against the wall on the swap state issue, but I'm gonna step away for a minute. @martonp, if you have a sec to take a look, I'd really appreciate it.

@buck54321 buck54321 force-pushed the eth-contract-versioning branch 3 times, most recently from 1cb84e8 to c2b1f43 Compare November 18, 2021 18:04
@chappjc chappjc modified the milestone: 0.4 Nov 22, 2021
@chappjc chappjc added the ETH label Nov 22, 2021
@@ -82,7 +82,6 @@ if [ "${CHAIN_ADDRESS}" != "_" ]; then

[Eth.Miner]
Etherbase = "0x${CHAIN_ADDRESS}"
GasPrice = 81000000000
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, sorry I couldn't answer you somewhere where you asked about this, I don't know. I'm not up to speed on the base fee, thing. The miners can set the gas price over time is all I think I know. Also, I think geth complained without setting this, maybe it doesn't now.

@JoeGruffins
Copy link
Member

So, the reentry test, it doesn't look like it's communicating with the contract. The balances aren't the simnet's contract's balance, and the initiations aren't being sent to the contract. Or, did you just fix it?

@buck54321
Copy link
Member Author

buck54321 commented Nov 24, 2021

So, the reentry test, it doesn't look like it's communicating with the contract. The balances aren't the simnet's contract's balance, and the initiations aren't being sent to the contract. Or, did you just fix it?

Thanks, @JoeGruffins. The contract address must've been part of it. It's partly passing now, but still seeing an unexpected change in the contract balance. Still missing something.

@JoeGruffins
Copy link
Member

JoeGruffins commented Nov 25, 2021

Passing for me. Please see the rpcclient.balance comment I made. *addr

@buck54321 buck54321 marked this pull request as ready for review November 25, 2021 17:58
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.

The Contractor interface looks good.

I'm not completely sold on the rpcclient changes. I think it was less work for us before. But it's fine I guess.

Also, some conflicts with #1309 so hopefully mine goes in first :P

node, err := prepareNode(&nodeConfig{
net: net,
appDir: dir,
logger: log.SubLogger("NODE"),
Copy link
Member

Choose a reason for hiding this comment

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

It looks like you already make this logger at the call site.

client/asset/eth/rpcclient.go Outdated Show resolved Hide resolved
Comment on lines 184 to 181
// balance gets the current balance of an address.
func (c *rpcclient) balance(ctx context.Context, addr *common.Address) (*big.Int, error) {
return c.ec.BalanceAt(ctx, *addr, nil)
func (n *nodeClient) balance(ctx context.Context) (*big.Int, error) {
return n.addressBalance(ctx, n.creds.addr)
}
Copy link
Member

Choose a reason for hiding this comment

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

This is the balance for the wallet only correct? Can change doc.

client/asset/eth/rpcclient.go Outdated Show resolved Hide resolved
return status != "Unlocked"
}

// sendToAddr sends funds from acct to addr.
Copy link
Member

Choose a reason for hiding this comment

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

acct is no longer a parameter, so from the internal account maybe?

}), n.chainID)

if err != nil {
return nil, fmt.Errorf("signing error: %v", err)
Copy link
Member

Choose a reason for hiding this comment

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

Depending on whether eth increments the nonce, this could cause problems if erroring if it does increment. I guess it would increment maybe when the tx is sent though? In that case it would be fine to let this error. But if it already marked it as used I don't know if it would get stuck trying to send transactions/swaps with an out of order nonce.

Copy link
Member Author

Choose a reason for hiding this comment

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

See other comment about nonce too, but go-ethereum seems to operate with the assumption that GetPoolNonce is accurate as soon as SendTx returns.

client/asset/eth/rpcclient.go Outdated Show resolved Hide resolved
dex/networks/eth/contractor.go Outdated Show resolved Hide resolved
Comment on lines 147 to 148
Secret: secret,
SecretHash: sha256.Sum256(secret[:]),
Copy link
Member

Choose a reason for hiding this comment

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

These couldn't be fake? I think there was some discussion about this but I forget. EstimateInit is using fake secret hashes.

Copy link
Member Author

Choose a reason for hiding this comment

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

Apparently not.

Copy link
Member

Choose a reason for hiding this comment

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

It was wrong with random bytes? I would expect only the length to be important, not the content.

client/asset/eth/eth.go Outdated Show resolved Hide resolved
@chappjc chappjc self-requested a review November 26, 2021 16:34
transactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
sendTransaction(ctx context.Context, tx map[string]string) (common.Hash, error)
connect(ctx context.Context) error
initiate(ctx context.Context, contracts []*asset.Contract, maxFeeRate uint64, contractVer uint32) (*types.Transaction, error)
Copy link
Member Author

Choose a reason for hiding this comment

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

The contractVer doesn't need to map precisely to the dex.Asset.Version, but as long as we make a rule that a change in contract always accompanies a bump in dex.Asset.Version, then we can find a scheme to get the contract version from the asset version and we don't need any new data.

Copy link
Member

@chappjc chappjc left a comment

Choose a reason for hiding this comment

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

A few quick comments, will have a proper review later today.

client/asset/eth/rpcclient.go Outdated Show resolved Hide resolved
client/asset/eth/rpcclient.go Outdated Show resolved Hide resolved
client/asset/eth/rpcclient.go Outdated Show resolved Hide resolved
client/asset/eth/rpcclient.go Outdated Show resolved Hide resolved
client/asset/eth/rpcclient.go Outdated Show resolved Hide resolved
dex/networks/eth/contractor.go Outdated Show resolved Hide resolved
dex/networks/eth/contractor.go Outdated Show resolved Hide resolved
// contractorv0 is the Contractor for contract version 0.
// Redeem and Refund methods of ETHSwap already have suitable return types.
type contractorV0 struct {
*ETHSwap
Copy link
Member

@chappjc chappjc Nov 26, 2021

Choose a reason for hiding this comment

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

We should save the following suggestion for another day, but since ETHSwap is the v0 contract API compiled from it's solidity, we'll probably want to move dex/networks/eth/contract.go -> dex/networks/eth/contracts/v0.go or dex/networks/eth/contracts/apis/v0.go

Copy link
Member

@chappjc chappjc left a comment

Choose a reason for hiding this comment

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

A bit more. Sorry I'm reviewing this in 20 minute stints.

dex/networks/eth/params.go Outdated Show resolved Hide resolved
dex/networks/eth/params.go Outdated Show resolved Hide resolved
client/asset/eth/eth.go Outdated Show resolved Hide resolved
dex/networks/eth/params.go Outdated Show resolved Hide resolved
dex/networks/eth/params.go Outdated Show resolved Hide resolved
dex/networks/eth/params.go Outdated Show resolved Hide resolved
// state of a swap.
SSNone SwapStep = iota
// SSInitiated indicates that the swap has been initiated.
SSInitiated
Copy link
Member

Choose a reason for hiding this comment

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

Just a note that like contract.go, these SwapSteps are potentially contract version-specific and we'll have to address that when we move contract.go into subfolders for the different contract api versions as described in my other comment. Same deal with SwapState. The v0 Contractor uses these types, but it's not clear they will be the same for all future contract versions.

We're taking a leap with the definition of Swap(ctx context.Context, secretHash [32]byte) (*SwapState, error) in the Contractor interface that the swap state will always have the same definition, or that we can shoe-horn any version's swap "state" into this definition. I hope that's the case.

Copy link
Member Author

Choose a reason for hiding this comment

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

The SwapState is already a mirror of the ETHSwapState, which is the contract-specific struct. The Contractor is translating. SwapStep and SwapState are meant to defined for the needs of the DEX protocol, so in theory, shouldn't need modification with future contract changes. I wouldn't rule out that we do need to modify this stuff someday though, esp. SwapState, but it's a struct so we can always make it work, I think.

Copy link
Member

@chappjc chappjc Nov 28, 2021

Choose a reason for hiding this comment

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

Understood that it's supposed to be a version-independent translation of ETHSwapState. I'm just crossing my fingers that it will always be adequate (all fields applicable and/or sufficient).
But meh, we can change the struct in the future so a translation is good.

_, err := eth.sendToAddr(common.HexToAddress(addr),
bigVal.Mul(bigVal, gweiFactorBig), big.NewInt(0).SetUint64(defaultGasFee))
func (eth *ExchangeWallet) Withdraw(addr string, value, _ uint64) (asset.Coin, error) {
_, err := eth.node.sendToAddr(eth.ctx, common.HexToAddress(addr), value)
Copy link
Member

@chappjc chappjc Nov 27, 2021

Choose a reason for hiding this comment

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

We seem to be deviating from the withdraw semantics we have been following with the other assets. That is, this gives to addr a certain value, whereas with the other assets we've had it reduce the wallet balance by value, giving addr something less than value by network fees. I'm basing this comment on my understanding of (*nodeClient).sendToAddr.
It does not seem to be an issue with this PR, but something we should resolve before production.

Copy link
Member Author

Choose a reason for hiding this comment

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

Continuation of previous conversation, I think, but yes. We need to address this.

@buck54321 buck54321 force-pushed the eth-contract-versioning branch 4 times, most recently from 19d93a0 to 616fd74 Compare November 28, 2021 21:51
Copy link
Member

@chappjc chappjc 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. Holding out for #1248, and for the outstanding reviewers to circle back.

@chappjc chappjc added this to the 0.5 milestone Nov 28, 2021
Comment on lines +97 to +109
// GweiToWei converts *big.Int Wei to uint64 Gwei.
func WeiToGwei(v *big.Int) uint64 {
return new(big.Int).Div(v, BigGweiFactor).Uint64()
}
Copy link
Member

Choose a reason for hiding this comment

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

The original value could potentially be negative or overflow the returned uint64. There is also already a ToGwei in cointypes.go

Copy link
Contributor

@martonp martonp left a comment

Choose a reason for hiding this comment

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

LGTM

The client/asset/eth package was using geth like an RPC client. That's
not crazy, considering we have still an *rpc.Client, but in reality,
there is no RPC server, and the client is just a shortcut to RPC handlers
that call functions that we already have access to through e.g.
*les.LightEthereum or *keystore.KeyStore. This removes most "rpc"
calls in favor of calling the underlying methods directly.

Moves some common types to dex/networks/eth. As a general rule, if
the client/asset/ imports from server/asset, we should just move it
to dex/networks.

Creates the Contractor interface, which serves as a clear boundary
between dex-space and abigen-space. This also enables clean upgrades,
implemented here.

Remove extraneous methods from the ethFetcher interface. Convert
Contractor redemption gas estimation to batch. Move node.Node
and KeyStore initialization to nodeClient constructor and remove
unused ExchangeWallet fields.

Move Contractor to client/asset/eth and unexport. Add params tests.
Use new(big.Int) instead of bit.NewInt(0). Version the gas values.

rename rpcclient files

add nonce-send mutex

fix atomic whoopsie
@chappjc chappjc merged commit b66b579 into decred:master Nov 29, 2021
@@ -1085,58 +941,9 @@ func TestSwap(t *testing.T) {
testName, receipt.Contract(), contract.SecretHash)
}

if !bytes.Equal(node.lastInitiation.hash.Bytes(), receipt.Coin().ID()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you remove these? Shouldn't we be checking that initiate is called with correct arguments?

Copy link
Member

Choose a reason for hiding this comment

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

Good call. You probably see the PR, but I'll link for all: #1318

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