diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 2c2ee59..ed7b1ed 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -23,7 +23,6 @@ import sqlalchemy as sa import collections import concurrent.futures -from enum import IntEnum, auto from sqlalchemy.orm import sessionmaker, scoped_session from sqlalchemy.orm.session import close_all_sessions @@ -93,314 +92,30 @@ from .explorers import ( ExplorerBitAps, ExplorerChainz, ) -from .types import ( - SEQUENCE_LOCK_BLOCKS, - SEQUENCE_LOCK_TIME, - ABS_LOCK_BLOCKS, - ABS_LOCK_TIME) import basicswap.config as cfg import basicswap.network as bsn import basicswap.protocols.atomic_swap_1 as atomic_swap_1 - - -class MessageTypes(IntEnum): - OFFER = auto() - BID = auto() - BID_ACCEPT = auto() - - XMR_OFFER = auto() - XMR_BID_FL = auto() - XMR_BID_SPLIT = auto() - XMR_BID_ACCEPT_LF = auto() - XMR_BID_TXN_SIGS_FL = auto() - XMR_BID_LOCK_SPEND_TX_LF = auto() - XMR_BID_LOCK_RELEASE_LF = auto() - OFFER_REVOKE = auto() - - -class SwapTypes(IntEnum): - SELLER_FIRST = auto() - BUYER_FIRST = auto() - SELLER_FIRST_2MSG = auto() - BUYER_FIRST_2MSG = auto() - XMR_SWAP = auto() - - -class OfferStates(IntEnum): - OFFER_SENT = auto() - OFFER_RECEIVED = auto() - OFFER_ABANDONED = auto() - - -class BidStates(IntEnum): - BID_SENT = auto() - BID_RECEIVING = auto() # Partially received - BID_RECEIVED = auto() - BID_RECEIVING_ACC = auto() # Partially received accept message - BID_ACCEPTED = auto() # BidAcceptMessage received/sent - SWAP_INITIATED = auto() # Initiate txn validated - SWAP_PARTICIPATING = auto() # Participate txn validated - SWAP_COMPLETED = auto() # All swap txns spent - XMR_SWAP_SCRIPT_COIN_LOCKED = auto() - XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX = auto() - XMR_SWAP_NOSCRIPT_COIN_LOCKED = auto() - XMR_SWAP_LOCK_RELEASED = auto() - XMR_SWAP_SCRIPT_TX_REDEEMED = auto() - XMR_SWAP_NOSCRIPT_TX_REDEEMED = auto() - XMR_SWAP_NOSCRIPT_TX_RECOVERED = auto() - XMR_SWAP_FAILED_REFUNDED = auto() - XMR_SWAP_FAILED_SWIPED = auto() - XMR_SWAP_FAILED = auto() - SWAP_DELAYING = auto() - SWAP_TIMEDOUT = auto() - BID_ABANDONED = auto() # Bid will no longer be processed - BID_ERROR = auto() # An error occurred - BID_STALLED_FOR_TEST = auto() - - -class TxStates(IntEnum): - TX_NONE = auto() - TX_SENT = auto() - TX_CONFIRMED = auto() - TX_REDEEMED = auto() - TX_REFUNDED = auto() - - -class TxTypes(IntEnum): - ITX = auto() - PTX = auto() - ITX_REDEEM = auto() - ITX_REFUND = auto() - PTX_REDEEM = auto() - PTX_REFUND = auto() - - XMR_SWAP_A_LOCK = auto() - XMR_SWAP_A_LOCK_SPEND = auto() - XMR_SWAP_A_LOCK_REFUND = auto() - XMR_SWAP_A_LOCK_REFUND_SPEND = auto() - XMR_SWAP_A_LOCK_REFUND_SWIPE = auto() - XMR_SWAP_B_LOCK = auto() - - -class EventTypes(IntEnum): - ACCEPT_BID = auto() - ACCEPT_XMR_BID = auto() - SIGN_XMR_SWAP_LOCK_TX_A = auto() - SEND_XMR_SWAP_LOCK_TX_A = auto() - SEND_XMR_SWAP_LOCK_TX_B = auto() - SEND_XMR_LOCK_RELEASE = auto() - REDEEM_XMR_SWAP_LOCK_TX_A = auto() # Follower - REDEEM_XMR_SWAP_LOCK_TX_B = auto() # Leader - RECOVER_XMR_SWAP_LOCK_TX_B = auto() - - -class EventLogTypes(IntEnum): - FAILED_TX_B_LOCK_PUBLISH = auto() - LOCK_TX_A_PUBLISHED = auto() - LOCK_TX_B_PUBLISHED = auto() - FAILED_TX_B_SPEND = auto() - LOCK_TX_A_SEEN = auto() - LOCK_TX_A_CONFIRMED = auto() - LOCK_TX_B_SEEN = auto() - LOCK_TX_B_CONFIRMED = auto() - DEBUG_TWEAK_APPLIED = auto() - FAILED_TX_B_REFUND = auto() - LOCK_TX_B_INVALID = auto() - LOCK_TX_A_REFUND_TX_PUBLISHED = auto() - LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED = auto() - LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED = auto() - LOCK_TX_B_REFUND_TX_PUBLISHED = auto() - SYSTEM_WARNING = auto() - LOCK_TX_A_SPEND_TX_PUBLISHED = auto() - LOCK_TX_B_SPEND_TX_PUBLISHED = auto() - - -class XmrSplitMsgTypes(IntEnum): - BID = auto() - BID_ACCEPT = auto() - - -class DebugTypes(IntEnum): - BID_STOP_AFTER_COIN_A_LOCK = auto() - BID_DONT_SPEND_COIN_A_LOCK_REFUND = auto() - CREATE_INVALID_COIN_B_LOCK = auto() - BUYER_STOP_AFTER_ITX = auto() - MAKE_INVALID_PTX = auto() - - -def strOfferState(state): - if state == OfferStates.OFFER_SENT: - return 'Sent' - if state == OfferStates.OFFER_RECEIVED: - return 'Received' - if state == OfferStates.OFFER_ABANDONED: - return 'Abandoned' - return 'Unknown' - - -def strBidState(state): - if state == BidStates.BID_SENT: - return 'Sent' - if state == BidStates.BID_RECEIVING: - return 'Receiving' - if state == BidStates.BID_RECEIVING_ACC: - return 'Receiving accept' - if state == BidStates.BID_RECEIVED: - return 'Received' - if state == BidStates.BID_ACCEPTED: - return 'Accepted' - if state == BidStates.SWAP_INITIATED: - return 'Initiated' - if state == BidStates.SWAP_PARTICIPATING: - return 'Participating' - if state == BidStates.SWAP_COMPLETED: - return 'Completed' - if state == BidStates.SWAP_TIMEDOUT: - return 'Timed-out' - if state == BidStates.BID_ABANDONED: - return 'Abandoned' - if state == BidStates.BID_STALLED_FOR_TEST: - return 'Stalled (debug)' - if state == BidStates.BID_ERROR: - return 'Error' - if state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED: - return 'Script coin locked' - if state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX: - return 'Script coin spend tx valid' - if state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED: - return 'Scriptless coin locked' - if state == BidStates.XMR_SWAP_LOCK_RELEASED: - return 'Script coin lock released' - if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED: - return 'Script tx redeemed' - if state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED: - return 'Scriptless tx redeemed' - if state == BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED: - return 'Scriptless tx recovered' - if state == BidStates.XMR_SWAP_FAILED_REFUNDED: - return 'Failed, refunded' - if state == BidStates.XMR_SWAP_FAILED_SWIPED: - return 'Failed, swiped' - if state == BidStates.XMR_SWAP_FAILED: - return 'Failed' - if state == BidStates.SWAP_DELAYING: - return 'Delaying' - return 'Unknown' - - -def strTxState(state): - if state == TxStates.TX_NONE: - return 'None' - if state == TxStates.TX_SENT: - return 'Sent' - if state == TxStates.TX_CONFIRMED: - return 'Confirmed' - if state == TxStates.TX_REDEEMED: - return 'Redeemed' - if state == TxStates.TX_REFUNDED: - return 'Refunded' - return 'Unknown' - - -def strTxType(tx_type): - if tx_type == TxTypes.XMR_SWAP_A_LOCK: - return 'Chain A Lock Tx' - if tx_type == TxTypes.XMR_SWAP_A_LOCK_SPEND: - return 'Chain A Lock Spend Tx' - if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND: - return 'Chain A Lock Refund Tx' - if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND: - return 'Chain A Lock Refund Spend Tx' - if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE: - return 'Chain A Lock Refund Swipe Tx' - if tx_type == TxTypes.XMR_SWAP_B_LOCK: - return 'Chain B Lock Tx' - return 'Unknown' - - -def getLockName(lock_type): - if lock_type == SEQUENCE_LOCK_BLOCKS: - return 'Sequence lock, blocks' - if lock_type == SEQUENCE_LOCK_TIME: - return 'Sequence lock, time' - if lock_type == ABS_LOCK_BLOCKS: - return 'blocks' - if lock_type == ABS_LOCK_TIME: - return 'time' - - -def describeEventEntry(event_type, event_msg): - if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH: - return 'Failed to publish lock tx B' - if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH: - return 'Failed to publish lock tx B' - if event_type == EventLogTypes.LOCK_TX_A_PUBLISHED: - return 'Lock tx A published' - if event_type == EventLogTypes.LOCK_TX_B_PUBLISHED: - return 'Lock tx B published' - if event_type == EventLogTypes.FAILED_TX_B_SPEND: - return 'Failed to publish lock tx B spend' - if event_type == EventLogTypes.LOCK_TX_A_SEEN: - return 'Lock tx A seen in chain' - if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED: - return 'Lock tx A confirmed in chain' - if event_type == EventLogTypes.LOCK_TX_B_SEEN: - return 'Lock tx B seen in chain' - if event_type == EventLogTypes.LOCK_TX_B_CONFIRMED: - return 'Lock tx B confirmed in chain' - if event_type == EventLogTypes.DEBUG_TWEAK_APPLIED: - return 'Debug tweak applied ' + event_msg - if event_type == EventLogTypes.FAILED_TX_B_REFUND: - return 'Failed to publish lock tx B refund' - if event_type == EventLogTypes.LOCK_TX_B_INVALID: - return 'Detected invalid lock Tx B' - if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED: - return 'Lock tx A refund tx published' - if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED: - return 'Lock tx A refund spend tx published' - if event_type == EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED: - return 'Lock tx A refund swipe tx published' - if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED: - return 'Lock tx B refund tx published' - if event_type == EventLogTypes.LOCK_TX_A_SPEND_TX_PUBLISHED: - return 'Lock tx A spend tx published' - if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED: - return 'Lock tx B spend tx published' - if event_type == EventLogTypes.SYSTEM_WARNING: - return 'Warning: ' + event_msg - - -def getVoutByAddress(txjs, p2sh): - for o in txjs['vout']: - try: - if p2sh in o['scriptPubKey']['addresses']: - return o['n'] - except Exception: - pass - raise ValueError('Address output not found in txn') - - -def getVoutByP2WSH(txjs, p2wsh_hex): - for o in txjs['vout']: - try: - if p2wsh_hex == o['scriptPubKey']['hex']: - return o['n'] - except Exception: - pass - raise ValueError('P2WSH output not found in txn') - - -def replaceAddrPrefix(addr, coin_type, chain_name, addr_type='pubkey_address'): - return encodeAddress(bytes((chainparams[coin_type][chain_name][addr_type],)) + decodeAddress(addr)[1:]) - - -def getOfferProofOfFundsHash(offer_msg, offer_addr): - # TODO: Hash must not include proof_of_funds sig if it exists in offer_msg - h = hashlib.sha256() - h.update(offer_addr.encode('utf-8')) - offer_bytes = offer_msg.SerializeToString() - h.update(offer_bytes) - return h.digest() +from .basicswap_util import ( + SEQUENCE_LOCK_TIME, + ABS_LOCK_BLOCKS, + ABS_LOCK_TIME, + MessageTypes, + SwapTypes, + OfferStates, + BidStates, + TxStates, + TxTypes, + EventTypes, + EventLogTypes, + XmrSplitMsgTypes, + DebugTypes, + strBidState, + describeEventEntry, + getVoutByAddress, + getVoutByP2WSH, + replaceAddrPrefix, + getOfferProofOfFundsHash, + getLastBidState) def threadPollChainState(swap_client, coin_type): @@ -2213,7 +1928,12 @@ class BasicSwap(BaseApp): assert(bid), 'Bid not found: {}.'.format(bid_id.hex()) assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex()) assert(bid.expire_at > now), 'Bid expired' - assert(bid.state == BidStates.BID_RECEIVED), 'Wrong bid state: {}'.format(str(BidStates(bid.state))) + + last_bid_state = bid.state + if last_bid_state == BidStates.SWAP_DELAYING: + last_bid_state = getLastBidState(bid.states) + + assert(last_bid_state == BidStates.BID_RECEIVED), 'Wrong bid state: {}'.format(str(BidStates(last_bid_state))) offer, xmr_offer = self.getXmrOffer(bid.offer_id) assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex()) @@ -3989,7 +3709,6 @@ class BasicSwap(BaseApp): self.log.info('Received valid bid %s for xmr offer %s', bid.bid_id.hex(), bid.offer_id.hex()) bid.setState(BidStates.BID_RECEIVED) - self.saveBidInSession(bid.bid_id, bid, session, xmr_swap) # Auto accept bid if set and no other non-abandoned bid for this order exists if offer.auto_accept_bids: @@ -4001,6 +3720,9 @@ class BasicSwap(BaseApp): delay = random.randrange(self.min_delay_event, self.max_delay_event) self.log.info('Auto accepting xmr bid %s in %d seconds', bid.bid_id.hex(), delay) self.createEventInSession(delay, EventTypes.ACCEPT_XMR_BID, bid.bid_id, session) + bid.setState(BidStates.SWAP_DELAYING) + + self.saveBidInSession(bid.bid_id, bid, session, xmr_swap) def receiveXmrBidAccept(self, bid, session): # Follower receiving MSG1F and MSG2F diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py new file mode 100644 index 0000000..8952856 --- /dev/null +++ b/basicswap/basicswap_util.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + + +import struct +import hashlib +from enum import IntEnum, auto +from .util import ( + encodeAddress, + decodeAddress, +) +from .chainparams import ( + chainparams, +) + + +SEQUENCE_LOCK_BLOCKS = 1 +SEQUENCE_LOCK_TIME = 2 +ABS_LOCK_BLOCKS = 3 +ABS_LOCK_TIME = 4 + + +class MessageTypes(IntEnum): + OFFER = auto() + BID = auto() + BID_ACCEPT = auto() + + XMR_OFFER = auto() + XMR_BID_FL = auto() + XMR_BID_SPLIT = auto() + XMR_BID_ACCEPT_LF = auto() + XMR_BID_TXN_SIGS_FL = auto() + XMR_BID_LOCK_SPEND_TX_LF = auto() + XMR_BID_LOCK_RELEASE_LF = auto() + OFFER_REVOKE = auto() + + +class SwapTypes(IntEnum): + SELLER_FIRST = auto() + BUYER_FIRST = auto() + SELLER_FIRST_2MSG = auto() + BUYER_FIRST_2MSG = auto() + XMR_SWAP = auto() + + +class OfferStates(IntEnum): + OFFER_SENT = auto() + OFFER_RECEIVED = auto() + OFFER_ABANDONED = auto() + + +class BidStates(IntEnum): + BID_SENT = auto() + BID_RECEIVING = auto() # Partially received + BID_RECEIVED = auto() + BID_RECEIVING_ACC = auto() # Partially received accept message + BID_ACCEPTED = auto() # BidAcceptMessage received/sent + SWAP_INITIATED = auto() # Initiate txn validated + SWAP_PARTICIPATING = auto() # Participate txn validated + SWAP_COMPLETED = auto() # All swap txns spent + XMR_SWAP_SCRIPT_COIN_LOCKED = auto() + XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX = auto() + XMR_SWAP_NOSCRIPT_COIN_LOCKED = auto() + XMR_SWAP_LOCK_RELEASED = auto() + XMR_SWAP_SCRIPT_TX_REDEEMED = auto() + XMR_SWAP_NOSCRIPT_TX_REDEEMED = auto() + XMR_SWAP_NOSCRIPT_TX_RECOVERED = auto() + XMR_SWAP_FAILED_REFUNDED = auto() + XMR_SWAP_FAILED_SWIPED = auto() + XMR_SWAP_FAILED = auto() + SWAP_DELAYING = auto() + SWAP_TIMEDOUT = auto() + BID_ABANDONED = auto() # Bid will no longer be processed + BID_ERROR = auto() # An error occurred + BID_STALLED_FOR_TEST = auto() + BID_STATE_UNKNOWN = auto() + + +class TxStates(IntEnum): + TX_NONE = auto() + TX_SENT = auto() + TX_CONFIRMED = auto() + TX_REDEEMED = auto() + TX_REFUNDED = auto() + + +class TxTypes(IntEnum): + ITX = auto() + PTX = auto() + ITX_REDEEM = auto() + ITX_REFUND = auto() + PTX_REDEEM = auto() + PTX_REFUND = auto() + + XMR_SWAP_A_LOCK = auto() + XMR_SWAP_A_LOCK_SPEND = auto() + XMR_SWAP_A_LOCK_REFUND = auto() + XMR_SWAP_A_LOCK_REFUND_SPEND = auto() + XMR_SWAP_A_LOCK_REFUND_SWIPE = auto() + XMR_SWAP_B_LOCK = auto() + + +class EventTypes(IntEnum): + ACCEPT_BID = auto() + ACCEPT_XMR_BID = auto() + SIGN_XMR_SWAP_LOCK_TX_A = auto() + SEND_XMR_SWAP_LOCK_TX_A = auto() + SEND_XMR_SWAP_LOCK_TX_B = auto() + SEND_XMR_LOCK_RELEASE = auto() + REDEEM_XMR_SWAP_LOCK_TX_A = auto() # Follower + REDEEM_XMR_SWAP_LOCK_TX_B = auto() # Leader + RECOVER_XMR_SWAP_LOCK_TX_B = auto() + + +class EventLogTypes(IntEnum): + FAILED_TX_B_LOCK_PUBLISH = auto() + LOCK_TX_A_PUBLISHED = auto() + LOCK_TX_B_PUBLISHED = auto() + FAILED_TX_B_SPEND = auto() + LOCK_TX_A_SEEN = auto() + LOCK_TX_A_CONFIRMED = auto() + LOCK_TX_B_SEEN = auto() + LOCK_TX_B_CONFIRMED = auto() + DEBUG_TWEAK_APPLIED = auto() + FAILED_TX_B_REFUND = auto() + LOCK_TX_B_INVALID = auto() + LOCK_TX_A_REFUND_TX_PUBLISHED = auto() + LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED = auto() + LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED = auto() + LOCK_TX_B_REFUND_TX_PUBLISHED = auto() + SYSTEM_WARNING = auto() + LOCK_TX_A_SPEND_TX_PUBLISHED = auto() + LOCK_TX_B_SPEND_TX_PUBLISHED = auto() + + +class XmrSplitMsgTypes(IntEnum): + BID = auto() + BID_ACCEPT = auto() + + +class DebugTypes(IntEnum): + BID_STOP_AFTER_COIN_A_LOCK = auto() + BID_DONT_SPEND_COIN_A_LOCK_REFUND = auto() + CREATE_INVALID_COIN_B_LOCK = auto() + BUYER_STOP_AFTER_ITX = auto() + MAKE_INVALID_PTX = auto() + + +def strOfferState(state): + if state == OfferStates.OFFER_SENT: + return 'Sent' + if state == OfferStates.OFFER_RECEIVED: + return 'Received' + if state == OfferStates.OFFER_ABANDONED: + return 'Abandoned' + return 'Unknown' + + +def strBidState(state): + if state == BidStates.BID_SENT: + return 'Sent' + if state == BidStates.BID_RECEIVING: + return 'Receiving' + if state == BidStates.BID_RECEIVING_ACC: + return 'Receiving accept' + if state == BidStates.BID_RECEIVED: + return 'Received' + if state == BidStates.BID_ACCEPTED: + return 'Accepted' + if state == BidStates.SWAP_INITIATED: + return 'Initiated' + if state == BidStates.SWAP_PARTICIPATING: + return 'Participating' + if state == BidStates.SWAP_COMPLETED: + return 'Completed' + if state == BidStates.SWAP_TIMEDOUT: + return 'Timed-out' + if state == BidStates.BID_ABANDONED: + return 'Abandoned' + if state == BidStates.BID_STALLED_FOR_TEST: + return 'Stalled (debug)' + if state == BidStates.BID_ERROR: + return 'Error' + if state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED: + return 'Script coin locked' + if state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX: + return 'Script coin spend tx valid' + if state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED: + return 'Scriptless coin locked' + if state == BidStates.XMR_SWAP_LOCK_RELEASED: + return 'Script coin lock released' + if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED: + return 'Script tx redeemed' + if state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED: + return 'Scriptless tx redeemed' + if state == BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED: + return 'Scriptless tx recovered' + if state == BidStates.XMR_SWAP_FAILED_REFUNDED: + return 'Failed, refunded' + if state == BidStates.XMR_SWAP_FAILED_SWIPED: + return 'Failed, swiped' + if state == BidStates.XMR_SWAP_FAILED: + return 'Failed' + if state == BidStates.SWAP_DELAYING: + return 'Delaying' + return 'Unknown' + + +def strTxState(state): + if state == TxStates.TX_NONE: + return 'None' + if state == TxStates.TX_SENT: + return 'Sent' + if state == TxStates.TX_CONFIRMED: + return 'Confirmed' + if state == TxStates.TX_REDEEMED: + return 'Redeemed' + if state == TxStates.TX_REFUNDED: + return 'Refunded' + return 'Unknown' + + +def strTxType(tx_type): + if tx_type == TxTypes.XMR_SWAP_A_LOCK: + return 'Chain A Lock Tx' + if tx_type == TxTypes.XMR_SWAP_A_LOCK_SPEND: + return 'Chain A Lock Spend Tx' + if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND: + return 'Chain A Lock Refund Tx' + if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND: + return 'Chain A Lock Refund Spend Tx' + if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE: + return 'Chain A Lock Refund Swipe Tx' + if tx_type == TxTypes.XMR_SWAP_B_LOCK: + return 'Chain B Lock Tx' + return 'Unknown' + + +def getLockName(lock_type): + if lock_type == SEQUENCE_LOCK_BLOCKS: + return 'Sequence lock, blocks' + if lock_type == SEQUENCE_LOCK_TIME: + return 'Sequence lock, time' + if lock_type == ABS_LOCK_BLOCKS: + return 'blocks' + if lock_type == ABS_LOCK_TIME: + return 'time' + + +def describeEventEntry(event_type, event_msg): + if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH: + return 'Failed to publish lock tx B' + if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH: + return 'Failed to publish lock tx B' + if event_type == EventLogTypes.LOCK_TX_A_PUBLISHED: + return 'Lock tx A published' + if event_type == EventLogTypes.LOCK_TX_B_PUBLISHED: + return 'Lock tx B published' + if event_type == EventLogTypes.FAILED_TX_B_SPEND: + return 'Failed to publish lock tx B spend' + if event_type == EventLogTypes.LOCK_TX_A_SEEN: + return 'Lock tx A seen in chain' + if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED: + return 'Lock tx A confirmed in chain' + if event_type == EventLogTypes.LOCK_TX_B_SEEN: + return 'Lock tx B seen in chain' + if event_type == EventLogTypes.LOCK_TX_B_CONFIRMED: + return 'Lock tx B confirmed in chain' + if event_type == EventLogTypes.DEBUG_TWEAK_APPLIED: + return 'Debug tweak applied ' + event_msg + if event_type == EventLogTypes.FAILED_TX_B_REFUND: + return 'Failed to publish lock tx B refund' + if event_type == EventLogTypes.LOCK_TX_B_INVALID: + return 'Detected invalid lock Tx B' + if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED: + return 'Lock tx A refund tx published' + if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED: + return 'Lock tx A refund spend tx published' + if event_type == EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED: + return 'Lock tx A refund swipe tx published' + if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED: + return 'Lock tx B refund tx published' + if event_type == EventLogTypes.LOCK_TX_A_SPEND_TX_PUBLISHED: + return 'Lock tx A spend tx published' + if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED: + return 'Lock tx B spend tx published' + if event_type == EventLogTypes.SYSTEM_WARNING: + return 'Warning: ' + event_msg + + +def getVoutByAddress(txjs, p2sh): + for o in txjs['vout']: + try: + if p2sh in o['scriptPubKey']['addresses']: + return o['n'] + except Exception: + pass + raise ValueError('Address output not found in txn') + + +def getVoutByP2WSH(txjs, p2wsh_hex): + for o in txjs['vout']: + try: + if p2wsh_hex == o['scriptPubKey']['hex']: + return o['n'] + except Exception: + pass + raise ValueError('P2WSH output not found in txn') + + +def replaceAddrPrefix(addr, coin_type, chain_name, addr_type='pubkey_address'): + return encodeAddress(bytes((chainparams[coin_type][chain_name][addr_type],)) + decodeAddress(addr)[1:]) + + +def getOfferProofOfFundsHash(offer_msg, offer_addr): + # TODO: Hash must not include proof_of_funds sig if it exists in offer_msg + h = hashlib.sha256() + h.update(offer_addr.encode('utf-8')) + offer_bytes = offer_msg.SerializeToString() + h.update(offer_bytes) + return h.digest() + + +def getLastBidState(packed_states): + num_states = len(packed_states) // 12 + if num_states < 2: + return BidStates.BID_STATE_UNKNOWN + return struct.unpack_from('Swap{{ data.amt_from }} {{ data.ticker_from }} for {{ data.amt_to }} {{ data.ticker_to }} {% endif %} Bid State{{ data.bid_state }} -StateDescription {{ data.state_description }} +State Description {{ data.state_description }} ITX State{{ data.itx_state }} PTX State{{ data.ptx_state }} Offer{{ data.offer_id }} diff --git a/basicswap/templates/bid_xmr.html b/basicswap/templates/bid_xmr.html index 50df7ab..4663d4e 100644 --- a/basicswap/templates/bid_xmr.html +++ b/basicswap/templates/bid_xmr.html @@ -16,7 +16,7 @@ Swap{{ data.amt_from }} {{ data.ticker_from }} for {{ data.amt_to }} {{ data.ticker_to }} {% endif %} Bid State{{ data.bid_state }} -StateDescription {{ data.state_description }} +State Description {{ data.state_description }} Offer{{ data.offer_id }} Address From{{ data.addr_from }} Created At{{ data.created_at }} diff --git a/basicswap/types.py b/basicswap/types.py deleted file mode 100644 index 645b24d..0000000 --- a/basicswap/types.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2021 tecnovert -# Distributed under the MIT software license, see the accompanying -# file LICENSE or http://www.opensource.org/licenses/mit-license.php. - -SEQUENCE_LOCK_BLOCKS = 1 -SEQUENCE_LOCK_TIME = 2 -ABS_LOCK_BLOCKS = 3 -ABS_LOCK_TIME = 4 diff --git a/basicswap/ui.py b/basicswap/ui.py index 24d0208..45c7ae5 100644 --- a/basicswap/ui.py +++ b/basicswap/ui.py @@ -12,7 +12,8 @@ from .util import ( from .chainparams import ( Coins, ) -from .basicswap import ( +from .basicswap_util import ( + SEQUENCE_LOCK_TIME, SwapTypes, BidStates, TxStates, @@ -20,9 +21,7 @@ from .basicswap import ( strTxType, strBidState, strTxState, -) -from .types import ( - SEQUENCE_LOCK_TIME, + getLastBidState, ) PAGE_LIMIT = 50 @@ -164,6 +163,42 @@ def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_b state_description = 'Bid abandoned' elif bid.state == BidStates.BID_ERROR: state_description = bid.state_note + elif offer.swap_type == SwapTypes.XMR_SWAP: + if bid.state == BidStates.BID_RECEIVING: + # Offerer receiving bid from bidder + state_description = 'Waiting for bid to be fully received' + elif bid.state == BidStates.BID_RECEIVED: + # Offerer received bid from bidder + # TODO: Manual vs automatic + state_description = 'Bid must be accepted' + elif bid.state == BidStates.BID_RECEIVING_ACC: + state_description = 'Receiving accepted bid message' + elif bid.state == BidStates.BID_ACCEPTED: + state_description = 'Offerer has accepted bid, waiting for bidder to respond' + elif bid.state == BidStates.SWAP_DELAYING: + last_state = getLastBidState(bid.states) + if last_state == BidStates.BID_RECEIVED: + state_description = 'Delaying before accepting bid' + elif last_state == BidStates.BID_RECEIVING_ACC: + state_description = 'Delaying before responding to accepted bid' + elif last_state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED: + state_description = f'Delaying before spending from {ticker_to} lock tx' + elif last_state == BidStates.BID_ACCEPTED: + state_description = f'Delaying before sending {ticker_from} lock tx' + else: + state_description = 'Delaying before automated action' + elif bid.state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX: + state_description = f'Waiting for {ticker_from} lock tx to confirm in chain' + elif bid.state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED: + state_description = f'Waiting for {ticker_to} lock tx to confirm in chain' + elif bid.state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED: + state_description = f'Waiting for offerer to unlock {ticker_from} lock tx' + elif bid.state == BidStates.XMR_SWAP_LOCK_RELEASED: + state_description = f'Waiting for bidder to spend from {ticker_from} lock tx' + elif bid.state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED: + state_description = f'Waiting for offerer to spend from {ticker_to} lock tx' + elif bid.state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED: + state_description = f'Waiting for {ticker_to} lock tx spend tx to confirm in chain' data = { 'amt_from': ci_from.format_amount(bid.amount), diff --git a/doc/notes.md b/doc/notes.md index d226a07..ca347ad 100644 --- a/doc/notes.md +++ b/doc/notes.md @@ -13,46 +13,3 @@ Features still required (of many): - More swap protocols - Manual method to set wallet seeds from particl mnemonic - prepare script tries to load seeds automatically, btc versions < 0.21 require a fully synced chain - - -## Seller first protocol: - -Seller sends the 1st transaction. - -1. Seller posts offer. - - smsg from seller to network - coin-from - coin-to - amount-from - rate - min-amount - time-valid - -2. Buyer posts bid: - - smsg from buyer to seller - offerid - amount - proof-of-funds - address_to_buyer - time-valid - -3. Seller accepts bid: - - verifies proof-of-funds - - generates secret - - submits initiate tx to coin-from network - - smsg from seller to buyer - txid - initiatescript (includes pkhash_to_seller as the pkhash_refund) - -4. Buyer participates: - - inspects initiate tx in coin-from network - - submits participate tx in coin-to network - -5. Seller redeems: - - constructs participatescript - - inspects participate tx in coin-to network - - redeems from participate tx revealing secret - -6. Buyer redeems: - - scans coin-to network for seller-redeem tx - - redeems from initiate tx with revealed secret diff --git a/doc/protocols/seller_first.md b/doc/protocols/seller_first.md new file mode 100644 index 0000000..1b4afd7 --- /dev/null +++ b/doc/protocols/seller_first.md @@ -0,0 +1,43 @@ +# Seller first protocol + +Seller sends the first transaction. +Both coin types must support scripts. + + +1. Seller posts offer. + - smsg from seller to network + coin-from + coin-to + amount-from + rate + min-amount + time-valid + +2. Buyer posts bid: + - smsg from buyer to seller + offerid + amount + proof-of-funds + address_to_buyer + time-valid + +3. Seller accepts bid: + - verifies proof-of-funds + - generates secret + - submits initiate tx to coin-from network + - smsg from seller to buyer + txid + initiatescript (includes pkhash_to_seller as the pkhash_refund) + +4. Buyer participates: + - inspects initiate tx in coin-from network + - submits participate tx in coin-to network + +5. Seller redeems: + - constructs participatescript + - inspects participate tx in coin-to network + - redeems from participate tx revealing secret + +6. Buyer redeems: + - scans coin-to network for seller-redeem tx + - redeems from initiate tx with revealed secret diff --git a/doc/protocols/xmr.md b/doc/protocols/xmr.md new file mode 100644 index 0000000..c47e494 --- /dev/null +++ b/doc/protocols/xmr.md @@ -0,0 +1,79 @@ +# XMR protocol + + +## WIP + +Relies on a One-Time Verifiably Encrypted Signature (OtVES) to function +An OtVES: + - Is a valid signature for key (a) encrypted with a public key (B) + - Can be decrypted into a valid signature for key (a) with the private key (b) to the encrypting public key (B) + - The encrypting private key (b) can be recovered using both the encrypted and decrypted signatures. + + +Leader - sends the first lock tx. + + +NOSCRIPT_COIN lock tx: + - sent second + - is sent to a combined key using a private key from each participant. + + +SCRIPT_COIN lock tx: + - Sent first + - Requires two signatures to spend from. + - Refund to sender txn is presigned for and can only be mined in the future. + - Spending the refund tx reveals the leader's NOSCRIPT_COIN split private key + - Sender withholds signature until NOSCRIPT_COIN lock tx is confirmed. + - spending the spend txn reveals the follower's NOSCRIPT_COIN split private key + + +``` +Offerer (Leader) | Bidder (Follower) | +------------------------------------------------------------------------|-------------------------------------------------------------------------------| +o1. Sends offer | | + - x SCRIPT_COIN for y NOSCRIPT_COIN | | + - sends smsg OfferMessage | | + | b1. Receives offer | + | - validates offer | + | b2. Sends bid | + | - sends smsgs XmrBidMessage + 2x XmrSplitMessage | + | | +o2. Receives bid | | + - validates bid | | +o3. Accepts bid | | + - sends smsgs XmrBidAcceptMessage + 2x XmrSplitMessage | | + | | + | b3. Receives bid accept | + | - validates | + | - signs for lock tx refund | + | - sends smsg XmrBidLockTxSigsMessage | + | | +o4. Receives bidder lock refund tx signatures | | + - sends smsg XmrBidLockSpendTxMessage | | + - full SCRIPT_COIN lock tx | | + - signature to prove leader can sign for split key | | + - submits SCRIPT_COIN lock tx to network | | + | | + | b4. Receives XmrBidLockSpendTxMessage | + | - validates SCRIPT_COIN lock tx and signature | + | - waits for SCRIPT_COIN lock tx to confirm in chain | + | b5. Sends NOSCRIPT_COIN lock tx | + | | +o5. Waits for NOSCRIPT_COIN lock tx to confirm in chain | | +o6. Sends SCRIPT_COIN lock release. | | + - sends smsg XmrBidLockReleaseMessage | | + - includes OtVES ciphertext signature for the SCRIPT_COIN lock | | + spend tx. | | + | | + | b6. Receives offerer OtVES for SCRIPT_COIN lock spend tx. | + | - submits SCRIPT_COIN lock spend tx to network. | + | | +o7. Waits for SCRIPT_COIN lock spend tx. | | + - Extracts the NOSCRIPT_COIN bidders key using the signature | | +o8. Combines the keys to spend from the NOSCRIPT_COIN lock tx | | + - submits NOSCRIPT_COIN lock spend tx to network | | +``` + +Offerer sent 6 smsgs (2 extra from split messages) +Bidder sent 4 smsgs (2 extra from split messages) + diff --git a/tests/basicswap/test_other.py b/tests/basicswap/test_other.py index 352702b..9cb4a70 100644 --- a/tests/basicswap/test_other.py +++ b/tests/basicswap/test_other.py @@ -24,15 +24,16 @@ from coincurve.keys import ( from basicswap.ecc_util import i2b from basicswap.interface_btc import BTCInterface from basicswap.interface_xmr import XMRInterface + +from basicswap.basicswap_util import ( + SEQUENCE_LOCK_BLOCKS, + SEQUENCE_LOCK_TIME) from basicswap.util import ( SerialiseNum, DeserialiseNum, make_int, format_amount, validate_amount) -from basicswap.types import ( - SEQUENCE_LOCK_BLOCKS, - SEQUENCE_LOCK_TIME) class Test(unittest.TestCase): diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index b1d2f76..9b3e5be 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -32,6 +32,8 @@ from basicswap.basicswap import ( BidStates, TxStates, DebugTypes, +) +from basicswap.basicswap_util import ( SEQUENCE_LOCK_BLOCKS, ) from basicswap.util import ( diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index acae892..d1feef3 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -25,6 +25,8 @@ from basicswap.basicswap import ( SwapTypes, BidStates, DebugTypes, +) +from basicswap.basicswap_util import ( SEQUENCE_LOCK_BLOCKS, ) from basicswap.util import (