remove NotifyXMRLocked message, have xmrtaker check for lock independently (#341)

This commit is contained in:
noot
2023-03-11 16:13:47 -05:00
committed by GitHub
parent 98f4e91ba1
commit 848051b852
14 changed files with 104 additions and 203 deletions

View File

@@ -22,7 +22,6 @@ const (
QueryResponseType byte = iota
SendKeysType
NotifyETHLockedType
NotifyXMRLockType
)
// TypeToString converts a message type into a string.
@@ -34,8 +33,6 @@ func TypeToString(t byte) string {
return "SendKeysMessage"
case NotifyETHLockedType:
return "NotifyETHLocked"
case NotifyXMRLockType:
return "NotifyXMRLock"
default:
return "unknown"
}
@@ -59,8 +56,6 @@ func DecodeMessage(b []byte) (common.Message, error) {
msg = &SendKeysMessage{}
case NotifyETHLockedType:
msg = &NotifyETHLocked{}
case NotifyXMRLockType:
msg = &NotifyXMRLock{}
default:
return nil, fmt.Errorf("invalid message type=%d", msgType)
}
@@ -173,29 +168,3 @@ func (m *NotifyETHLocked) Encode() ([]byte, error) {
func (m *NotifyETHLocked) Type() byte {
return NotifyETHLockedType
}
// NotifyXMRLock is sent by XMRMaker to XMRTaker after locking his XMR.
type NotifyXMRLock struct {
Address *mcrypto.Address `json:"address" validate:"required"` // address the monero was sent to
TxID types.Hash `json:"txID" validate:"required"` // Monero transaction ID (transaction hash in hex)
}
// String ...
func (m *NotifyXMRLock) String() string {
return "NotifyXMRLock"
}
// Encode ...
func (m *NotifyXMRLock) Encode() ([]byte, error) {
b, err := vjson.MarshalStruct(m)
if err != nil {
return nil, err
}
return append([]byte{NotifyXMRLockType}, b...), nil
}
// Type ...
func (m *NotifyXMRLock) Type() byte {
return NotifyXMRLockType
}

View File

@@ -190,7 +190,7 @@ func (s *swapState) handleEvent(event Event) {
return
}
err := s.handleEventETHLocked(e)
err := s.handleNotifyETHLocked(e.message)
if err != nil {
e.errCh <- fmt.Errorf("failed to handle EventETHLocked: %w", err)
if !s.fundsLocked {
@@ -250,15 +250,6 @@ func (s *swapState) handleEvent(event Event) {
}
}
func (s *swapState) handleEventETHLocked(e *EventETHLocked) error {
resp, err := s.handleNotifyETHLocked(e.message)
if err != nil {
return err
}
return s.SendSwapMessage(resp, s.ID())
}
func (s *swapState) handleEventContractReady() error {
log.Debug("contract ready, attempting to claim funds...")
close(s.readyCh)

View File

@@ -58,7 +58,7 @@ func TestSwapState_handleEvent_EventETHRefunded(t *testing.T) {
newSwap(t, s, [32]byte{}, refundKey, desiredAmount.BigInt(), duration)
// lock XMR
_, err = s.lockFunds(coins.MoneroToPiconero(s.info.ProvidedAmount))
err = s.lockFunds(coins.MoneroToPiconero(s.info.ProvidedAmount))
require.NoError(t, err)
// call refund w/ XMRTaker's secret

View File

@@ -146,11 +146,6 @@ func newTestInstanceAndDB(t *testing.T) (*Instance, *offers.MockDatabase) {
return inst, db
}
func newTestInstanceAndNet(t *testing.T) (*Instance, *mockNet) {
inst, _, net := newTestInstanceAndDBAndNet(t)
return inst, net
}
func TestInstance_createOngoingSwap(t *testing.T) {
inst, offerDB := newTestInstanceAndDB(t)
rdb := inst.backend.RecoveryDB().(*backend.MockRecoveryDB)

View File

@@ -86,20 +86,20 @@ func (s *swapState) setNextExpectedEvent(event EventType) error {
return nil
}
func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) (common.Message, error) {
func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) error {
if msg.Address == (ethcommon.Address{}) {
return nil, errMissingAddress
return errMissingAddress
}
if types.IsHashZero(msg.ContractSwapID) {
return nil, errNilContractSwapID
return errNilContractSwapID
}
log.Infof("got NotifyETHLocked; address=%s contract swap ID=%s", msg.Address, msg.ContractSwapID)
// validate that swap ID == keccak256(swap struct)
if err := checkContractSwapID(msg); err != nil {
return nil, err
return err
}
s.contractSwapID = msg.ContractSwapID
@@ -107,7 +107,7 @@ func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) (common.
receipt, err := s.Backend.ETHClient().Raw().TransactionReceipt(s.ctx, msg.TxHash)
if err != nil {
return nil, err
return err
}
contractAddr := msg.Address
@@ -116,11 +116,11 @@ func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) (common.
// doesn't hurt though I suppose.
_, err = contracts.CheckSwapFactoryContractCode(s.ctx, s.Backend.ETHClient().Raw(), contractAddr)
if err != nil {
return nil, err
return err
}
if err = s.setContract(contractAddr); err != nil {
return nil, fmt.Errorf("failed to instantiate contract instance: %w", err)
return fmt.Errorf("failed to instantiate contract instance: %w", err)
}
ethInfo := &db.EthereumSwapInfo{
@@ -131,25 +131,25 @@ func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) (common.
}
if err = s.Backend.RecoveryDB().PutContractSwapInfo(s.ID(), ethInfo); err != nil {
return nil, err
return err
}
if err = s.checkContract(msg.TxHash); err != nil {
return nil, err
return err
}
err = s.checkAndSetTimeouts(msg.ContractSwap.Timeout0, msg.ContractSwap.Timeout1)
if err != nil {
return nil, err
return err
}
notifyXMRLocked, err := s.lockFunds(coins.MoneroToPiconero(s.info.ProvidedAmount))
err = s.lockFunds(coins.MoneroToPiconero(s.info.ProvidedAmount))
if err != nil {
return nil, fmt.Errorf("failed to lock funds: %w", err)
return fmt.Errorf("failed to lock funds: %w", err)
}
go s.runT0ExpirationHandler()
return notifyXMRLocked, nil
return nil
}
func (s *swapState) runT0ExpirationHandler() {

View File

@@ -510,14 +510,14 @@ func (s *swapState) setContract(address ethcommon.Address) error {
// lockFunds locks XMRMaker's funds in the monero account specified by public key
// (S_a + S_b), viewable with (V_a + V_b)
// It accepts the amount to lock as the input
func (s *swapState) lockFunds(amount *coins.PiconeroAmount) (*message.NotifyXMRLock, error) {
func (s *swapState) lockFunds(amount *coins.PiconeroAmount) error {
xmrtakerPublicKeys := mcrypto.NewPublicKeyPair(s.xmrtakerPublicSpendKey, s.xmrtakerPrivateViewKey.Public())
swapDestAddr := mcrypto.SumSpendAndViewKeys(xmrtakerPublicKeys, s.pubkeys).Address(s.Env())
log.Infof("going to lock XMR funds, amount=%s XMR", amount.AsMoneroString())
balance, err := s.XMRClient().GetBalance(0)
if err != nil {
return nil, err
return err
}
log.Debug("total XMR balance: ", coins.FmtPiconeroAmtAsXMR(balance.Balance))
@@ -526,19 +526,11 @@ func (s *swapState) lockFunds(amount *coins.PiconeroAmount) (*message.NotifyXMRL
log.Infof("Starting lock of %s XMR in address %s", amount.AsMoneroString(), swapDestAddr)
transfer, err := s.XMRClient().Transfer(s.ctx, swapDestAddr, 0, amount, monero.MinSpendConfirmations)
if err != nil {
return nil, err
return err
}
log.Infof("Successfully locked XMR funds: txID=%s address=%s block=%d",
transfer.TxID, swapDestAddr, transfer.Height)
s.fundsLocked = true
txID, err := types.HexToHash(transfer.TxID)
if err != nil {
return nil, err
}
return &message.NotifyXMRLock{
Address: swapDestAddr,
TxID: txID,
}, nil
return nil
}

View File

@@ -86,7 +86,7 @@ func TestSwapStateOngoing_Refund(t *testing.T) {
newSwap(t, s, [32]byte{}, refundKey, desiredAmount.BigInt(), duration)
// lock XMR
_, err = s.lockFunds(coins.MoneroToPiconero(s.info.ProvidedAmount))
err = s.lockFunds(coins.MoneroToPiconero(s.info.ProvidedAmount))
require.NoError(t, err)
s.cancel()

View File

@@ -45,27 +45,6 @@ func newTestSwapStateAndDB(t *testing.T) (*Instance, *swapState, *offers.MockDat
return xmrmaker, swapState, db
}
func newTestSwapStateAndNet(t *testing.T) (*Instance, *swapState, *mockNet) {
xmrmaker, net := newTestInstanceAndNet(t)
swapState, err := newSwapStateFromStart(
xmrmaker.backend,
types.NewOffer(
coins.ProvidesXMR,
coins.StrToDecimal("0.1"),
coins.StrToDecimal("1"),
coins.StrToExchangeRate("0.1"),
types.EthAssetETH,
),
&types.OfferExtra{},
xmrmaker.offerManager,
coins.MoneroToPiconero(coins.StrToDecimal("0.1")),
desiredAmount,
)
require.NoError(t, err)
return xmrmaker, swapState, net
}
func newTestSwapState(t *testing.T) (*Instance, *swapState) {
xmrmaker, swapState, _ := newTestSwapStateAndDB(t)
return xmrmaker, swapState
@@ -177,7 +156,7 @@ func TestSwapState_handleSendKeysMessage(t *testing.T) {
}
func TestSwapState_HandleProtocolMessage_NotifyETHLocked_ok(t *testing.T) {
_, s, net := newTestSwapStateAndNet(t)
_, s := newTestSwapState(t)
defer s.cancel()
s.nextExpectedEvent = EventETHLockedType
@@ -208,12 +187,6 @@ func TestSwapState_HandleProtocolMessage_NotifyETHLocked_ok(t *testing.T) {
err = s.HandleProtocolMessage(msg)
require.NoError(t, err)
resp := net.LastSentMessage()
require.NotNil(t, resp)
require.Equal(t, message.NotifyXMRLockType, resp.Type())
require.Equal(t, duration, s.t1.Sub(s.t0))
require.Equal(t, EventContractReadyType, s.nextExpectedEvent)
require.True(t, s.info.Status.IsOngoing())
}
func TestSwapState_HandleProtocolMessage_NotifyETHLocked_timeout(t *testing.T) {
@@ -279,7 +252,7 @@ func TestSwapState_handleRefund(t *testing.T) {
newSwap(t, s, [32]byte{}, refundKey, desiredAmount.BigInt(), duration)
// lock XMR
_, err = s.lockFunds(coins.MoneroToPiconero(s.info.ProvidedAmount))
err = s.lockFunds(coins.MoneroToPiconero(s.info.ProvidedAmount))
require.NoError(t, err)
// call refund w/ XMRTaker's spend key
@@ -327,7 +300,7 @@ func TestSwapState_Exit_Reclaim(t *testing.T) {
newSwap(t, s, [32]byte{}, refundKey, desiredAmount.BigInt(), duration)
// lock XMR
_, err = s.lockFunds(coins.MoneroToPiconero(s.info.ProvidedAmount))
err = s.lockFunds(coins.MoneroToPiconero(s.info.ProvidedAmount))
require.NoError(t, err)
balAfterLock, err := s.XMRClient().GetBalance(0)

View File

@@ -18,7 +18,6 @@ var (
errCannotRefund = errors.New("swap is not at a stage where it can refund")
errRefundInvalid = errors.New("cannot refund, swap does not exist")
errRefundSwapCompleted = fmt.Errorf("cannot refund, %w", errSwapCompleted)
errNoLockedXMRAddress = errors.New("got empty address for locked XMR")
errCounterpartyKeysNotSet = errors.New("counterparty's keys aren't set")
errSwapInstantiationNoLogs = errors.New("expected 1 log, got 0")
errSwapCompleted = errors.New("swap is already completed")

View File

@@ -131,8 +131,7 @@ func newEventKeysReceived(msg *message.SendKeysMessage) *EventKeysReceived {
// EventXMRLocked is the second expected event. It represents XMR being locked
// on-chain.
type EventXMRLocked struct {
message *message.NotifyXMRLock
errCh chan error
errCh chan error
}
// Type ...
@@ -140,10 +139,9 @@ func (*EventXMRLocked) Type() EventType {
return EventXMRLockedType
}
func newEventXMRLocked(msg *message.NotifyXMRLock) *EventXMRLocked {
func newEventXMRLocked() *EventXMRLocked {
return &EventXMRLocked{
message: msg,
errCh: make(chan error),
errCh: make(chan error),
}
}
@@ -248,7 +246,7 @@ func (s *swapState) handleEvent(event Event) {
return
}
err := s.handleEventXMRLocked(e)
err := s.handleNotifyXMRLock()
if err != nil {
e.errCh <- fmt.Errorf("failed to handle %s: %w", e.Type(), err)
return
@@ -311,10 +309,6 @@ func (s *swapState) handleEventKeysReceived(event *EventKeysReceived) error {
return s.SendSwapMessage(resp, s.ID())
}
func (s *swapState) handleEventXMRLocked(event *EventXMRLocked) error {
return s.handleNotifyXMRLock(event.message)
}
func (s *swapState) handleEventETHClaimed(event *EventETHClaimed) error {
_, err := s.claimMonero(event.sk)
if err != nil {

View File

@@ -32,15 +32,6 @@ func lockXMRAndCheckForReadyLog(t *testing.T, s *swapState, xmrAddr *mcrypto.Add
t.Logf("Transferred %d pico XMR (fees %d) to account %s", transfer.Amount, transfer.Fee, xmrAddr)
t.Logf("Transfer was mined at block=%d with %d confirmations", transfer.Height, transfer.Confirmations)
txID, err := types.HexToHash(transfer.TxID)
require.NoError(t, err)
// send notification that monero was locked
lmsg := &message.NotifyXMRLock{
Address: xmrAddr,
TxID: txID,
}
// assert that ready() is called, setup contract watcher
ethHeader, err := backend.ETHClient().Raw().HeaderByNumber(backend.Ctx(), nil)
require.NoError(t, err)
@@ -58,17 +49,12 @@ func lockXMRAndCheckForReadyLog(t *testing.T, s *swapState, xmrAddr *mcrypto.Add
err = readyWatcher.Start()
require.NoError(t, err)
// now handle the NotifyXMRLock message
err = s.HandleProtocolMessage(lmsg)
require.NoError(t, err)
require.Equal(t, s.nextExpectedEvent, EventETHClaimedType)
require.Equal(t, types.ContractReady, s.info.Status)
// goroutine in SendKeysMessage handler should handle the NotifyXMRLock message
select {
case log := <-logReadyCh:
err = pcommon.CheckSwapID(&log, readyTopic, s.contractSwapID)
require.NoError(t, err)
case <-time.After(time.Second * 2):
case <-time.After(time.Second * 5):
t.Fatalf("didn't get ready logs in time")
}
}
@@ -106,6 +92,11 @@ func TestSwapState_handleEvent_EventETHClaimed(t *testing.T) {
kp := mcrypto.SumSpendAndViewKeys(s.pubkeys, s.pubkeys)
xmrAddr := kp.Address(common.Mainnet)
lockXMRAndCheckForReadyLog(t, s, xmrAddr)
// give handleNotifyXMRLock some time to return, since the event watcher
// sees the Ready event before swapState.ready() returns
time.Sleep(time.Second * 2)
require.Equal(t, EventETHClaimedType, s.nextExpectedEvent)
require.Equal(t, types.ContractReady, s.info.Status)
// simulate xmrmaker calling claim
// call swap.Swap.Claim() w/ b.privkeys.sk, revealing XMRMaker's secret spend key

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"time"
"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"
@@ -29,13 +28,6 @@ func (s *swapState) HandleProtocolMessage(msg common.Message) error {
if err != nil {
return err
}
case *message.NotifyXMRLock:
event := newEventXMRLocked(msg)
s.eventCh <- event
err := <-event.errCh
if err != nil {
return err
}
default:
return errUnexpectedMessageType
}
@@ -138,6 +130,9 @@ func (s *swapState) handleSendKeysMessage(msg *message.SendKeysMessage) (common.
// start goroutine to check that XMRMaker locks before t_0
go s.runT0ExpirationHandler()
// start goroutine to check for xmr being locked
go s.checkForXMRLock()
out := &message.NotifyETHLocked{
Address: s.ContractAddr(),
TxHash: txHash,
@@ -148,6 +143,57 @@ func (s *swapState) handleSendKeysMessage(msg *message.SendKeysMessage) (common.
return out, nil
}
func (s *swapState) checkForXMRLock() {
var checkForXMRLockInterval time.Duration
if s.Env() == common.Development {
checkForXMRLockInterval = time.Second
} else {
// monero block time is >1 minute, so this should be fine
checkForXMRLockInterval = time.Minute
}
// check that XMR was locked in expected account, and confirm amount
lockedAddr, vk := s.expectedXMRLockAccount()
conf := s.XMRClient().CreateWalletConf("xmrtaker-swap-wallet-verify-funds")
abViewCli, err := monero.CreateViewOnlyWalletFromKeys(conf, vk, lockedAddr, s.walletScanHeight)
if err != nil {
log.Errorf("failed to generate view-only wallet to verify locked XMR: %s", err)
return
}
defer abViewCli.CloseAndRemoveWallet()
log.Debugf("generated view-only wallet to check funds: %s", abViewCli.WalletName())
timer := time.NewTicker(checkForXMRLockInterval)
for {
select {
case <-s.ctx.Done():
return
case <-timer.C:
balance, err := abViewCli.GetBalance(0)
if err != nil {
log.Errorf("failed to get balance: %s", err)
continue
}
log.Debugf("checking locked wallet, address=%s balance=%d blocks-to-unlock=%d",
lockedAddr, balance.Balance, balance.BlocksToUnlock)
if s.expectedPiconeroAmount().CmpU64(balance.UnlockedBalance) <= 0 {
event := newEventXMRLocked()
s.eventCh <- event
err := <-event.errCh
if err != nil {
log.Errorf("eventXMRLocked errored: %s", err)
}
return
}
}
}
}
func (s *swapState) runT0ExpirationHandler() {
defer log.Debugf("returning from runT0ExpirationHandler")
@@ -189,51 +235,7 @@ func (s *swapState) expectedXMRLockAccount() (*mcrypto.Address, *mcrypto.Private
return mcrypto.NewPublicKeyPair(sk, vk.Public()).Address(s.Env()), vk
}
func (s *swapState) handleNotifyXMRLock(msg *message.NotifyXMRLock) error {
if msg.Address == nil {
return errNoLockedXMRAddress
}
// check that XMR was locked in expected account, and confirm amount
lockedAddr, vk := s.expectedXMRLockAccount()
if !msg.Address.Equal(lockedAddr) {
return fmt.Errorf("address received in message does not match expected address")
}
conf := s.XMRClient().CreateWalletConf("xmrtaker-swap-wallet-verify-funds")
abViewCli, err := monero.CreateViewOnlyWalletFromKeys(conf, vk, lockedAddr, s.walletScanHeight)
if err != nil {
return fmt.Errorf("failed to generate view-only wallet to verify locked XMR: %w", err)
}
defer abViewCli.CloseAndRemoveWallet()
log.Debugf("generated view-only wallet to check funds: %s", abViewCli.WalletName())
balance, err := abViewCli.GetBalance(0)
if err != nil {
return fmt.Errorf("failed to get balance: %w", err)
}
log.Debugf("checking locked wallet, address=%s balance=%d blocks-to-unlock=%d",
lockedAddr, balance.Balance, balance.BlocksToUnlock)
if s.expectedPiconeroAmount().CmpU64(balance.Balance) > 0 {
return fmt.Errorf("locked XMR amount is less than expected: got %s, expected %s",
coins.FmtPiconeroAmtAsXMR(balance.Balance), s.ExpectedAmount().Text('f'))
}
// Monero received from a transfer is locked for a minimum of 10 confirmations before
// it can be spent again. The maker is required to wait for 10 confirmations before
// notifying us that the XMR is locked and should not be adding additional wait
// requirements. We give one block of leniency, in case the taker's node is not fully
// synced. Our goal is to prevent double spends, issues due to block reorgs, and
// prevent the maker from locking our funds until close to the heat death of the
// universe (https://github.com/monero-project/research-lab/issues/78).
if balance.BlocksToUnlock > 1 {
return fmt.Errorf("received XMR funds are not unlocked as required (blocks-to-unlock=%d)",
balance.BlocksToUnlock)
}
func (s *swapState) handleNotifyXMRLock() error {
close(s.xmrLockedCh)
log.Info("XMR was locked successfully, setting contract to ready...")

View File

@@ -189,6 +189,10 @@ func newSwapStateFromOngoing(
s.contractSwap = ethSwapInfo.Swap
s.xmrmakerPublicSpendKey = makerSk
s.xmrmakerPrivateViewKey = makerVk
if info.Status == types.ETHLocked {
go s.checkForXMRLock()
}
return s, nil
}

View File

@@ -239,15 +239,10 @@ func lockXMRFunds(
wc monero.WalletClient,
destAddr *mcrypto.Address,
amount *coins.PiconeroAmount,
) types.Hash {
) {
monero.MineMinXMRBalance(t, wc, amount)
transfer, err := wc.Transfer(ctx, destAddr, 0, amount, monero.MinSpendConfirmations)
_, err := wc.Transfer(ctx, destAddr, 0, amount, monero.MinSpendConfirmations)
require.NoError(t, err)
txID, err := types.HexToHash(transfer.TxID)
require.NoError(t, err)
return txID
}
func TestSwapState_NotifyXMRLock(t *testing.T) {
@@ -271,12 +266,10 @@ func TestSwapState_NotifyXMRLock(t *testing.T) {
kp := mcrypto.SumSpendAndViewKeys(xmrmakerKeysAndProof.PublicKeyPair, s.pubkeys)
xmrAddr := kp.Address(common.Development)
msg := &message.NotifyXMRLock{
Address: xmrAddr,
TxID: lockXMRFunds(t, s.ctx, s.XMRClient(), xmrAddr, s.expectedPiconeroAmount()),
}
err = s.HandleProtocolMessage(msg)
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)
}
@@ -305,12 +298,10 @@ func TestSwapState_NotifyXMRLock_Refund(t *testing.T) {
kp := mcrypto.SumSpendAndViewKeys(xmrmakerKeysAndProof.PublicKeyPair, s.pubkeys)
xmrAddr := kp.Address(common.Development)
msg := &message.NotifyXMRLock{
Address: xmrAddr,
TxID: lockXMRFunds(t, s.ctx, s.XMRClient(), xmrAddr, s.expectedPiconeroAmount()),
}
err = s.HandleProtocolMessage(msg)
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)