Skip to content

Commit

Permalink
dex/networks,server/eth: decode swap data message blob
Browse files Browse the repository at this point in the history
The msgjson.Init.Contract and server/asset.Contract.RedeemScript fields
contain an encoding of the ETH contract version concatenated with the
swap's secret hash that is the unique key for the swap.

This updates server/asset/eth's ValidateContract and Contract methods
to decode and check this data.

This also adds the dexeth.EncodeSwapData and DecodeSwapData functions.

Arrayify swapReceipt.secretHash.
  • Loading branch information
chappjc committed Dec 8, 2021
1 parent 6bd4dc8 commit 6c947e0
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 215 deletions.
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 {
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)
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)
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
Loading

0 comments on commit 6c947e0

Please sign in to comment.