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.
  • Loading branch information
chappjc committed Dec 1, 2021
1 parent 70df246 commit cd2d8ab
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 41 deletions.
4 changes: 2 additions & 2 deletions dex/networks/eth/cointypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"github.com/ethereum/go-ethereum/common"
)

// CoinIDFlag signifies the type of coin ID. Currenty an eth coin ID can be
// CoinIDFlag signifies the type of coin ID. Currently an eth coin ID can be
// either a contract address and secret hash or a txid.
type CoinIDFlag uint16

Expand Down Expand Up @@ -50,7 +50,7 @@ type CoinID interface {
const (
// coin type id (2) + tx id (32) + index (4) = 38
txCoinIDSize = 38
// coin type id (2) + address (20) + secret has (32) = 54
// coin type id (2) + address (20) + secret hash (32) = 54
swapCoinIDSize = 54
// coin type id (2) + address (20) + amount (8) + nonce (8) = 38
amountCoinIDSize = 38
Expand Down
25 changes: 25 additions & 0 deletions dex/networks/eth/params.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

//go:build lgpl
// +build lgpl

package eth

import (
"encoding/binary"
"errors"
"math"
"math/big"
"time"
Expand Down Expand Up @@ -65,6 +70,26 @@ var v0Gases = &Gases{
RefundGas: 43000,
}

// EncodeSwapData packs the contract version and the secret hash into a byte
// slice for communicating a swap's identity.
func EncodeSwapData(contractVersion uint32, swapKey [SecretHashSize]byte) []byte {
b := make([]byte, SecretHashSize+4)
binary.BigEndian.PutUint32(b[:4], contractVersion)
copy(b[4:], swapKey[:])
return b
}

// DecodeSwapData unpacks the contract version and secret hash.
func DecodeSwapData(data []byte) (contractVersion uint32, swapKey [SecretHashSize]byte, err error) {
if len(data) != SecretHashSize+4 {
err = errors.New("invalid swap data")
return
}
contractVersion = binary.BigEndian.Uint32(data[:4])
copy(swapKey[:], data[4:])
return
}

// InitGas calculates the gas required for a batch of n inits.
func InitGas(n int, contractVer uint32) uint64 {
if n == 0 {
Expand Down
6 changes: 3 additions & 3 deletions server/asset/eth/coiner.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func (c *swapCoin) validateRedeem(contractID []byte) error {
// confirming a swap. Even though we check the initial transaction's data, if
// that transaction were in mempool at the time, it could be swapped out with
// any other values if a user sent another transaction with a higher gas fee
// and the same account and nonce, effectivly voiding the transaction we
// and the same account and nonce, effectively voiding the transaction we
// expected to be mined.
func (c *swapCoin) Confirmations(_ context.Context) (int64, error) {
swap, err := c.backend.node.swap(c.backend.rpcCtx, c.secretHash)
Expand Down Expand Up @@ -224,7 +224,7 @@ func (c *swapCoin) Confirmations(_ context.Context) (int64, error) {
// Uninitiated state is zero confs. It could still be in mempool.
// It is important to only trust confirmations according to the
// swap contract. Until there are confirmations we cannot be sure
// that initiation happened successfuly.
// that initiation happened successfully.
if dexeth.SwapStep(swap.State) == dexeth.SSNone {
// Assume the tx still has a chance of being mined.
return 0, nil
Expand All @@ -234,7 +234,7 @@ func (c *swapCoin) Confirmations(_ context.Context) (int64, error) {

// The swap initiation transaction has some number of
// confirmations, and we are sure the secret hash belongs to
// this swap. Assert that the value, reciever, and locktime are
// this swap. Assert that the value, receiver, and locktime are
// as expected.
value, err := dexeth.ToGwei(new(big.Int).Set(swap.Value))
if err != nil {
Expand Down
25 changes: 13 additions & 12 deletions server/asset/eth/coiner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,9 @@ func TestNewSwapCoin(t *testing.T) {
func TestConfirmations(t *testing.T) {
contractAddr, nullAddr := new(common.Address), new(common.Address)
copy(contractAddr[:], encode.RandomBytes(20))
secretHash, txHash := [32]byte{}, [32]byte{}
var secret, secretHash, txHash [32]byte
copy(txHash[:], encode.RandomBytes(32))
copy(secret[:], secretSlice)
copy(secretHash[:], secretHashSlice)
tc := dexeth.TxCoinID{
TxID: txHash,
Expand All @@ -229,24 +230,24 @@ func TestConfirmations(t *testing.T) {
}{{
name: "ok has confs value not verified",
bn: 100,
swap: tSwap(97, locktime, value, dexeth.SSInitiated, &initParticipantAddr),
swap: tSwap(97, locktime, value, secret, dexeth.SSInitiated, &initParticipantAddr),
value: value,
ct: sctInit,
wantConfs: 3,
}, {
name: "ok no confs",
swap: tSwap(0, bigO, bigO, dexeth.SSNone, nullAddr),
swap: tSwap(0, bigO, bigO, secret, dexeth.SSNone, nullAddr),
value: value,
ct: sctInit,
}, {
name: "ok redeem swap status redeemed",
swap: tSwap(97, locktime, value, dexeth.SSRedeemed, &initParticipantAddr),
swap: tSwap(97, locktime, value, secret, dexeth.SSRedeemed, &initParticipantAddr),
value: bigO,
ct: sctRedeem,
wantConfs: 1,
}, {
name: "ok redeem swap status initiated",
swap: tSwap(97, locktime, value, dexeth.SSInitiated, &initParticipantAddr),
swap: tSwap(97, locktime, value, secret, dexeth.SSInitiated, &initParticipantAddr),
value: bigO,
ct: sctRedeem,
}, {
Expand All @@ -256,7 +257,7 @@ func TestConfirmations(t *testing.T) {
wantErr: true,
}, {
name: "redeem bad swap state None",
swap: tSwap(0, bigO, bigO, dexeth.SSNone, nullAddr),
swap: tSwap(0, bigO, bigO, secret, dexeth.SSNone, nullAddr),
value: bigO,
ct: sctRedeem,
wantErr: true,
Expand All @@ -268,37 +269,37 @@ func TestConfirmations(t *testing.T) {
wantErr: true,
}, {
name: "swap value causes ToGwei error",
swap: tSwap(99, locktime, overMaxWei(), dexeth.SSInitiated, &initParticipantAddr),
swap: tSwap(99, locktime, overMaxWei(), secret, dexeth.SSInitiated, &initParticipantAddr),
value: value,
ct: sctInit,
wantErr: true,
}, {
name: "value differs from initial transaction",
swap: tSwap(99, locktime, oneGweiMore, dexeth.SSInitiated, &initParticipantAddr),
swap: tSwap(99, locktime, oneGweiMore, secret, dexeth.SSInitiated, &initParticipantAddr),
value: value,
ct: sctInit,
wantErr: true,
}, {
name: "participant differs from initial transaction",
swap: tSwap(99, locktime, value, dexeth.SSInitiated, nullAddr),
swap: tSwap(99, locktime, value, secret, dexeth.SSInitiated, nullAddr),
value: value,
ct: sctInit,
wantErr: true,
}, {
name: "locktime not an int64",
swap: tSwap(99, new(big.Int).SetUint64(^uint64(0)), value, dexeth.SSInitiated, &initParticipantAddr),
swap: tSwap(99, new(big.Int).SetUint64(^uint64(0)), value, secret, dexeth.SSInitiated, &initParticipantAddr),
value: value,
ct: sctInit,
wantErr: true,
}, {
name: "locktime differs from initial transaction",
swap: tSwap(99, bigO, value, dexeth.SSInitiated, &initParticipantAddr),
swap: tSwap(99, bigO, value, secret, dexeth.SSInitiated, &initParticipantAddr),
value: value,
ct: sctInit,
wantErr: true,
}, {
name: "block number error",
swap: tSwap(97, locktime, value, dexeth.SSInitiated, &initParticipantAddr),
swap: tSwap(97, locktime, value, secret, dexeth.SSInitiated, &initParticipantAddr),
value: value,
ct: sctInit,
bnErr: errors.New(""),
Expand Down
43 changes: 31 additions & 12 deletions server/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func init() {
}

const (
version = 0
version = 0 // not the contract version, see Backend.ver instead
assetName = "eth"
// The blockPollInterval is the delay between calls to bestBlockHash to
// check for new blocks.
Expand Down Expand Up @@ -263,12 +263,39 @@ func (eth *Backend) BlockChannel(size int) <-chan *asset.BlockUpdate {
return c
}

// Contract is part of the asset.Backend interface.
func (eth *Backend) Contract(coinID, _ []byte) (*asset.Contract, error) {
// ValidateContract ensures that contractData encodes both the expected contract
// version targeted and the secret hash.
func (eth *Backend) ValidateContract(contractData []byte) error {
ver, _, err := dexeth.DecodeSwapData(contractData)
if err != nil {
return err
}
if ver != eth.ver {
return fmt.Errorf("incorrect contract version %d, wanted %d", ver, eth.ver)
}
return nil
}

// Contract is part of the asset.Backend interface. The contractData bytes
// encodes both the contract version targeted and the secret hash.
func (eth *Backend) Contract(coinID, contractData []byte) (*asset.Contract, error) {
ver, secretHash, err := dexeth.DecodeSwapData(contractData)
if err != nil {
return nil, err
}
if ver != eth.ver {
return nil, fmt.Errorf("incorrect contract version %d, wanted %d", ver, eth.ver)
}

sc, err := eth.newSwapCoin(coinID, sctInit)
if err != nil {
return nil, fmt.Errorf("unable to create coiner: %w", err)
}

if secretHash != sc.secretHash {
return nil, fmt.Errorf("secret hash mismatch between tx calldata and provided contract data")
}

// Confirmations performs some extra swap status checks if the the tx
// is mined.
_, err = sc.Confirmations(eth.rpcCtx)
Expand All @@ -278,7 +305,7 @@ func (eth *Backend) Contract(coinID, _ []byte) (*asset.Contract, error) {
return &asset.Contract{
Coin: sc,
SwapAddress: sc.counterParty.String(),
RedeemScript: sc.secretHash[:],
RedeemScript: dexeth.EncodeSwapData(eth.ver, sc.secretHash),
LockTime: encode.UnixTimeMilli(sc.locktime),
}, nil
}
Expand Down Expand Up @@ -342,14 +369,6 @@ func (eth *Backend) ValidateCoinID(coinID []byte) (string, error) {
return coinId.String(), nil
}

// ValidateContract ensures that the secret hash is the correct length.
func (eth *Backend) ValidateContract(secretHash []byte) error {
if len(secretHash) != dexeth.SecretHashSize {
return fmt.Errorf("secret hash is wrong size: want %d but got %d", dexeth.SecretHashSize, len(secretHash))
}
return nil
}

// CheckAddress checks that the given address is parseable.
func (eth *Backend) CheckAddress(addr string) bool {
return common.IsHexAddress(addr)
Expand Down
41 changes: 29 additions & 12 deletions server/asset/eth/eth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"math/big"
Expand Down Expand Up @@ -130,8 +131,9 @@ func (n *testNode) accountBalance(ctx context.Context, addr common.Address) (*bi
return n.acctBal, n.acctBalErr
}

func tSwap(bn int64, locktime, value *big.Int, state dexeth.SwapStep, participantAddr *common.Address) *dexeth.ETHSwapSwap {
func tSwap(bn int64, locktime, value *big.Int, secret [32]byte, state dexeth.SwapStep, participantAddr *common.Address) *dexeth.ETHSwapSwap {
return &swap.ETHSwapSwap{
Secret: secret,
InitBlockNumber: big.NewInt(bn),
RefundBlockTimestamp: locktime,
Participant: *participantAddr,
Expand Down Expand Up @@ -376,12 +378,16 @@ func TestContract(t *testing.T) {
gasPrice := big.NewInt(3e10)
value := big.NewInt(5e18)
tc := dexeth.TxCoinID{
TxID: txHash,
TxID: txHash,
Index: 1, // for the secret/hash in secretSlice/secretHashSlice
}
txCoinIDBytes := tc.Encode()
sc := dexeth.SwapCoinID{}
swapCoinIDBytes := sc.Encode()
locktime := big.NewInt(initLocktime)
var secret, secretHash [32]byte
copy(secret[:], secretSlice)
copy(secretHash[:], secretHashSlice)
tests := []struct {
name string
coinID []byte
Expand All @@ -392,12 +398,12 @@ func TestContract(t *testing.T) {
}{{
name: "ok",
tx: tTx(gasPrice, value, contractAddr, initCalldata),
swap: tSwap(97, locktime, value, dexeth.SSInitiated, &initParticipantAddr),
swap: tSwap(97, locktime, value, secret, dexeth.SSInitiated, &initParticipantAddr),
coinID: txCoinIDBytes,
}, {
name: "new coiner error, wrong tx type",
tx: tTx(gasPrice, value, contractAddr, initCalldata),
swap: tSwap(97, locktime, value, dexeth.SSInitiated, &initParticipantAddr),
swap: tSwap(97, locktime, value, secret, dexeth.SSInitiated, &initParticipantAddr),
coinID: swapCoinIDBytes,
wantErr: true,
}, {
Expand All @@ -419,7 +425,8 @@ func TestContract(t *testing.T) {
log: tLogger,
contractAddr: *contractAddr,
}
contract, err := eth.Contract(test.coinID, nil)
contractData := dexeth.EncodeSwapData(0, secretHash) // matches initCalldata
contract, err := eth.Contract(test.coinID, contractData)
if test.wantErr {
if err == nil {
t.Fatalf("expected error for test %q", test.name)
Expand Down Expand Up @@ -467,7 +474,8 @@ func TestRedemption(t *testing.T) {
receiverAddr, contractAddr := new(common.Address), new(common.Address)
copy(receiverAddr[:], encode.RandomBytes(20))
copy(contractAddr[:], encode.RandomBytes(20))
secretHash, txHash := [32]byte{}, [32]byte{}
var secret, secretHash, txHash [32]byte
copy(secret[:], secretSlice)
copy(secretHash[:], secretHashSlice)
copy(txHash[:], encode.RandomBytes(32))
gasPrice := big.NewInt(3e10)
Expand All @@ -493,7 +501,7 @@ func TestRedemption(t *testing.T) {
tx: tTx(gasPrice, bigO, contractAddr, redeemCalldata),
contractID: ccID.Encode(),
coinID: txCoinID.Encode(),
swp: tSwap(0, bigO, bigO, dexeth.SSRedeemed, receiverAddr),
swp: tSwap(0, bigO, bigO, secret, dexeth.SSRedeemed, receiverAddr),
}, {
name: "new coiner error, wrong tx type",
tx: tTx(gasPrice, bigO, contractAddr, redeemCalldata),
Expand All @@ -504,15 +512,15 @@ func TestRedemption(t *testing.T) {
name: "confirmations error, swap wrong state",
tx: tTx(gasPrice, bigO, contractAddr, redeemCalldata),
contractID: ccID.Encode(),
swp: tSwap(0, bigO, bigO, dexeth.SSRefunded, receiverAddr),
swp: tSwap(0, bigO, bigO, secret, dexeth.SSRefunded, receiverAddr),
coinID: txCoinID.Encode(),
wantErr: true,
}, {
name: "validate redeem error",
tx: tTx(gasPrice, bigO, contractAddr, redeemCalldata),
contractID: new(dexeth.SwapCoinID).Encode(),
coinID: txCoinID.Encode(),
swp: tSwap(0, bigO, bigO, dexeth.SSRedeemed, receiverAddr),
swp: tSwap(0, bigO, bigO, secret, dexeth.SSRedeemed, receiverAddr),
wantErr: true,
}}
for _, test := range tests {
Expand Down Expand Up @@ -595,19 +603,28 @@ func TestTxData(t *testing.T) {
func TestValidateContract(t *testing.T) {
tests := []struct {
name string
ver uint32
secretHash []byte
wantErr bool
}{{
name: "ok",
secretHash: make([]byte, 32),
secretHash: make([]byte, dexeth.SecretHashSize),
}, {
name: "wrong size",
secretHash: make([]byte, 31),
secretHash: make([]byte, dexeth.SecretHashSize-1),
wantErr: true,
}, {
name: "wrong version",
ver: 1,
secretHash: make([]byte, dexeth.SecretHashSize),
wantErr: true,
}}
for _, test := range tests {
eth := new(Backend)
err := eth.ValidateContract(test.secretHash)
swapData := make([]byte, 4+len(test.secretHash))
binary.BigEndian.PutUint32(swapData[:4], test.ver)
copy(swapData[4:], test.secretHash)
err := eth.ValidateContract(swapData)
if test.wantErr {
if err == nil {
t.Fatalf("expected error for test %q", test.name)
Expand Down

0 comments on commit cd2d8ab

Please sign in to comment.