added new waitForNewSwapReceipt method for xmrmaker (#455)

This commit is contained in:
Dmitry Holodov
2023-05-03 11:08:00 -05:00
committed by GitHub
parent e3c24b5e4a
commit 998347ded0
14 changed files with 257 additions and 83 deletions

View File

@@ -17,7 +17,6 @@ import (
"time"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
"github.com/athanorlabs/atomic-swap/common"
@@ -55,7 +54,8 @@ func CreateTestConf(t *testing.T, ethKey *ecdsa.PrivateKey) *SwapdConfig {
envConf := new(common.Config)
*envConf = *common.ConfigDefaultsForEnv(common.Development)
envConf.DataDir = t.TempDir()
envConf.SwapCreatorAddr = getSwapCreatorAddress(t, ec.Raw())
// Passed in ETH key may not have funds, deploy contract with the funded taker key
envConf.SwapCreatorAddr, _ = contracts.DevDeploySwapCreator(t, ec.Raw(), tests.GetTakerTestKey(t))
return &SwapdConfig{
EnvConf: envConf,
@@ -133,28 +133,6 @@ func WaitForSwapdStart(t *testing.T, rpcPort uint16) {
t.Fatalf("giving up, swapd RPC port %d is not listening after %d seconds", rpcPort, maxSeconds)
}
// these variables are only for use by getSwapCreatorAddress
var _swapCreatorAddr *ethcommon.Address
var _swapCreatorAddrMu sync.Mutex
func getSwapCreatorAddress(t *testing.T, ec *ethclient.Client) ethcommon.Address {
_swapCreatorAddrMu.Lock()
defer _swapCreatorAddrMu.Unlock()
if _swapCreatorAddr != nil {
return *_swapCreatorAddr
}
ctx := context.Background()
ethKey := tests.GetTakerTestKey(t) // requester might not have ETH, so we don't pass the key in
swapCreatorAddr, _, err := contracts.DeploySwapCreatorWithKey(ctx, ec, ethKey)
require.NoError(t, err)
_swapCreatorAddr = &swapCreatorAddr
return swapCreatorAddr
}
// these variables are only for use by GetMockTokens
var _mockTokens map[string]ethcommon.Address
var _mockTokensMu sync.Mutex

View File

@@ -21,7 +21,7 @@ import (
func getContractCode(t *testing.T) []byte {
ec, _ := tests.NewEthClient(t)
pk := tests.GetMakerTestKey(t)
contractAddr, _ := deploySwapCreator(t, ec, pk)
contractAddr, _ := DevDeploySwapCreator(t, ec, pk)
code, err := ec.CodeAt(context.Background(), contractAddr, nil)
require.NoError(t, err)
return code
@@ -42,7 +42,7 @@ func TestCheckSwapCreatorContractCode(t *testing.T) {
ec, _ := tests.NewEthClient(t)
pk := tests.GetMakerTestKey(t)
contractAddr, _ := deploySwapCreator(t, ec, pk)
contractAddr, _ := DevDeploySwapCreator(t, ec, pk)
err := CheckSwapCreatorContractCode(context.Background(), ec, contractAddr)
require.NoError(t, err)
}

View File

@@ -13,3 +13,9 @@ const (
MaxRefundTokenGas = 47294
MaxTokenApproveGas = 47000 // 46223 with our contract
)
// constants that are interesting to track, but not used by swaps
const (
maxSwapCreatorDeployGas = 1094089
maxTestERC20DeployGas = 798286 // using long token names or symbols will increase this
)

View File

@@ -1,8 +0,0 @@
package contracts
// We don't deploy SwapCreator contracts or ERC20 token contracts in swaps, so
// these constants are only compiled in for test files.
const (
maxSwapCreatorDeployGas = 1094089
maxTestERC20DeployGas = 798286 // using long token names or symbols will increase this
)

View File

@@ -31,7 +31,7 @@ func DeploySwapCreatorWithKey(
}
log.Infof("deploying SwapCreator.sol")
address, tx, sf, err := DeploySwapCreator(txOpts, ec)
address, tx, sc, err := DeploySwapCreator(txOpts, ec)
if err != nil {
return ethcommon.Address{}, nil, fmt.Errorf("failed to deploy swap creator: %w", err)
}
@@ -42,7 +42,7 @@ func DeploySwapCreatorWithKey(
}
log.Infof("deployed SwapCreator.sol: address=%s tx hash=%s", address, tx.Hash())
return address, sf, nil
return address, sc, nil
}
func newTXOpts(ctx context.Context, ec *ethclient.Client, privkey *ecdsa.PrivateKey) (*bind.TransactOpts, error) {

View File

@@ -63,23 +63,11 @@ func approveERC20(t *testing.T,
require.GreaterOrEqual(t, MaxTokenApproveGas, int(receipt.GasUsed), "Token Approve")
}
func deploySwapCreator(t *testing.T, ec *ethclient.Client, pk *ecdsa.PrivateKey) (ethcommon.Address, *SwapCreator) {
swapCreatorAddr, tx, swapCreator, err := DeploySwapCreator(getAuth(t, pk), ec)
require.NoError(t, err)
receipt := getReceipt(t, ec, tx)
t.Logf("gas cost to deploy SwapCreator.sol: %d (delta %d)",
receipt.GasUsed, maxSwapCreatorDeployGas-int(receipt.GasUsed))
require.GreaterOrEqual(t, maxSwapCreatorDeployGas, int(receipt.GasUsed), "deploy SwapCreator")
return swapCreatorAddr, swapCreator
}
func testNewSwap(t *testing.T, asset types.EthAsset, erc20Contract *TestERC20) {
pk := tests.GetTakerTestKey(t)
ec, _ := tests.NewEthClient(t)
swapCreatorAddr, swapCreator := deploySwapCreator(t, ec, pk)
swapCreatorAddr, swapCreator := DevDeploySwapCreator(t, ec, pk)
owner := crypto.PubkeyToAddress(pk.PublicKey)
claimer := common.EthereumPrivateKeyToAddress(tests.GetMakerTestKey(t))
@@ -179,7 +167,7 @@ func TestSwapCreator_Claim_vec(t *testing.T) {
ec, _ := tests.NewEthClient(t)
addr := crypto.PubkeyToAddress(pkA.PublicKey)
_, swapCreator := deploySwapCreator(t, ec, pkA)
_, swapCreator := DevDeploySwapCreator(t, ec, pkA)
txOpts := getAuth(t, pkA)
txOpts.Value = defaultSwapValue
@@ -248,7 +236,7 @@ func testClaim(t *testing.T, asset types.EthAsset, newLogIndex int, value *big.I
ec, _ := tests.NewEthClient(t)
addr := crypto.PubkeyToAddress(pkA.PublicKey)
swapCreatorAddr, swapCreator := deploySwapCreator(t, ec, pkA)
swapCreatorAddr, swapCreator := DevDeploySwapCreator(t, ec, pkA)
if asset.IsToken() {
approveERC20(t, ec, pkA, erc20Contract, swapCreatorAddr, value)
@@ -344,7 +332,7 @@ func testRefundBeforeT1(t *testing.T, asset types.EthAsset, erc20Contract *TestE
ec, _ := tests.NewEthClient(t)
addr := crypto.PubkeyToAddress(pkA.PublicKey)
swapCreatorAddr, swapCreator := deploySwapCreator(t, ec, pkA)
swapCreatorAddr, swapCreator := DevDeploySwapCreator(t, ec, pkA)
if asset.IsToken() {
approveERC20(t, ec, pkA, erc20Contract, swapCreatorAddr, defaultSwapValue)
@@ -430,7 +418,7 @@ func testRefundAfterT2(t *testing.T, asset types.EthAsset, erc20Contract *TestER
ec, _ := tests.NewEthClient(t)
addr := crypto.PubkeyToAddress(pkA.PublicKey)
swapCreatorAddr, swapCreator := deploySwapCreator(t, ec, pkA)
swapCreatorAddr, swapCreator := DevDeploySwapCreator(t, ec, pkA)
if asset.IsToken() {
approveERC20(t, ec, pkA, erc20Contract, swapCreatorAddr, defaultSwapValue)
@@ -515,7 +503,7 @@ func TestSwapCreator_MultipleSwaps(t *testing.T) {
pkContractCreator := tests.GetTestKeyByIndex(t, 0)
ec, _ := tests.NewEthClient(t)
_, swapCreator := deploySwapCreator(t, ec, pkContractCreator)
_, swapCreator := DevDeploySwapCreator(t, ec, pkContractCreator)
const numSwaps = 16
type swapCase struct {

56
ethereum/test_support.go Normal file
View File

@@ -0,0 +1,56 @@
// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
//go:build !prod
package contracts
import (
"context"
"crypto/ecdsa"
"sync"
"testing"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
"github.com/athanorlabs/atomic-swap/ethereum/block"
)
//
// FUNCTIONS ONLY FOR UNIT TESTS
//
// these variables should only be accessed by DevDeploySwapCreator
var _swapCreator *SwapCreator
var _swapCreatorAddr *ethcommon.Address
var _swapCreatorAddrMu sync.Mutex
// DevDeploySwapCreator deploys and returns the swapCreator address and contract
// binding for unit tests, returning a cached result if available.
func DevDeploySwapCreator(t *testing.T, ec *ethclient.Client, pk *ecdsa.PrivateKey) (ethcommon.Address, *SwapCreator) {
ctx := context.Background()
_swapCreatorAddrMu.Lock()
defer _swapCreatorAddrMu.Unlock()
if _swapCreatorAddr == nil {
txOpts, err := newTXOpts(ctx, ec, pk)
require.NoError(t, err)
swapCreatorAddr, tx, swapCreator, err := DeploySwapCreator(txOpts, ec)
require.NoError(t, err)
receipt, err := block.WaitForReceipt(ctx, ec, tx.Hash())
require.NoError(t, err)
t.Logf("gas cost to deploy SwapCreator.sol: %d (delta %d)",
receipt.GasUsed, maxSwapCreatorDeployGas-int(receipt.GasUsed))
require.GreaterOrEqual(t, maxSwapCreatorDeployGas, int(receipt.GasUsed), "deploy SwapCreator")
_swapCreatorAddr = &swapCreatorAddr
_swapCreator = swapCreator
}
return *_swapCreatorAddr, _swapCreator
}

View File

@@ -5,10 +5,14 @@ package xmrmaker
import (
"context"
"errors"
"fmt"
"time"
"github.com/ethereum/go-ethereum"
ethcommon "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/athanorlabs/atomic-swap/coins"
"github.com/athanorlabs/atomic-swap/common"
@@ -77,6 +81,46 @@ func (s *swapState) setNextExpectedEvent(event EventType) error {
return nil
}
// waitForNewSwapReceipt waits for the newSwap transaction, that locks the
// taker's ETH, to be seen as included in a block by our endpoint. This is a
// pre-requirement for validating the newSwap transaction, which should be done
// after calling this method.
func waitForNewSwapReceipt(
ctx context.Context,
ec *ethclient.Client,
txHash ethcommon.Hash,
) (*ethtypes.Receipt, error) {
const loopPause = 1500 * time.Millisecond // 1.5 seconds
// In mainnet testing, when the maker and taker are using different ETH
// endpoints, we've seen cases where the taker receives a TX receipt and
// transmits the hash to the maker before the maker's side thinks the TX has
// been included in a block. We wait for up to 15 seconds if our attempts at
// getting the transaction receipt return NotFound.
for i := 0; i < 10; i++ {
receipt, err := ec.TransactionReceipt(ctx, txHash)
if err != nil && !errors.Is(err, ethereum.NotFound) {
return nil, err
}
// If err is still set, the error was ethereum.NotFound, which is returned
// even if our endpoint sees the TX as pending.
if err != nil {
if err = common.SleepWithContext(ctx, loopPause); err != nil {
return nil, err // context expired
}
continue
}
if receipt.Status != ethtypes.ReceiptStatusSuccessful {
return nil, fmt.Errorf("received newSwap tx=%s was reverted", txHash.Hex())
}
return receipt, nil
}
return nil, ethereum.NotFound
}
func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) error {
if msg.Address == (ethcommon.Address{}) {
return errMissingAddress
@@ -96,7 +140,7 @@ func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) error {
s.contractSwapID = msg.ContractSwapID
s.contractSwap = msg.ContractSwap
receipt, err := s.Backend.ETHClient().Raw().TransactionReceipt(s.ctx, msg.TxHash)
receipt, err := waitForNewSwapReceipt(s.ctx, s.Backend.ETHClient().Raw(), msg.TxHash)
if err != nil {
return err
}

View File

@@ -0,0 +1,118 @@
package xmrmaker
import (
"context"
"fmt"
"math/big"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum"
ethcommon "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
"github.com/athanorlabs/atomic-swap/common/types"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
"github.com/athanorlabs/atomic-swap/tests"
)
func Test_waitForNewSwapReceipt(t *testing.T) {
ec, _ := tests.NewEthClient(t)
pk := tests.GetMakerTestKey(t)
addr := crypto.PubkeyToAddress(pk.PublicKey)
_, swapCreator := contracts.DevDeploySwapCreator(t, ec, pk)
timeoutDuration := big.NewInt(10)
value := big.NewInt(2e16)
tx, err := swapCreator.NewSwap(
tests.TxOptsWithValue(t, pk, value),
[32]byte{1},
[32]byte{2},
addr,
timeoutDuration,
timeoutDuration,
types.EthAssetETH.Address(),
value,
contracts.GenerateNewSwapNonce(),
)
require.NoError(t, err)
// Simulate a maker's endpoint not being synchronized with the taker's
// endpoint by calling waitForNewSwapReceipt without waiting for the
// transaction to be mined into a block.
var wg sync.WaitGroup
wg.Add(1)
defer wg.Wait()
go func() {
defer wg.Done()
time.Sleep(2 * time.Second)
_ = tests.MineTransaction(t, ec, tx)
}()
receipt, err := waitForNewSwapReceipt(context.Background(), ec, tx.Hash())
require.NoError(t, err)
require.Equal(t, tx.Hash(), receipt.TxHash)
}
func Test_waitForNewSwapReceipt_reverted(t *testing.T) {
ctx := context.Background()
ec, chainID := tests.NewEthClient(t)
pk := tests.GetMakerTestKey(t)
addr := crypto.PubkeyToAddress(pk.PublicKey)
swapCreatorAddr, _ := contracts.DevDeploySwapCreator(t, ec, pk)
// Create a NewSwap transaction using lots of zero values that are checked in the contract
// to trigger a revert
callData, err := contracts.SwapCreatorParsedABI.Pack(
"newSwap",
[32]byte{0},
[32]byte{0},
addr,
new(big.Int),
new(big.Int),
ethcommon.Address{},
new(big.Int),
new(big.Int),
)
require.NoError(t, err)
nonce, err := ec.PendingNonceAt(context.Background(), addr)
require.NoError(t, err)
gasPrice, err := ec.SuggestGasPrice(context.Background())
require.NoError(t, err)
tx := ethtypes.NewTx(&ethtypes.LegacyTx{
Nonce: nonce,
GasPrice: gasPrice,
Gas: contracts.MaxNewSwapETHGas,
To: &swapCreatorAddr,
Value: nil,
Data: callData,
})
signedTx, err := ethtypes.SignTx(tx, ethtypes.NewEIP155Signer(chainID), pk)
require.NoError(t, err)
err = ec.SendTransaction(ctx, signedTx)
require.NoError(t, err)
_, err = waitForNewSwapReceipt(ctx, ec, signedTx.Hash())
require.ErrorContains(t, err, fmt.Sprintf("received newSwap tx=%s was reverted", signedTx.Hash()))
}
func Test_waitForNewSwapReceipt_NotFound(t *testing.T) {
ctx := context.Background()
ec, _ := tests.NewEthClient(t)
txHash := ethcommon.Hash{0x1, 0x2}
// Requires a 15 second wait
_, err := waitForNewSwapReceipt(ctx, ec, txHash)
require.ErrorIs(t, err, ethereum.NotFound)
}

View File

@@ -106,7 +106,7 @@ func newSwapStateFromStart(
desiredAmount coins.EthAssetAmount,
) (*swapState, error) {
// at this point, we've received the counterparty's keys,
// and will send our own after this function returns.
// and we'll send our own after this function returns.
// see HandleInitiateMessage().
stage := types.KeysExchanged
if offerExtra.StatusCh == nil {

View File

@@ -4,7 +4,6 @@
package relayer
import (
"context"
"crypto/ecdsa"
"math/big"
"testing"
@@ -12,7 +11,6 @@ import (
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
"github.com/athanorlabs/atomic-swap/common/types"
@@ -20,23 +18,6 @@ import (
"github.com/athanorlabs/atomic-swap/tests"
)
// Speed up tests a little by giving deployContracts(...) a package-level cache.
// These variables should not be accessed by other functions.
var _swapCreatorAddr *ethcommon.Address
// deployContracts deploys and returns the swapCreator addresses.
func deployContracts(t *testing.T, ec *ethclient.Client, key *ecdsa.PrivateKey) ethcommon.Address {
ctx := context.Background()
if _swapCreatorAddr == nil {
swapCreatorAddr, _, err := contracts.DeploySwapCreatorWithKey(ctx, ec, key)
require.NoError(t, err)
_swapCreatorAddr = &swapCreatorAddr
}
return *_swapCreatorAddr
}
func createTestSwap(claimer ethcommon.Address) *contracts.SwapCreatorSwap {
return &contracts.SwapCreatorSwap{
Owner: ethcommon.Address{0x1},
@@ -56,7 +37,7 @@ func TestCreateRelayClaimRequest(t *testing.T) {
claimer := crypto.PubkeyToAddress(*ethKey.Public().(*ecdsa.PublicKey))
ec, _ := tests.NewEthClient(t)
secret := [32]byte{0x1}
swapCreatorAddr := deployContracts(t, ec, ethKey)
swapCreatorAddr, _ := contracts.DevDeploySwapCreator(t, ec, ethKey)
// success path
swap := createTestSwap(claimer)

View File

@@ -51,9 +51,7 @@ func Test_ValidateAndSendTransaction(t *testing.T) {
claimerAddr := crypto.PubkeyToAddress(*pub)
t.Log("claimerAddr: ", claimerAddr)
swapCreatorAddr := deployContracts(t, ec.Raw(), sk)
swapCreator, err := contracts.NewSwapCreator(swapCreatorAddr, ec.Raw())
require.NoError(t, err)
swapCreatorAddr, swapCreator := contracts.DevDeploySwapCreator(t, ec.Raw(), sk)
testT1Timeout := big.NewInt(300) // 5 minutes
testT2Timeout := testT1Timeout

View File

@@ -25,7 +25,7 @@ func TestValidateRelayerFee(t *testing.T) {
ctx := context.Background()
ec, _ := tests.NewEthClient(t)
key := tests.GetTakerTestKey(t)
swapCreatorAddr := deployContracts(t, ec, key)
swapCreatorAddr, _ := contracts.DevDeploySwapCreator(t, ec, key)
// 20-byte empty address, 4-byte zero salt
empty := [24]byte{}
@@ -112,7 +112,7 @@ func Test_validateClaimValues_takerClaim_contractAddressNotEqualFail(t *testing.
func Test_validateClaimValues_dhtClaim_contractAddressNotEqual(t *testing.T) {
ec, _ := tests.NewEthClient(t)
key := tests.GetTakerTestKey(t)
swapCreatorAddr := deployContracts(t, ec, key)
swapCreatorAddr, _ := contracts.DevDeploySwapCreator(t, ec, key)
request := &message.RelayClaimRequest{
OfferID: nil, // DHT relayer claim
@@ -131,7 +131,7 @@ func Test_validateSignature(t *testing.T) {
claimer := crypto.PubkeyToAddress(*ethKey.Public().(*ecdsa.PublicKey))
ec, _ := tests.NewEthClient(t)
secret := [32]byte{0x1}
swapCreatorAddr := deployContracts(t, ec, ethKey)
swapCreatorAddr, _ := contracts.DevDeploySwapCreator(t, ec, ethKey)
swap := createTestSwap(claimer)
relaySwap := &contracts.SwapCreatorRelaySwap{
@@ -161,7 +161,7 @@ func Test_validateClaimRequest(t *testing.T) {
claimer := crypto.PubkeyToAddress(*ethKey.Public().(*ecdsa.PublicKey))
ec, _ := tests.NewEthClient(t)
secret := [32]byte{0x1}
swapCreatorAddr := deployContracts(t, ec, ethKey)
swapCreatorAddr, _ := contracts.DevDeploySwapCreator(t, ec, ethKey)
// 20-byte empty address, 4-byte zero salt
empty := [24]byte{}

View File

@@ -252,3 +252,16 @@ func MineTransaction(t *testing.T, ec bind.DeployBackend, tx *ethtypes.Transacti
require.Equal(t, ethtypes.ReceiptStatusSuccessful, receipt.Status) // Make sure the transaction was not reverted
return receipt
}
// TxOpts returns a fresh TransactOpts for use in tests
func TxOpts(t *testing.T, pk *ecdsa.PrivateKey) *bind.TransactOpts {
return TxOptsWithValue(t, pk, nil)
}
// TxOptsWithValue returns a fresh TransactOpts with a set value for use in tests
func TxOptsWithValue(t *testing.T, pk *ecdsa.PrivateKey, value *big.Int) *bind.TransactOpts {
txOpts, err := bind.NewKeyedTransactorWithChainID(pk, big.NewInt(common.GanacheChainID))
require.NoError(t, err)
txOpts.Value = value
return txOpts
}