mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 18:38:09 +01:00
NMC and CLTV, abs lock values still to be verified
This commit is contained in:
@@ -126,6 +126,9 @@ class TxTypes(IntEnum): # For PooledAddress
|
||||
|
||||
SEQUENCE_LOCK_BLOCKS = 1
|
||||
SEQUENCE_LOCK_TIME = 2
|
||||
ABS_LOCK_BLOCKS = 3
|
||||
ABS_LOCK_TIME = 4
|
||||
|
||||
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
|
||||
SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22)
|
||||
SEQUENCE_LOCKTIME_MASK = 0x0000ffff
|
||||
@@ -183,6 +186,10 @@ def getLockName(lock_type):
|
||||
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 getExpectedSequence(lockType, lockVal, coin_type):
|
||||
@@ -206,7 +213,7 @@ def decodeSequence(lock_value):
|
||||
return lock_value & SEQUENCE_LOCKTIME_MASK
|
||||
|
||||
|
||||
def buildContractScriptCSV(sequence, secret_hash, pkh_redeem, pkh_refund):
|
||||
def buildContractScript(lock_val, secret_hash, pkh_redeem, pkh_refund, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY):
|
||||
script = bytearray([
|
||||
OpCodes.OP_IF,
|
||||
OpCodes.OP_SIZE,
|
||||
@@ -222,9 +229,9 @@ def buildContractScriptCSV(sequence, secret_hash, pkh_redeem, pkh_refund):
|
||||
0x14]) \
|
||||
+ pkh_redeem \
|
||||
+ bytearray([OpCodes.OP_ELSE, ]) \
|
||||
+ SerialiseNum(sequence) \
|
||||
+ SerialiseNum(lock_val) \
|
||||
+ bytearray([
|
||||
OpCodes.OP_CHECKSEQUENCEVERIFY,
|
||||
op_lock,
|
||||
OpCodes.OP_DROP,
|
||||
OpCodes.OP_DUP,
|
||||
OpCodes.OP_HASH160,
|
||||
@@ -590,6 +597,7 @@ class BasicSwap():
|
||||
'watched_outputs': [],
|
||||
'last_height_checked': last_height_checked,
|
||||
'use_segwit': use_segwit,
|
||||
'use_csv': chain_client_settings.get('use_csv', True),
|
||||
}
|
||||
|
||||
def start(self):
|
||||
@@ -710,8 +718,16 @@ class BasicSwap():
|
||||
def validateOfferLockValue(self, coin_from, coin_to, lock_type, lock_value):
|
||||
if lock_type == OfferMessage.SEQUENCE_LOCK_TIME:
|
||||
assert(lock_value >= 2 * 60 * 60 and lock_value <= 96 * 60 * 60), 'Invalid lock_value time'
|
||||
assert(self.coin_clients[coin_from]['use_csv'] and self.coin_clients[coin_to]['use_csv']), 'Both coins need CSV activated.'
|
||||
elif lock_type == OfferMessage.SEQUENCE_LOCK_BLOCKS:
|
||||
assert(lock_value >= 5 and lock_value <= 1000), 'Invalid lock_value blocks'
|
||||
assert(self.coin_clients[coin_from]['use_csv'] and self.coin_clients[coin_to]['use_csv']), 'Both coins need CSV activated.'
|
||||
elif lock_type == ABS_LOCK_TIME:
|
||||
# TODO: range?
|
||||
assert(lock_value >= 4 * 60 * 60 and lock_value <= 96 * 60 * 60), 'Invalid lock_value time'
|
||||
elif lock_type == ABS_LOCK_BLOCKS:
|
||||
# TODO: range?
|
||||
assert(lock_value >= 10 and lock_value <= 1000), 'Invalid lock_value blocks'
|
||||
else:
|
||||
raise ValueError('Unknown locktype')
|
||||
|
||||
@@ -872,7 +888,7 @@ class BasicSwap():
|
||||
def getReceiveAddressForCoin(self, coin_type):
|
||||
if coin_type == Coins.PART:
|
||||
new_addr = self.callcoinrpc(Coins.PART, 'getnewaddress')
|
||||
elif coin_type == Coins.LTC or coin_type == Coins.BTC:
|
||||
elif coin_type == Coins.LTC or coin_type == Coins.BTC or coin_type == Coins.NMC:
|
||||
args = []
|
||||
if self.coin_clients[coin_type]['use_segwit']:
|
||||
args = ['swap_receive', 'bech32']
|
||||
@@ -1123,15 +1139,22 @@ class BasicSwap():
|
||||
coin_from = Coins(offer.coin_from)
|
||||
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
|
||||
|
||||
# TODO: Use CLTV for coins without CSV
|
||||
sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
|
||||
secret = self.getContractSecret(bid_date, bid.contract_count)
|
||||
secret_hash = hashlib.sha256(secret).digest()
|
||||
|
||||
pubkey_refund = self.getContractPubkey(bid_date, bid.contract_count)
|
||||
pkhash_refund = getKeyID(pubkey_refund)
|
||||
|
||||
script = buildContractScriptCSV(sequence, secret_hash, bid.pkhash_buyer, pkhash_refund)
|
||||
if offer.lock_type < ABS_LOCK_BLOCKS:
|
||||
sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
|
||||
script = buildContractScript(sequence, secret_hash, bid.pkhash_buyer, pkhash_refund)
|
||||
else:
|
||||
if offer.lock_type == ABS_LOCK_BLOCKS:
|
||||
lock_value = self.callcoinrpc(coin_from, 'getblockchaininfo')['blocks'] + offer.lock_value
|
||||
else:
|
||||
lock_value = int(time.time()) + offer.lock_value
|
||||
logging.debug('initiate %s lock_value %d %d', coin_from, offer.lock_value, lock_value)
|
||||
script = buildContractScript(lock_value, secret_hash, bid.pkhash_buyer, pkhash_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY)
|
||||
|
||||
p2sh = self.callcoinrpc(Coins.PART, 'decodescript', [script.hex()])['p2sh']
|
||||
|
||||
@@ -1141,7 +1164,7 @@ class BasicSwap():
|
||||
txn = self.createInitiateTxn(coin_from, bid_id, bid)
|
||||
|
||||
# Store the signed refund txn in case wallet is locked when refund is possible
|
||||
refund_txn = self.createRefundTxn(coin_from, txn, bid, script)
|
||||
refund_txn = self.createRefundTxn(coin_from, txn, offer, bid, script)
|
||||
bid.initiate_txn_refund = bytes.fromhex(refund_txn)
|
||||
|
||||
txid = self.submitTxn(coin_from, txn)
|
||||
@@ -1153,7 +1176,7 @@ class BasicSwap():
|
||||
txid = self.submitTxn(coin_from, bid.initiate_txn_refund.hex())
|
||||
self.log.error('Submit refund_txn unexpectedly worked: ' + txid)
|
||||
except Exception as e:
|
||||
if 'non-BIP68-final' not in str(e):
|
||||
if 'non-BIP68-final' not in str(e) and 'non-final' not in str(e):
|
||||
self.log.error('Submit refund_txn unexpected error' + str(e))
|
||||
|
||||
if txid is not None:
|
||||
@@ -1269,14 +1292,22 @@ class BasicSwap():
|
||||
|
||||
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
|
||||
|
||||
# Participate txn is locked for half the time of the initiate txn
|
||||
lock_value = decodeSequence(offer.lock_value) // 2
|
||||
sequence = getExpectedSequence(offer.lock_type, lock_value, coin_to)
|
||||
|
||||
secret_hash = extractScriptSecretHash(bid.initiate_script)
|
||||
pkhash_seller = bid.pkhash_seller
|
||||
pkhash_buyer_refund = bid.pkhash_buyer
|
||||
bid.participate_script = buildContractScriptCSV(sequence, secret_hash, pkhash_seller, pkhash_buyer_refund)
|
||||
|
||||
# Participate txn is locked for half the time of the initiate txn
|
||||
lock_value = offer.lock_value // 2
|
||||
if offer.lock_type < ABS_LOCK_BLOCKS:
|
||||
sequence = getExpectedSequence(offer.lock_type, lock_value, coin_to)
|
||||
bid.participate_script = buildContractScript(sequence, secret_hash, pkhash_seller, pkhash_buyer_refund)
|
||||
else:
|
||||
if offer.lock_type == ABS_LOCK_BLOCKS:
|
||||
contract_lock_value = self.callcoinrpc(coin_to, 'getblockchaininfo')['blocks'] + lock_value
|
||||
else:
|
||||
contract_lock_value = int(time.time()) + lock_value
|
||||
logging.debug('participate %s lock_value %d %d', coin_to, lock_value, contract_lock_value)
|
||||
bid.participate_script = buildContractScript(contract_lock_value, secret_hash, pkhash_seller, pkhash_buyer_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY)
|
||||
|
||||
def createParticipateTxn(self, bid_id, bid, offer):
|
||||
self.log.debug('createParticipateTxn')
|
||||
@@ -1302,7 +1333,7 @@ class BasicSwap():
|
||||
|
||||
txn_signed = self.callcoinrpc(coin_to, 'signrawtransactionwithwallet', [txn_funded])['hex']
|
||||
|
||||
refund_txn = self.createRefundTxn(coin_to, txn_signed, bid, bid.participate_script, tx_type=TxTypes.PTX_REFUND)
|
||||
refund_txn = self.createRefundTxn(coin_to, txn_signed, offer, bid, bid.participate_script, tx_type=TxTypes.PTX_REFUND)
|
||||
bid.participate_txn_refund = bytes.fromhex(refund_txn)
|
||||
|
||||
chain_height = self.callcoinrpc(coin_to, 'getblockchaininfo')['blocks']
|
||||
@@ -1433,7 +1464,7 @@ class BasicSwap():
|
||||
|
||||
return redeem_txn
|
||||
|
||||
def createRefundTxn(self, coin_type, txn, bid, txn_script, addr_refund_out=None, tx_type=TxTypes.ITX_REFUND):
|
||||
def createRefundTxn(self, coin_type, txn, offer, bid, txn_script, addr_refund_out=None, tx_type=TxTypes.ITX_REFUND):
|
||||
self.log.debug('createRefundTxn')
|
||||
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
|
||||
return None
|
||||
@@ -1459,7 +1490,11 @@ class BasicSwap():
|
||||
'redeemScript': txn_script.hex(),
|
||||
'amount': prev_amount}
|
||||
|
||||
sequence = DeserialiseNum(txn_script, 64)
|
||||
lock_value = DeserialiseNum(txn_script, 64)
|
||||
if offer.lock_type < ABS_LOCK_BLOCKS:
|
||||
sequence = lock_value
|
||||
else:
|
||||
sequence = 1
|
||||
prevout_s = ' in={}:{}:{}'.format(txjs['txid'], vout, sequence)
|
||||
|
||||
fee_rate = self.getFeeRateForCoin(coin_type)
|
||||
@@ -1488,6 +1523,9 @@ class BasicSwap():
|
||||
else:
|
||||
refund_txn = self.calltx('-btcmode -create nversion=2' + prevout_s + output_to)
|
||||
|
||||
if offer.lock_type == ABS_LOCK_BLOCKS or offer.lock_type == ABS_LOCK_TIME:
|
||||
refund_txn = self.calltx('{} locktime={}'.format(refund_txn, lock_value))
|
||||
|
||||
options = {}
|
||||
if self.coin_clients[coin_type]['use_segwit']:
|
||||
options['force_segwit'] = True
|
||||
@@ -1691,7 +1729,7 @@ class BasicSwap():
|
||||
self.initiateTxnConfirmed(bid_id, bid, offer)
|
||||
save_bid = True
|
||||
|
||||
# Bid times out if buyer doesn't see tx in chain within
|
||||
# Bid times out if buyer doesn't see tx in chain within INITIATE_TX_TIMEOUT seconds
|
||||
if bid.state_time + INITIATE_TX_TIMEOUT < int(time.time()):
|
||||
self.log.info('Swap timed out waiting for initiate tx for bid %s', bid_id.hex())
|
||||
bid.setState(BidStates.SWAP_TIMEDOUT)
|
||||
@@ -1757,7 +1795,7 @@ class BasicSwap():
|
||||
self.log.debug('Submitted initiate refund txn %s to %s chain for bid %s', txid, chainparams[coin_from]['name'], bid_id.hex())
|
||||
# State will update when spend is detected
|
||||
except Exception as e:
|
||||
if 'non-BIP68-final (code 64)' not in str(e):
|
||||
if 'non-BIP68-final (code 64)' not in str(e) and 'non-final' not in str(e):
|
||||
self.log.warning('Error trying to submit initiate refund txn: %s', str(e))
|
||||
if (bid.participate_txn_state == TxStates.TX_SENT or bid.participate_txn_state == TxStates.TX_CONFIRMED) \
|
||||
and bid.participate_txn_refund is not None:
|
||||
@@ -1766,7 +1804,7 @@ class BasicSwap():
|
||||
self.log.debug('Submitted participate refund txn %s to %s chain for bid %s', txid, chainparams[coin_to]['name'], bid_id.hex())
|
||||
# State will update when spend is detected
|
||||
except Exception as e:
|
||||
if 'non-BIP68-final (code 64)' not in str(e):
|
||||
if 'non-BIP68-final (code 64)' not in str(e) and 'non-final' not in str(e):
|
||||
self.log.warning('Error trying to submit participate refund txn: %s', str(e))
|
||||
return False # Bid is still active
|
||||
|
||||
@@ -2090,19 +2128,11 @@ class BasicSwap():
|
||||
|
||||
self.log.debug('for bid %s', bid_accept_data.bid_msg_id.hex())
|
||||
|
||||
decoded_script = self.callcoinrpc(Coins.PART, 'decodescript', [bid_accept_data.contract_script.hex()])
|
||||
|
||||
# TODO: Verify script without decoding?
|
||||
prog = re.compile('OP_IF OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 (\w+) OP_EQUALVERIFY OP_DUP OP_HASH160 (\w+) OP_ELSE (\d+) OP_CHECKSEQUENCEVERIFY OP_DROP OP_DUP OP_HASH160 (\w+) OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG')
|
||||
rr = prog.match(decoded_script['asm'])
|
||||
if not rr:
|
||||
raise ValueError('Bad script')
|
||||
scriptvalues = rr.groups()
|
||||
|
||||
bid_id = bid_accept_data.bid_msg_id
|
||||
bid, offer = self.getBidAndOffer(bid_id)
|
||||
assert(bid is not None and bid.was_sent is True), 'Unknown bidid'
|
||||
assert(offer), 'Offer not found ' + bid.offer_id.hex()
|
||||
coin_from = Coins(offer.coin_from)
|
||||
|
||||
# assert(bid.expire_at > now), 'Bid expired' # How much time over to accept
|
||||
|
||||
@@ -2112,12 +2142,26 @@ class BasicSwap():
|
||||
return
|
||||
raise ValueError('Wrong bid state: {}'.format(str(BidStates(bid.state))))
|
||||
|
||||
coin_from = Coins(offer.coin_from)
|
||||
expect_sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
|
||||
use_csv = True if offer.lock_type < ABS_LOCK_BLOCKS else False
|
||||
|
||||
# TODO: Verify script without decoding?
|
||||
decoded_script = self.callcoinrpc(Coins.PART, 'decodescript', [bid_accept_data.contract_script.hex()])
|
||||
lock_check_op = 'OP_CHECKSEQUENCEVERIFY' if use_csv else 'OP_CHECKLOCKTIMEVERIFY'
|
||||
prog = re.compile('OP_IF OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 (\w+) OP_EQUALVERIFY OP_DUP OP_HASH160 (\w+) OP_ELSE (\d+) {} OP_DROP OP_DUP OP_HASH160 (\w+) OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG'.format(lock_check_op))
|
||||
rr = prog.match(decoded_script['asm'])
|
||||
if not rr:
|
||||
raise ValueError('Bad script')
|
||||
scriptvalues = rr.groups()
|
||||
|
||||
assert(len(scriptvalues[0]) == 64), 'Bad secret_hash length'
|
||||
assert(bytes.fromhex(scriptvalues[1]) == bid.pkhash_buyer), 'pkhash_buyer mismatch'
|
||||
assert(int(scriptvalues[2]) == expect_sequence), 'sequence mismatch'
|
||||
|
||||
if use_csv:
|
||||
expect_sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
|
||||
assert(int(scriptvalues[2]) == expect_sequence), 'sequence mismatch'
|
||||
else:
|
||||
self.log.warning('TODO: validate absolute lock values')
|
||||
|
||||
assert(len(scriptvalues[3]) == 40), 'pkhash_refund bad length'
|
||||
|
||||
assert(bid.accept_msg_id is None), 'Bid already accepted'
|
||||
|
||||
@@ -22,3 +22,8 @@ LITECOIN_BINDIR = os.path.expanduser(os.getenv('LITECOIN_BINDIR', ''))
|
||||
LITECOIND = os.getenv('LITECOIND', 'litecoind' + ('.exe' if os.name == 'nt' else ''))
|
||||
LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli' + ('.exe' if os.name == 'nt' else ''))
|
||||
LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx' + ('.exe' if os.name == 'nt' else ''))
|
||||
|
||||
NAMECOIN_BINDIR = os.path.expanduser(os.getenv('NAMECOIN_BINDIR', ''))
|
||||
NAMECOIND = os.getenv('NAMECOIND', 'namecoind' + ('.exe' if os.name == 'nt' else ''))
|
||||
NAMECOIN_CLI = os.getenv('NAMECOIN_CLI', 'namecoin-cli' + ('.exe' if os.name == 'nt' else ''))
|
||||
NAMECOIN_TX = os.getenv('NAMECOIN_TX', 'namecoin-tx' + ('.exe' if os.name == 'nt' else ''))
|
||||
|
||||
@@ -14,6 +14,8 @@ message OfferMessage {
|
||||
NOT_SET = 0;
|
||||
SEQUENCE_LOCK_BLOCKS = 1;
|
||||
SEQUENCE_LOCK_TIME = 2;
|
||||
ABS_LOCK_BLOCKS = 3;
|
||||
ABS_LOCK_TIME = 4;
|
||||
}
|
||||
LockType lock_type = 7;
|
||||
uint32 lock_value = 8;
|
||||
|
||||
Reference in New Issue
Block a user