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

dex/networks,server/eth: decode swap data message blob #1320

Merged
merged 1 commit into from
Dec 8, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
87 changes: 47 additions & 40 deletions client/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
Expand Down Expand Up @@ -133,6 +132,12 @@ func (d *Driver) Create(params *asset.CreateWalletParams) error {
return CreateWallet(params)
}

// Balance is the current balance, including information about the pending
// balance.
type Balance struct {
chappjc marked this conversation as resolved.
Show resolved Hide resolved
Current, PendingIn, PendingOut *big.Int
}

// ethFetcher represents a blockchain information fetcher. In practice, it is
// satisfied by rpcclient. For testing, it can be satisfied by a stub.
type ethFetcher interface {
Expand Down Expand Up @@ -564,7 +569,7 @@ func (eth *ExchangeWallet) unlockFunds(coins asset.Coins) error {
// swapReceipt implements the asset.Receipt interface for ETH.
type swapReceipt struct {
txHash common.Hash
secretHash []byte
secretHash [dexeth.SecretHashSize]byte
// expiration and value can be determined with a blockchain
// lookup, but we cache these values to avoid this.
expiration time.Time
Expand All @@ -586,9 +591,10 @@ func (r *swapReceipt) Coin() asset.Coin {
}
}

// Contract returns the swap's secret hash.
// Contract returns the swap's identifying data, which the concatenation of the
// contract version and the secret hash.
func (r *swapReceipt) Contract() dex.Bytes {
return versionedBytes(r.ver, r.secretHash[:])
return dexeth.EncodeContractData(r.ver, r.secretHash)
}

// String returns a string representation of the swapReceipt.
Expand Down Expand Up @@ -645,12 +651,14 @@ func (eth *ExchangeWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin

txHash := tx.Hash()
for _, swap := range swaps.Contracts {
var secretHash [dexeth.SecretHashSize]byte
copy(secretHash[:], swap.SecretHash)
receipts = append(receipts,
&swapReceipt{
expiration: encode.UnixTimeMilli(int64(swap.LockTime)),
value: swap.Value,
txHash: txHash,
secretHash: swap.SecretHash,
secretHash: secretHash,
ver: swaps.AssetVersion,
})
}
Expand All @@ -667,7 +675,10 @@ func (eth *ExchangeWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin
}

// Redeem sends the redemption transaction, which may contain more than one
// redemption.
// redemption. All redemptions must be for the same contract version because the
// current API requires a single transaction reported (asset.Coin output), but
// conceptually a batch of redeems could be processed for any number of
// different contract addresses with multiple transactions.
func (eth *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) {
fail := func(err error) ([]dex.Bytes, asset.Coin, uint64, error) {
return nil, nil, 0, err
Expand All @@ -677,13 +688,32 @@ func (eth *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Co
return fail(errors.New("Redeem: must be called with at least 1 redemption"))
}

var contractVersion uint32 // require a consistent version since this is a single transaction
inputs := make([]dex.Bytes, 0, len(form.Redemptions))
var redeemedValue uint64
for _, redemption := range form.Redemptions {
var secretHash, secret [32]byte
copy(secretHash[:], redemption.Spends.SecretHash)
for i, redemption := range form.Redemptions {
// NOTE: redemption.Spends.SecretHash is a dup of the hash extracted
// from redemption.Spends.Contract. Even for scriptable UTXO assets, the
// redeem script in this Contract field is redundant with the SecretHash
// field as ExtractSwapDetails can be applied to extract the hash.
ver, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract)
Copy link
Member Author

Choose a reason for hiding this comment

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

Note that AuditContract is still in PR, but see also my comment: https://github.com/decred/dcrdex/pull/1319/files#r764101981

if err != nil {
return fail(fmt.Errorf("Redeem: invalid versioned swap contract data: %w", err))
}
if i == 0 {
contractVersion = ver
} else if contractVersion != ver {
return fail(fmt.Errorf("Redeem: inconsistent contract versions in RedeemForm.Redemptions: "+
"%d != %d", contractVersion, ver))
}

// Use the contract's free public view function to validate the secret
// against the secret hash, and ensure the swap is otherwise redeemable
// before broadcasting our secrets, which is especially important if we
// are maker (the swap initiator).
var secret [32]byte
copy(secret[:], redemption.Secret)
redeemable, err := eth.node.isRedeemable(secretHash, secret, form.AssetVersion)
redeemable, err := eth.node.isRedeemable(secretHash, secret, ver)
if err != nil {
return fail(fmt.Errorf("Redeem: failed to check if swap is redeemable: %w", err))
}
Expand All @@ -692,19 +722,20 @@ func (eth *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Co
secretHash, secret))
}

swapData, err := eth.node.swap(eth.ctx, secretHash, form.AssetVersion)
swapData, err := eth.node.swap(eth.ctx, secretHash, ver)
if err != nil {
return nil, nil, 0, fmt.Errorf("Redeem: error finding swap state: %w", err)
}
redeemedValue += swapData.Value
inputs = append(inputs, redemption.Spends.Coin.ID())
}
outputCoin := eth.createAmountCoin(redeemedValue)
chappjc marked this conversation as resolved.
Show resolved Hide resolved
fundsRequired := dexeth.RedeemGas(len(form.Redemptions), form.AssetVersion) * form.FeeSuggestion

outputCoin := eth.createFundingCoin(redeemedValue)
fundsRequired := dexeth.RedeemGas(len(form.Redemptions), contractVersion) * form.FeeSuggestion

// TODO: make sure the amount we locked for redemption is enough to cover the gas
// fees. Also unlock coins.
_, err := eth.node.redeem(eth.ctx, form.Redemptions, form.FeeSuggestion, form.AssetVersion)
_, 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 Expand Up @@ -744,7 +775,7 @@ func (*ExchangeWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroad
// LocktimeExpired returns true if the specified contract's locktime has
// expired, making it possible to issue a Refund.
func (eth *ExchangeWallet) LocktimeExpired(contract dex.Bytes) (bool, time.Time, error) {
contractVer, secretHash, err := decodeVersionedSecretHash(contract)
contractVer, secretHash, err := dexeth.DecodeContractData(contract)
if err != nil {
return false, time.Time{}, err
}
Expand Down Expand Up @@ -809,7 +840,7 @@ func (*ExchangeWallet) PayFee(address string, regFee, feeRateSuggestion uint64)
// SwapConfirmations gets the number of confirmations and the spend status
// for the specified swap.
func (eth *ExchangeWallet) SwapConfirmations(ctx context.Context, _ dex.Bytes, contract dex.Bytes, _ time.Time) (confs uint32, spent bool, err error) {
contractVer, secretHash, err := decodeVersionedSecretHash(contract)
contractVer, secretHash, err := dexeth.DecodeContractData(contract)
if err != nil {
return 0, false, err
}
Expand Down Expand Up @@ -944,27 +975,3 @@ func (eth *ExchangeWallet) checkForNewBlocks() {
prevTip.Hash(), newTip.NumberU64(), newTip.Hash())
go eth.tipChange(nil)
}

// Balance is the current balance, including information about the pending
// balance.
type Balance struct {
Current, PendingIn, PendingOut *big.Int
}

func versionedBytes(ver uint32, h []byte) []byte {
b := make([]byte, len(h)+4)
binary.BigEndian.PutUint32(b[:4], ver)
copy(b[4:], h)
return b
}

// decodeVersionedSecretHash unpacks the contract version and secret hash.
func decodeVersionedSecretHash(data []byte) (contractVersion uint32, swapKey [dexeth.SecretHashSize]byte, err error) {
if len(data) != dexeth.SecretHashSize+4 {
err = errors.New("invalid swap data")
return
}
contractVersion = binary.BigEndian.Uint32(data[:4])
copy(swapKey[:], data[4:])
return
}
45 changes: 29 additions & 16 deletions client/asset/eth/eth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"context"
"crypto/ecdsa"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
Expand Down Expand Up @@ -944,12 +943,16 @@ func TestSwap(t *testing.T) {
testName, receipt.Coin().Value(), contract.Value)
}
contractData := receipt.Contract()
if swaps.AssetVersion != binary.BigEndian.Uint32(contractData[:4]) {
ver, secretHash, err := dexeth.DecodeContractData(contractData)
if err != nil {
t.Fatalf("failed to decode contract data: %v", err)
}
if swaps.AssetVersion != ver {
t.Fatal("wrong contract version")
}
if !bytes.Equal(contractData[4:], contract.SecretHash[:]) {
if !bytes.Equal(contract.SecretHash, secretHash[:]) {
t.Fatalf("%v, contract: %x != secret hash in input: %x",
testName, receipt.Contract(), contract.SecretHash)
testName, receipt.Contract(), secretHash)
}

totalCoinValue += receipt.Coin().Value()
Expand Down Expand Up @@ -1135,9 +1138,13 @@ func TestPreRedeem(t *testing.T) {
}

func TestRedeem(t *testing.T) {
// Test with a non-zero contract version to ensure it makes it into the receipt
contractVer := uint32(1)
dexeth.VersionedGases[1] = dexeth.VersionedGases[0] // for dexeth.RedeemGas(..., 1)
defer delete(dexeth.VersionedGases, 1)
node := &testNode{
swapVers: map[uint32]struct{}{
0: {},
contractVer: {},
},
swapMap: make(map[[32]byte]*dexeth.SwapState),
}
Expand Down Expand Up @@ -1190,7 +1197,8 @@ func TestRedeem(t *testing.T) {
Redemptions: []*asset.Redemption{
{
Spends: &asset.AuditInfo{
SecretHash: secretHashes[0][:],
Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]),
SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth
Coin: &coin{
id: encode.RandomBytes(32),
},
Expand All @@ -1199,6 +1207,7 @@ func TestRedeem(t *testing.T) {
},
{
Spends: &asset.AuditInfo{
Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]),
SecretHash: secretHashes[1][:],
Coin: &coin{
id: encode.RandomBytes(32),
Expand All @@ -1208,7 +1217,6 @@ func TestRedeem(t *testing.T) {
},
},
FeeSuggestion: 100,
AssetVersion: 0,
},
},
{
Expand All @@ -1219,6 +1227,7 @@ func TestRedeem(t *testing.T) {
Redemptions: []*asset.Redemption{
{
Spends: &asset.AuditInfo{
Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]),
SecretHash: secretHashes[0][:],
Coin: &coin{
id: encode.RandomBytes(32),
Expand All @@ -1228,6 +1237,7 @@ func TestRedeem(t *testing.T) {
},
{
Spends: &asset.AuditInfo{
Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]),
SecretHash: secretHashes[1][:],
Coin: &coin{
id: encode.RandomBytes(32),
Expand All @@ -1237,7 +1247,6 @@ func TestRedeem(t *testing.T) {
},
},
FeeSuggestion: 100,
AssetVersion: 0,
},
},
{
Expand All @@ -1249,6 +1258,7 @@ func TestRedeem(t *testing.T) {
Redemptions: []*asset.Redemption{
{
Spends: &asset.AuditInfo{
Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]),
SecretHash: secretHashes[0][:],
Coin: &coin{
id: encode.RandomBytes(32),
Expand All @@ -1258,6 +1268,7 @@ func TestRedeem(t *testing.T) {
},
{
Spends: &asset.AuditInfo{
Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]),
SecretHash: secretHashes[1][:],
Coin: &coin{
id: encode.RandomBytes(32),
Expand All @@ -1267,7 +1278,6 @@ func TestRedeem(t *testing.T) {
},
},
FeeSuggestion: 100,
AssetVersion: 0,
},
},
{
Expand All @@ -1279,6 +1289,7 @@ func TestRedeem(t *testing.T) {
Redemptions: []*asset.Redemption{
{
Spends: &asset.AuditInfo{
Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]),
SecretHash: secretHashes[0][:],
Coin: &coin{
id: encode.RandomBytes(32),
Expand All @@ -1288,7 +1299,6 @@ func TestRedeem(t *testing.T) {
},
},
FeeSuggestion: 200,
AssetVersion: 0,
},
},
{
Expand All @@ -1299,6 +1309,7 @@ func TestRedeem(t *testing.T) {
Redemptions: []*asset.Redemption{
{
Spends: &asset.AuditInfo{
Contract: dexeth.EncodeContractData(contractVer, secretHashes[2]),
SecretHash: secretHashes[2][:],
Coin: &coin{
id: encode.RandomBytes(32),
Expand All @@ -1308,7 +1319,6 @@ func TestRedeem(t *testing.T) {
},
},
FeeSuggestion: 100,
AssetVersion: 0,
},
},
{
Expand All @@ -1318,7 +1328,6 @@ func TestRedeem(t *testing.T) {
form: asset.RedeemForm{
Redemptions: []*asset.Redemption{},
FeeSuggestion: 100,
AssetVersion: 0,
},
},
}
Expand Down Expand Up @@ -1359,8 +1368,12 @@ func TestRedeem(t *testing.T) {
test.name, coinID, ins[i])
}

var secretHash [32]byte
copy(secretHash[:], redemption.Spends.SecretHash)
_, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract)
if err != nil {
t.Fatalf("DecodeContractData: %v", err)
}
// secretHash should equal redemption.Spends.SecretHash, but it's
// not part of the Redeem code, just the test input consistency.
swap := node.swapMap[secretHash]
totalSwapValue += swap.Value
}
Expand Down Expand Up @@ -1684,7 +1697,7 @@ func TestSwapConfirmation(t *testing.T) {
ver := uint32(0)

checkResult := func(expErr bool, expConfs uint32, expSpent bool) {
confs, spent, err := eth.SwapConfirmations(nil, nil, versionedBytes(ver, secretHash[:]), time.Time{})
confs, spent, err := eth.SwapConfirmations(nil, nil, dexeth.EncodeContractData(ver, secretHash), time.Time{})
if err != nil {
if expErr {
return
Expand Down Expand Up @@ -1718,7 +1731,7 @@ func TestSwapConfirmation(t *testing.T) {

// CoinNotFoundError
state.State = dexeth.SSNone
_, _, err := eth.SwapConfirmations(nil, nil, versionedBytes(0, secretHash[:]), time.Time{})
_, _, err := eth.SwapConfirmations(nil, nil, dexeth.EncodeContractData(0, secretHash), time.Time{})
if !errors.Is(err, asset.CoinNotFoundError) {
t.Fatalf("expected CoinNotFoundError, got %v", err)
}
Expand Down