mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-07 21:34:05 -05:00
483 lines
14 KiB
Go
483 lines
14 KiB
Go
// Copyright 2023 The AthanorLabs/atomic-swap Authors
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
package xmrtaker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"math/big"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/cockroachdb/apd/v3"
|
|
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
|
ethcommon "github.com/ethereum/go-ethereum/common"
|
|
"github.com/golang/mock/gomock"
|
|
logging "github.com/ipfs/go-log"
|
|
"github.com/libp2p/go-libp2p/core/peer"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/athanorlabs/atomic-swap/coins"
|
|
"github.com/athanorlabs/atomic-swap/common"
|
|
"github.com/athanorlabs/atomic-swap/common/types"
|
|
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
|
|
contracts "github.com/athanorlabs/atomic-swap/ethereum"
|
|
"github.com/athanorlabs/atomic-swap/ethereum/extethclient"
|
|
"github.com/athanorlabs/atomic-swap/monero"
|
|
"github.com/athanorlabs/atomic-swap/net/message"
|
|
pcommon "github.com/athanorlabs/atomic-swap/protocol"
|
|
"github.com/athanorlabs/atomic-swap/protocol/backend"
|
|
pswap "github.com/athanorlabs/atomic-swap/protocol/swap"
|
|
"github.com/athanorlabs/atomic-swap/tests"
|
|
)
|
|
|
|
var (
|
|
testPeerID, _ = peer.Decode("12D3KooWQQRJuKTZ35eiHGNPGDpQqjpJSdaxEMJRxi6NWFrrvQVi")
|
|
)
|
|
|
|
func init() {
|
|
logging.SetLogLevel("xmrtaker", "debug")
|
|
logging.SetLogLevel("protocol", "debug")
|
|
logging.SetLogLevel("txsender", "debug")
|
|
}
|
|
|
|
type mockNet struct {
|
|
msgMu sync.Mutex // lock needed, as SendSwapMessage is called async from timeout handlers
|
|
msg common.Message // last value passed to SendSwapMessage
|
|
}
|
|
|
|
func (n *mockNet) LastSentMessage() common.Message {
|
|
n.msgMu.Lock()
|
|
defer n.msgMu.Unlock()
|
|
return n.msg
|
|
}
|
|
|
|
func (n *mockNet) SendSwapMessage(msg common.Message, _ types.Hash) error {
|
|
n.msgMu.Lock()
|
|
defer n.msgMu.Unlock()
|
|
n.msg = msg
|
|
return nil
|
|
}
|
|
|
|
func (n *mockNet) DiscoverRelayers() ([]peer.ID, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *mockNet) SubmitClaimToRelayer(_ peer.ID, _ *message.RelayClaimRequest) (*message.RelayClaimResponse, error) {
|
|
return new(message.RelayClaimResponse), nil
|
|
}
|
|
|
|
func (n *mockNet) CloseProtocolStream(_ types.Hash) {}
|
|
|
|
func newSwapManager(t *testing.T) pswap.Manager {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
db := pswap.NewMockDatabase(ctrl)
|
|
db.EXPECT().GetAllSwaps()
|
|
db.EXPECT().PutSwap(gomock.Any()).AnyTimes()
|
|
|
|
sm, err := pswap.NewManager(db)
|
|
require.NoError(t, err)
|
|
return sm
|
|
}
|
|
|
|
func newBackendAndNet(t *testing.T) (backend.Backend, *mockNet) {
|
|
pk := tests.GetTakerTestKey(t)
|
|
ec := extethclient.CreateTestClient(t, pk)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(func() {
|
|
cancel()
|
|
})
|
|
|
|
txOpts, err := bind.NewKeyedTransactorWithChainID(pk, ec.ChainID())
|
|
require.NoError(t, err)
|
|
|
|
var forwarderAddr ethcommon.Address
|
|
_, tx, _, err := contracts.DeploySwapCreator(txOpts, ec.Raw(), forwarderAddr)
|
|
require.NoError(t, err)
|
|
|
|
addr, err := bind.WaitDeployed(ctx, ec.Raw(), tx)
|
|
require.NoError(t, err)
|
|
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
rdb := backend.NewMockRecoveryDB(ctrl)
|
|
rdb.EXPECT().PutContractSwapInfo(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
|
rdb.EXPECT().PutSwapPrivateKey(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
|
rdb.EXPECT().PutCounterpartySwapPrivateKey(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
|
rdb.EXPECT().PutCounterpartySwapKeys(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
|
rdb.EXPECT().DeleteSwap(gomock.Any()).Return(nil).AnyTimes()
|
|
|
|
net := new(mockNet)
|
|
bcfg := &backend.Config{
|
|
Ctx: ctx,
|
|
MoneroClient: monero.CreateWalletClient(t),
|
|
EthereumClient: ec,
|
|
Environment: common.Development,
|
|
SwapManager: newSwapManager(t),
|
|
SwapCreatorAddr: addr,
|
|
Net: net,
|
|
RecoveryDB: rdb,
|
|
}
|
|
|
|
b, err := backend.NewBackend(bcfg)
|
|
require.NoError(t, err)
|
|
return b, net
|
|
}
|
|
|
|
func newBackend(t *testing.T) backend.Backend {
|
|
b, _ := newBackendAndNet(t)
|
|
return b
|
|
}
|
|
|
|
func newTestSwapStateAndNet(t *testing.T) (*swapState, *mockNet) {
|
|
b, net := newBackendAndNet(t)
|
|
providedAmt := coins.EtherToWei(coins.StrToDecimal("1"))
|
|
exchangeRate := coins.ToExchangeRate(coins.StrToDecimal("1.0")) // 100%
|
|
swapState, err := newSwapStateFromStart(b, testPeerID, types.Hash{}, true,
|
|
providedAmt, exchangeRate, types.EthAssetETH)
|
|
require.NoError(t, err)
|
|
return swapState, net
|
|
}
|
|
|
|
func newTestSwapState(t *testing.T) *swapState {
|
|
s, _ := newTestSwapStateAndNet(t)
|
|
return s
|
|
}
|
|
|
|
func newTestSwapStateWithERC20(t *testing.T, providesAmt *apd.Decimal) (*swapState, *contracts.TestERC20) {
|
|
b := newBackend(t)
|
|
const numDecimals = 13
|
|
|
|
// Increase the provided amount to token units, by adding to the exponent
|
|
providesAmtTokenUnits := apd.NewWithBigInt(&providesAmt.Coeff, providesAmt.Exponent+numDecimals)
|
|
require.Positive(t, providesAmtTokenUnits.Exponent)
|
|
|
|
// Set the exponent to zero pushing everything into the coefficient
|
|
_, err := coins.DecimalCtx().Quantize(providesAmtTokenUnits, providesAmtTokenUnits, 0)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Now that everything is in the coefficient, we can convert to big.Int
|
|
providesAmtTokenUnitsBI := new(big.Int).SetBytes(providesAmtTokenUnits.Coeff.Bytes())
|
|
|
|
txOpts, err := b.ETHClient().TxOpts(b.Ctx())
|
|
require.NoError(t, err)
|
|
|
|
_, tx, contract, err := contracts.DeployTestERC20(
|
|
txOpts,
|
|
b.ETHClient().Raw(),
|
|
"☢☣☠\a Obnoxious Token \a☠☣☢",
|
|
"\a☢\n☣\a☠", // ensure we escape this
|
|
13,
|
|
b.ETHClient().Address(),
|
|
providesAmtTokenUnitsBI,
|
|
)
|
|
require.NoError(t, err)
|
|
addr, err := bind.WaitDeployed(b.Ctx(), b.ETHClient().Raw(), tx)
|
|
require.NoError(t, err)
|
|
|
|
tokenInfo, err := b.ETHClient().ERC20Info(b.Ctx(), addr)
|
|
require.NoError(t, err)
|
|
|
|
providesEthAssetAmt := coins.NewERC20TokenAmountFromDecimals(providesAmt, tokenInfo)
|
|
|
|
exchangeRate := coins.ToExchangeRate(apd.New(1, 0)) // 100%
|
|
swapState, err := newSwapStateFromStart(b, testPeerID, types.Hash{}, false,
|
|
providesEthAssetAmt, exchangeRate, types.EthAsset(addr))
|
|
require.NoError(t, err)
|
|
return swapState, contract
|
|
}
|
|
|
|
func newTestXMRMakerSendKeysMessage(t *testing.T) (*message.SendKeysMessage, *pcommon.KeysAndProof) {
|
|
keysAndProof, err := pcommon.GenerateKeysAndProof()
|
|
require.NoError(t, err)
|
|
|
|
msg := &message.SendKeysMessage{
|
|
PublicSpendKey: keysAndProof.PublicKeyPair.SpendKey(),
|
|
PrivateViewKey: keysAndProof.PrivateKeyPair.ViewKey(),
|
|
DLEqProof: keysAndProof.DLEqProof.Proof(),
|
|
Secp256k1PublicKey: keysAndProof.Secp256k1PublicKey,
|
|
EthAddress: ethcommon.Address{0x1},
|
|
ProvidedAmount: apd.New(1, 0),
|
|
}
|
|
|
|
return msg, keysAndProof
|
|
}
|
|
|
|
func TestSwapState_HandleProtocolMessage_SendKeysMessage(t *testing.T) {
|
|
s, net := newTestSwapStateAndNet(t)
|
|
defer s.cancel()
|
|
|
|
msg := &message.SendKeysMessage{}
|
|
err := s.HandleProtocolMessage(msg)
|
|
require.True(t, errors.Is(err, errMissingProvidedAmount))
|
|
|
|
msg, xmrmakerKeysAndProof := newTestXMRMakerSendKeysMessage(t)
|
|
|
|
err = s.HandleProtocolMessage(msg)
|
|
require.NoError(t, err)
|
|
|
|
resp := net.LastSentMessage()
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, s.SwapTimeout(), s.t1.Sub(s.t0))
|
|
require.Equal(t, xmrmakerKeysAndProof.PublicKeyPair.SpendKey().String(), s.xmrmakerPublicSpendKey.String())
|
|
require.Equal(t, xmrmakerKeysAndProof.PrivateKeyPair.ViewKey().String(), s.xmrmakerPrivateViewKey.String())
|
|
}
|
|
|
|
// test the case where XMRTaker deploys and locks her eth, but XMRMaker never locks his monero.
|
|
// XMRTaker should call refund before the timeout t0.
|
|
func TestSwapState_HandleProtocolMessage_SendKeysMessage_Refund(t *testing.T) {
|
|
s, net := newTestSwapStateAndNet(t)
|
|
defer s.cancel()
|
|
s.SetSwapTimeout(time.Second * 15)
|
|
|
|
msg, xmrmakerKeysAndProof := newTestXMRMakerSendKeysMessage(t)
|
|
|
|
err := s.HandleProtocolMessage(msg)
|
|
require.NoError(t, err)
|
|
|
|
resp := net.LastSentMessage()
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, message.NotifyETHLockedType, resp.Type())
|
|
require.Equal(t, s.SwapTimeout(), s.t1.Sub(s.t0))
|
|
require.Equal(t, xmrmakerKeysAndProof.PublicKeyPair.SpendKey().String(), s.xmrmakerPublicSpendKey.String())
|
|
require.Equal(t, xmrmakerKeysAndProof.PrivateKeyPair.ViewKey().String(), s.xmrmakerPrivateViewKey.String())
|
|
|
|
// ensure we refund before t0
|
|
for status := range s.info.StatusCh() {
|
|
if status == types.CompletedRefund {
|
|
// check this is before t0
|
|
// TODO: remove the 10-second buffer, this is needed for now
|
|
// because the exact refund time isn't stored, and the time
|
|
// between the refund happening and this line being called
|
|
// causes it to fail
|
|
require.Greater(t, s.t0.Add(time.Second*10), time.Now())
|
|
break
|
|
} else if !status.IsOngoing() {
|
|
t.Fatalf("got wrong exit status %s, expected CompletedRefund", status)
|
|
}
|
|
}
|
|
|
|
// check swap is marked completed
|
|
stage, err := s.SwapCreator().Swaps(nil, s.contractSwapID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, contracts.StageCompleted, stage)
|
|
}
|
|
|
|
func lockXMRFunds(
|
|
t *testing.T,
|
|
ctx context.Context, //nolint:revive
|
|
wc monero.WalletClient,
|
|
destAddr *mcrypto.Address,
|
|
amount *coins.PiconeroAmount,
|
|
) {
|
|
monero.MineMinXMRBalance(t, wc, amount)
|
|
_, err := wc.Transfer(ctx, destAddr, 0, amount, monero.MinSpendConfirmations)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestSwapState_NotifyXMRLock(t *testing.T) {
|
|
s := newTestSwapState(t)
|
|
defer s.cancel()
|
|
s.nextExpectedEvent = EventXMRLockedType
|
|
|
|
xmrmakerKeysAndProof, err := generateKeys()
|
|
require.NoError(t, err)
|
|
|
|
err = s.setXMRMakerKeys(
|
|
xmrmakerKeysAndProof.PublicKeyPair.SpendKey(),
|
|
xmrmakerKeysAndProof.PrivateKeyPair.ViewKey(),
|
|
xmrmakerKeysAndProof.Secp256k1PublicKey,
|
|
)
|
|
require.NoError(t, err)
|
|
s.xmrmakerAddress = fakeAddress
|
|
|
|
_, err = s.lockAsset()
|
|
require.NoError(t, err)
|
|
|
|
kp := mcrypto.SumSpendAndViewKeys(xmrmakerKeysAndProof.PublicKeyPair, s.pubkeys)
|
|
xmrAddr := kp.Address(common.Development)
|
|
|
|
lockXMRFunds(t, s.ctx, s.XMRClient(), xmrAddr, s.expectedPiconeroAmount())
|
|
event := newEventXMRLocked()
|
|
s.eventCh <- event
|
|
err = <-event.errCh
|
|
require.NoError(t, err)
|
|
require.Equal(t, EventETHClaimedType, s.nextExpectedEvent)
|
|
}
|
|
|
|
// test the case where the monero is locked, but XMRMaker never claims.
|
|
// XMRTaker should call refund after the timeout t1.
|
|
func TestSwapState_NotifyXMRLock_Refund(t *testing.T) {
|
|
s := newTestSwapState(t)
|
|
defer s.cancel()
|
|
s.nextExpectedEvent = EventXMRLockedType
|
|
s.SetSwapTimeout(time.Second * 3)
|
|
|
|
xmrmakerKeysAndProof, err := generateKeys()
|
|
require.NoError(t, err)
|
|
|
|
err = s.setXMRMakerKeys(
|
|
xmrmakerKeysAndProof.PublicKeyPair.SpendKey(),
|
|
xmrmakerKeysAndProof.PrivateKeyPair.ViewKey(),
|
|
xmrmakerKeysAndProof.Secp256k1PublicKey,
|
|
)
|
|
require.NoError(t, err)
|
|
s.xmrmakerAddress = fakeAddress
|
|
|
|
_, err = s.lockAsset()
|
|
require.NoError(t, err)
|
|
|
|
kp := mcrypto.SumSpendAndViewKeys(xmrmakerKeysAndProof.PublicKeyPair, s.pubkeys)
|
|
xmrAddr := kp.Address(common.Development)
|
|
|
|
lockXMRFunds(t, s.ctx, s.XMRClient(), xmrAddr, s.expectedPiconeroAmount())
|
|
event := newEventXMRLocked()
|
|
s.eventCh <- event
|
|
err = <-event.errCh
|
|
require.NoError(t, err)
|
|
require.Equal(t, EventETHClaimedType, s.nextExpectedEvent)
|
|
|
|
for status := range s.info.StatusCh() {
|
|
if status == types.CompletedRefund {
|
|
// check this is after t1
|
|
require.Less(t, s.t1, time.Now())
|
|
break
|
|
} else if !status.IsOngoing() {
|
|
t.Fatalf("got wrong exit status %s, expected CompletedRefund", status)
|
|
}
|
|
}
|
|
|
|
// check balance of contract is 0
|
|
balance, err := s.ETHClient().Raw().BalanceAt(context.Background(), s.SwapCreatorAddr(), nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint64(0), balance.Uint64())
|
|
}
|
|
|
|
func TestExit_afterSendKeysMessage(t *testing.T) {
|
|
s := newTestSwapState(t)
|
|
defer s.cancel()
|
|
s.nextExpectedEvent = EventKeysReceivedType
|
|
err := s.Exit()
|
|
require.NoError(t, err)
|
|
info, err := s.SwapManager().GetPastSwap(s.info.OfferID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.CompletedAbort, info.Status)
|
|
}
|
|
|
|
func TestExit_afterNotifyXMRLock(t *testing.T) {
|
|
s := newTestSwapState(t)
|
|
defer s.cancel()
|
|
s.nextExpectedEvent = EventXMRLockedType
|
|
|
|
xmrmakerKeysAndProof, err := generateKeys()
|
|
require.NoError(t, err)
|
|
|
|
err = s.setXMRMakerKeys(
|
|
xmrmakerKeysAndProof.PublicKeyPair.SpendKey(),
|
|
xmrmakerKeysAndProof.PrivateKeyPair.ViewKey(),
|
|
xmrmakerKeysAndProof.Secp256k1PublicKey,
|
|
)
|
|
require.NoError(t, err)
|
|
s.xmrmakerAddress = fakeAddress
|
|
|
|
_, err = s.lockAsset()
|
|
require.NoError(t, err)
|
|
|
|
err = s.Exit()
|
|
require.NoError(t, err)
|
|
|
|
info, err := s.SwapManager().GetPastSwap(s.info.OfferID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.CompletedRefund, info.Status)
|
|
}
|
|
|
|
func TestExit_afterNotifyClaimed(t *testing.T) {
|
|
s := newTestSwapState(t)
|
|
defer s.cancel()
|
|
s.nextExpectedEvent = EventETHClaimedType
|
|
|
|
xmrmakerKeysAndProof, err := generateKeys()
|
|
require.NoError(t, err)
|
|
|
|
err = s.setXMRMakerKeys(
|
|
xmrmakerKeysAndProof.PublicKeyPair.SpendKey(),
|
|
xmrmakerKeysAndProof.PrivateKeyPair.ViewKey(),
|
|
xmrmakerKeysAndProof.Secp256k1PublicKey,
|
|
)
|
|
require.NoError(t, err)
|
|
s.xmrmakerAddress = fakeAddress
|
|
|
|
_, err = s.lockAsset()
|
|
require.NoError(t, err)
|
|
|
|
err = s.Exit()
|
|
require.NoError(t, err)
|
|
|
|
info, err := s.SwapManager().GetPastSwap(s.info.OfferID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.CompletedRefund, info.Status)
|
|
}
|
|
|
|
func TestExit_invalidNextMessageType(t *testing.T) {
|
|
// this case shouldn't ever really happen
|
|
s := newTestSwapState(t)
|
|
defer s.cancel()
|
|
s.nextExpectedEvent = EventExitType
|
|
|
|
xmrmakerKeysAndProof, err := generateKeys()
|
|
require.NoError(t, err)
|
|
|
|
err = s.setXMRMakerKeys(
|
|
xmrmakerKeysAndProof.PublicKeyPair.SpendKey(),
|
|
xmrmakerKeysAndProof.PrivateKeyPair.ViewKey(),
|
|
xmrmakerKeysAndProof.Secp256k1PublicKey,
|
|
)
|
|
require.NoError(t, err)
|
|
s.xmrmakerAddress = fakeAddress
|
|
|
|
_, err = s.lockAsset()
|
|
require.NoError(t, err)
|
|
|
|
err = s.Exit()
|
|
require.True(t, errors.Is(err, errUnexpectedEventType))
|
|
|
|
info, err := s.SwapManager().GetPastSwap(s.info.OfferID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.CompletedAbort, info.Status)
|
|
}
|
|
|
|
func TestSwapState_ApproveToken(t *testing.T) {
|
|
const expectedAmtStr = "5678"
|
|
providesAmt := coins.StrToDecimal(expectedAmtStr)
|
|
|
|
s, contract := newTestSwapStateWithERC20(t, providesAmt)
|
|
|
|
xmrmakerKeysAndProof, err := generateKeys()
|
|
require.NoError(t, err)
|
|
|
|
err = s.setXMRMakerKeys(
|
|
xmrmakerKeysAndProof.PublicKeyPair.SpendKey(),
|
|
xmrmakerKeysAndProof.PrivateKeyPair.ViewKey(),
|
|
xmrmakerKeysAndProof.Secp256k1PublicKey,
|
|
)
|
|
require.NoError(t, err)
|
|
s.xmrmakerAddress = fakeAddress
|
|
|
|
// approve is called by NewSwap() in lockAsset()
|
|
_, err = s.lockAsset()
|
|
require.NoError(t, err)
|
|
|
|
// Now that the tokens are locked in the contract, validate that
|
|
// the contract is no longer approved to transfer additional tokens
|
|
// from us.
|
|
callOpts := &bind.CallOpts{Context: s.ctx}
|
|
allowance, err := contract.Allowance(callOpts, s.ETHClient().Address(), s.SwapCreatorAddr())
|
|
require.NoError(t, err)
|
|
require.Equal(t, "0", allowance.String())
|
|
}
|