Skip to content

Commit

Permalink
eth/client: AuditContract
Browse files Browse the repository at this point in the history
AuditContract decodes the init txns call data, matches the swap specified in
the contract data bytes, extracts the recipient address, lock time, and value,
and optionally rebroadcasts the transaction.
  • Loading branch information
martonp authored Dec 9, 2021
1 parent 90716b9 commit bd1d3b2
Show file tree
Hide file tree
Showing 4 changed files with 364 additions and 3 deletions.
71 changes: 68 additions & 3 deletions client/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"decred.org/dcrdex/dex/encode"
"decred.org/dcrdex/dex/keygen"
dexeth "decred.org/dcrdex/dex/networks/eth"
swapv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/hdkeychain/v3"
"github.com/ethereum/go-ethereum"
Expand Down Expand Up @@ -162,6 +163,7 @@ type ethFetcher interface {
signData(addr common.Address, data []byte) ([]byte, error)
sendToAddr(ctx context.Context, addr common.Address, val uint64) (*types.Transaction, error)
transactionConfirmations(context.Context, common.Hash) (uint32, error)
sendSignedTransaction(ctx context.Context, tx *types.Transaction) error
}

// Check that ExchangeWallet satisfies the asset.Wallet interface.
Expand Down Expand Up @@ -772,9 +774,72 @@ func (eth *ExchangeWallet) SignMessage(coin asset.Coin, msg dex.Bytes) (pubkeys,

// AuditContract retrieves information about a swap contract on the
// blockchain. This would be used to verify the counter-party's contract
// during a swap.
func (*ExchangeWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroadcast bool) (*asset.AuditInfo, error) {
return nil, asset.ErrNotImplemented
// during a swap. coinID is expected to be the transaction id, and must
// be the same as the hash of txData. contract is expected to be
// (contractVersion|secretHash) where the secretHash uniquely keys the swap.
func (eth *ExchangeWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroadcast bool) (*asset.AuditInfo, error) {
tx := new(types.Transaction)
err := tx.UnmarshalBinary(txData)
if err != nil {
return nil, fmt.Errorf("AuditContract: failed to unmarshal transaction: %w", err)
}

txHash := tx.Hash()
if !bytes.Equal(coinID, txHash[:]) {
return nil, fmt.Errorf("AuditContract: coin id != txHash - coin id: %x, txHash: %x", coinID, tx.Hash())
}

initiations, err := dexeth.ParseInitiateData(tx.Data())
if err != nil {
return nil, fmt.Errorf("AuditContract: failed to parse initiate data: %w", err)
}

_, secretHash, err := dexeth.DecodeContractData(contract)
if err != nil {
return nil, fmt.Errorf("AuditContract: failed to decode versioned bytes: %w", err)
}

var initiation *swapv0.ETHSwapInitiation
for _, init := range initiations {
if init.SecretHash == secretHash {
initiation = &init
break
}
}
if initiation == nil {
return nil, errors.New("AuditContract: tx does not initiate secret hash")
}

expiration := time.Unix(initiation.RefundTimestamp.Int64(), 0)
recipient := initiation.Participant.Hex()
gweiVal, err := dexeth.ToGwei(initiation.Value)
if err != nil {
return nil, fmt.Errorf("AuditContract: failed to convert value to gwei: %w", err)
}

coin := &coin{
id: coinID,
value: gweiVal,
}

// The counter-party should have broadcasted the contract tx but rebroadcast
// just in case to ensure that the tx is sent to the network. Do not block
// because this is not required and does not affect the audit result.
if rebroadcast {
go func() {
if err := eth.node.sendSignedTransaction(eth.ctx, tx); err != nil {
eth.log.Debugf("Rebroadcasting counterparty contract %v (THIS MAY BE NORMAL): %v", txHash, err)
}
}()
}

return &asset.AuditInfo{
Recipient: recipient,
Expiration: expiration,
Coin: coin,
Contract: contract,
SecretHash: secretHash[:],
}, nil
}

// LocktimeExpired returns true if the specified contract's locktime has
Expand Down
214 changes: 214 additions & 0 deletions client/asset/eth/eth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/encode"
dexeth "decred.org/dcrdex/dex/networks/eth"
swapv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
Expand Down Expand Up @@ -162,6 +163,19 @@ func (n *testNode) signData(addr common.Address, data []byte) ([]byte, error) {

return crypto.Sign(crypto.Keccak256(data), n.privKeyForSigning)
}
func (n *testNode) sendSignedTransaction(ctx context.Context, tx *types.Transaction) error {
return nil
}

func tTx(gasFeeCap, gasTipCap, value uint64, to *common.Address, data []byte) *types.Transaction {
return types.NewTx(&types.DynamicFeeTx{
GasFeeCap: dexeth.GweiToWei(gasFeeCap),
GasTipCap: dexeth.GweiToWei(gasTipCap),
To: to,
Value: dexeth.GweiToWei(value),
Data: data,
})
}

func (n *testNode) sendToAddr(ctx context.Context, addr common.Address, val uint64) (*types.Transaction, error) {
return nil, nil
Expand Down Expand Up @@ -1510,6 +1524,206 @@ func TestMaxOrder(t *testing.T) {
}
}

func overMaxWei() *big.Int {
maxInt := ^uint64(0)
maxWei := new(big.Int).SetUint64(maxInt)
gweiFactorBig := big.NewInt(dexeth.GweiFactor)
maxWei.Mul(maxWei, gweiFactorBig)
overMaxWei := new(big.Int).Set(maxWei)
return overMaxWei.Add(overMaxWei, gweiFactorBig)
}

func TestAuditContract(t *testing.T) {
node := &testNode{}
eth := &ExchangeWallet{
node: node,
log: tLogger,
}

numSecretHashes := 3
secretHashes := make([][32]byte, 0, numSecretHashes)
for i := 0; i < numSecretHashes; i++ {
var secretHash [32]byte
copy(secretHash[:], encode.RandomBytes(32))
secretHashes = append(secretHashes, secretHash)
}

now := time.Now().Unix() / 1000
laterThanNow := now + 1000

tests := []struct {
name string
contract dex.Bytes
initiations []swapv0.ETHSwapInitiation
differentHash bool
badTxData bool
badTxBinary bool
wantErr bool
wantRecipient string
wantExpiration time.Time
}{
{
name: "ok",
contract: dexeth.EncodeContractData(0, secretHashes[1]),
initiations: []swapv0.ETHSwapInitiation{
swapv0.ETHSwapInitiation{
RefundTimestamp: big.NewInt(now),
SecretHash: secretHashes[0],
Participant: testAddressA,
Value: big.NewInt(1),
},
swapv0.ETHSwapInitiation{
RefundTimestamp: big.NewInt(laterThanNow),
SecretHash: secretHashes[1],
Participant: testAddressB,
Value: big.NewInt(1),
},
},
wantRecipient: testAddressB.Hex(),
wantExpiration: time.Unix(laterThanNow, 0),
},
{
name: "coin id different than tx hash",
contract: dexeth.EncodeContractData(0, secretHashes[0]),
initiations: []swapv0.ETHSwapInitiation{
swapv0.ETHSwapInitiation{
RefundTimestamp: big.NewInt(now),
SecretHash: secretHashes[0],
Participant: testAddressA,
Value: big.NewInt(1),
},
},
differentHash: true,
wantErr: true,
},
{
name: "contract is invalid versioned bytes",
contract: []byte{},
wantErr: true,
},
{
name: "contract not part of transaction",
contract: dexeth.EncodeContractData(0, secretHashes[2]),
initiations: []swapv0.ETHSwapInitiation{
swapv0.ETHSwapInitiation{
RefundTimestamp: big.NewInt(now),
SecretHash: secretHashes[0],
Participant: testAddressA,
Value: big.NewInt(1),
},
swapv0.ETHSwapInitiation{
RefundTimestamp: big.NewInt(laterThanNow),
SecretHash: secretHashes[1],
Participant: testAddressB,
Value: big.NewInt(1),
},
},
wantErr: true,
},
{
name: "cannot parse tx data",
contract: dexeth.EncodeContractData(0, secretHashes[2]),
badTxData: true,
wantErr: true,
},
{
name: "cannot unmarshal tx binary",
contract: dexeth.EncodeContractData(0, secretHashes[1]),
initiations: []swapv0.ETHSwapInitiation{
swapv0.ETHSwapInitiation{
RefundTimestamp: big.NewInt(now),
SecretHash: secretHashes[0],
Participant: testAddressA,
Value: big.NewInt(1),
},
swapv0.ETHSwapInitiation{
RefundTimestamp: big.NewInt(laterThanNow),
SecretHash: secretHashes[1],
Participant: testAddressB,
Value: big.NewInt(1),
},
},
badTxBinary: true,
wantErr: true,
},
{
name: "value over max gwei",
contract: dexeth.EncodeContractData(0, secretHashes[1]),
initiations: []swapv0.ETHSwapInitiation{
swapv0.ETHSwapInitiation{
RefundTimestamp: big.NewInt(now),
SecretHash: secretHashes[0],
Participant: testAddressA,
Value: big.NewInt(1),
},
swapv0.ETHSwapInitiation{
RefundTimestamp: big.NewInt(laterThanNow),
SecretHash: secretHashes[1],
Participant: testAddressB,
Value: overMaxWei(),
},
},
wantErr: true,
},
}

for _, test := range tests {
txData, err := dexeth.PackInitiateData(test.initiations)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if test.badTxData {
txData = []byte{0}
}

tx := tTx(2, 300, uint64(len(test.initiations)), &testAddressC, txData)
txBinary, err := tx.MarshalBinary()
if err != nil {
t.Fatalf(`"%v": failed to marshal binary: %v`, test.name, err)
}
if test.badTxBinary {
txBinary = []byte{0}
}

txHash := tx.Hash()
if test.differentHash {
copy(txHash[:], encode.RandomBytes(32))
}

auditInfo, err := eth.AuditContract(txHash[:], test.contract, txBinary, true)
if test.wantErr {
if err == nil {
t.Fatalf(`"%v": expected error but did not get`, test.name)
}
continue
}
if err != nil {
t.Fatalf(`"%v": unexpected error: %v`, test.name, err)
}

if test.wantRecipient != auditInfo.Recipient {
t.Fatalf(`"%v": expected recipient %v != actual %v`, test.name, test.wantRecipient, auditInfo.Recipient)
}
if test.wantExpiration.Unix() != auditInfo.Expiration.Unix() {
t.Fatalf(`"%v": expected expiration %v != actual %v`, test.name, test.wantExpiration, auditInfo.Expiration)
}
if !bytes.Equal(txHash[:], auditInfo.Coin.ID()) {
t.Fatalf(`"%v": tx hash %x != coin id %x`, test.name, txHash, auditInfo.Coin.ID())
}
if !bytes.Equal(test.contract, auditInfo.Contract) {
t.Fatalf(`"%v": expected contract %x != actual %x`, test.name, test.contract, auditInfo.Contract)
}

_, expectedSecretHash, err := dexeth.DecodeContractData(test.contract)
if err != nil {
t.Fatalf(`"%v": failed to decode versioned bytes: %v`, test.name, err)
}
if !bytes.Equal(expectedSecretHash[:], auditInfo.SecretHash) {
t.Fatalf(`"%v": expected secret hash %x != actual %x`, test.name, expectedSecretHash, auditInfo.SecretHash)
}
}
}

func TestOwnsAddress(t *testing.T) {
address := "0b84C791b79Ee37De042AD2ffF1A253c3ce9bc27" // no "0x" prefix
if !common.IsHexAddress(address) {
Expand Down
10 changes: 10 additions & 0 deletions client/asset/eth/nodeclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,11 @@ func (n *nodeClient) addSignerToOpts(txOpts *bind.TransactOpts) error {
return nil
}

// signTransaction signs a transaction.
func (n *nodeClient) signTransaction(addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
return n.creds.ks.SignTx(accounts.Account{Address: addr}, tx, n.chainID)
}

// initiate initiates multiple swaps in the same transaction.
func (n *nodeClient) initiate(ctx context.Context, contracts []*asset.Contract, maxFeeRate uint64, contractVer uint32) (tx *types.Transaction, err error) {
gas := dexeth.InitGas(len(contracts), contractVer)
Expand Down Expand Up @@ -511,6 +516,11 @@ func (n *nodeClient) transactionConfirmations(ctx context.Context, txHash common
return 0, asset.CoinNotFoundError
}

// sendSignedTransaction injects a signed transaction into the pending pool for execution.
func (n *nodeClient) sendSignedTransaction(ctx context.Context, tx *types.Transaction) error {
return n.leth.ApiBackend.SendTx(ctx, tx)
}

// newTxOpts is a constructor for a TransactOpts.
func newTxOpts(ctx context.Context, from common.Address, val, maxGas uint64, maxFeeRate, gasTipCap *big.Int) *bind.TransactOpts {
if gasTipCap.Cmp(maxFeeRate) > 0 {
Expand Down
Loading

0 comments on commit bd1d3b2

Please sign in to comment.