mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-10 06:38:04 -05:00
247 lines
6.8 KiB
Go
247 lines
6.8 KiB
Go
// Copyright 2023 The AthanorLabs/atomic-swap Authors
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
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"
|
|
"github.com/athanorlabs/atomic-swap/common/types"
|
|
"github.com/athanorlabs/atomic-swap/db"
|
|
contracts "github.com/athanorlabs/atomic-swap/ethereum"
|
|
"github.com/athanorlabs/atomic-swap/net/message"
|
|
pcommon "github.com/athanorlabs/atomic-swap/protocol"
|
|
)
|
|
|
|
// HandleProtocolMessage is called by the network to handle an incoming message.
|
|
// If the message received is not the expected type for the point in the protocol we're at,
|
|
// this function will return an error.
|
|
func (s *swapState) HandleProtocolMessage(msg common.Message) error {
|
|
if s == nil {
|
|
return errNilSwapState
|
|
}
|
|
|
|
if s.ctx.Err() != nil {
|
|
return fmt.Errorf("protocol exited: %w", s.ctx.Err())
|
|
}
|
|
|
|
switch msg := msg.(type) {
|
|
case *message.NotifyETHLocked:
|
|
event := newEventETHLocked(msg)
|
|
s.eventCh <- event
|
|
err := <-event.errCh
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return errUnexpectedMessageType
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *swapState) clearNextExpectedEvent(status types.Status) {
|
|
s.nextExpectedEvent = EventNoneType
|
|
s.info.SetStatus(status)
|
|
}
|
|
|
|
func (s *swapState) setNextExpectedEvent(event EventType) error {
|
|
if s.nextExpectedEvent == EventNoneType {
|
|
// should have called clearNextExpectedEvent instead
|
|
panic("cannot set next expected event to EventNoneType")
|
|
}
|
|
|
|
if event == s.nextExpectedEvent {
|
|
panic("cannot set next expected event to same as current")
|
|
}
|
|
|
|
s.nextExpectedEvent = event
|
|
status := event.getStatus()
|
|
|
|
if status == types.UnknownStatus {
|
|
panic("status corresponding to event cannot be UnknownStatus")
|
|
}
|
|
|
|
s.info.SetStatus(status)
|
|
err := s.Backend.SwapManager().WriteSwapToDB(s.info)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if types.IsHashZero(msg.ContractSwapID) {
|
|
return errNilContractSwapID
|
|
}
|
|
|
|
log.Infof("got NotifyETHLocked; address=%s contract swap ID=%s", msg.Address, msg.ContractSwapID)
|
|
|
|
// validate that swap ID == keccak256(swap struct)
|
|
if msg.ContractSwap.SwapID() != msg.ContractSwapID {
|
|
return errSwapIDMismatch
|
|
}
|
|
|
|
s.contractSwapID = msg.ContractSwapID
|
|
s.contractSwap = msg.ContractSwap
|
|
|
|
receipt, err := waitForNewSwapReceipt(s.ctx, s.Backend.ETHClient().Raw(), msg.TxHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
contractAddr := msg.Address
|
|
err = contracts.CheckSwapCreatorContractCode(s.ctx, s.Backend.ETHClient().Raw(), contractAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = s.setContract(contractAddr); err != nil {
|
|
return fmt.Errorf("failed to instantiate contract instance: %w", err)
|
|
}
|
|
|
|
ethInfo := &db.EthereumSwapInfo{
|
|
StartNumber: receipt.BlockNumber,
|
|
SwapID: s.contractSwapID,
|
|
Swap: s.contractSwap,
|
|
SwapCreatorAddr: contractAddr,
|
|
}
|
|
|
|
if err = s.Backend.RecoveryDB().PutContractSwapInfo(s.OfferID(), ethInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Infof("stored ContractSwapInfo: id=%s", s.OfferID())
|
|
|
|
if err = s.checkContract(msg.TxHash); err != nil {
|
|
return err
|
|
}
|
|
|
|
err = s.checkAndSetTimeouts(msg.ContractSwap.Timeout1, msg.ContractSwap.Timeout2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = s.lockFunds(coins.MoneroToPiconero(s.info.ProvidedAmount))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to lock funds: %w", err)
|
|
}
|
|
|
|
go s.runT1ExpirationHandler()
|
|
return nil
|
|
}
|
|
|
|
func (s *swapState) runT1ExpirationHandler() {
|
|
log.Debugf("time until t1 (%s): %vs",
|
|
s.t1.Format(common.TimeFmtSecs),
|
|
time.Until(s.t1).Seconds(),
|
|
)
|
|
|
|
waitCtx, waitCtxCancel := context.WithCancel(context.Background())
|
|
defer waitCtxCancel() // Unblock WaitForTimestamp if still running when we exit
|
|
|
|
// note: this will cause unit tests to hang if not running ganache
|
|
// with --miner.blockTime!!!
|
|
waitCh := make(chan error)
|
|
go func() {
|
|
waitCh <- s.ETHClient().WaitForTimestamp(waitCtx, s.t1)
|
|
close(waitCh)
|
|
}()
|
|
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return
|
|
case <-s.readyCh:
|
|
log.Debugf("returning from runT1ExpirationHandler as contract was set to ready")
|
|
return
|
|
case err := <-waitCh:
|
|
if err != nil {
|
|
// TODO: Do we propagate this error? If we retry, the logic should probably be inside
|
|
// WaitForTimestamp. (#162)
|
|
log.Errorf("Failure waiting for T1 timeout: err=%s", err)
|
|
return
|
|
}
|
|
log.Debugf("reached t1, time to claim")
|
|
s.handleT1Expired()
|
|
}
|
|
}
|
|
|
|
func (s *swapState) handleT1Expired() {
|
|
event := newEventContractReady()
|
|
s.eventCh <- event
|
|
err := <-event.errCh
|
|
if err != nil {
|
|
// TODO: this is quite bad, how should this be handled? (#162)
|
|
log.Errorf("failed to handle t1 expiration: %s", err)
|
|
}
|
|
}
|
|
|
|
func (s *swapState) handleSendKeysMessage(msg *message.SendKeysMessage) error {
|
|
if msg.PublicSpendKey == nil || msg.PrivateViewKey == nil {
|
|
return errMissingKeys
|
|
}
|
|
|
|
// verify counterparty's DLEq proof and ensure the resulting secp256k1 key is correct
|
|
verifyResult, err := pcommon.VerifyKeysAndProof(msg.DLEqProof, msg.Secp256k1PublicKey, msg.PublicSpendKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.setXMRTakerKeys(msg.PublicSpendKey, msg.PrivateViewKey, verifyResult.Secp256k1PublicKey)
|
|
}
|