|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
# Copyright (c) 2020-2022 tecnovert
|
|
|
|
# Distributed under the MIT software license, see the accompanying
|
|
|
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
|
|
|
|
import json
|
|
|
|
import time
|
|
|
|
import base64
|
|
|
|
import hashlib
|
|
|
|
import logging
|
|
|
|
import traceback
|
|
|
|
from io import BytesIO
|
|
|
|
from basicswap.contrib.test_framework import segwit_addr
|
|
|
|
|
|
|
|
from basicswap.util import (
|
|
|
|
dumpj,
|
|
|
|
ensure,
|
|
|
|
make_int,
|
|
|
|
b2h, i2b, b2i, i2h)
|
|
|
|
from basicswap.util.ecc import (
|
|
|
|
ep,
|
|
|
|
pointToCPK, CPKToPoint,
|
|
|
|
getSecretInt)
|
|
|
|
from basicswap.util.script import (
|
|
|
|
decodeScriptNum,
|
|
|
|
getCompactSizeLen,
|
|
|
|
SerialiseNumCompact,
|
|
|
|
getWitnessElementLen,
|
|
|
|
)
|
|
|
|
from basicswap.util.address import (
|
|
|
|
toWIF,
|
|
|
|
b58encode,
|
|
|
|
decodeWif,
|
|
|
|
decodeAddress,
|
|
|
|
pubkeyToAddress,
|
|
|
|
)
|
|
|
|
from coincurve.keys import (
|
|
|
|
PrivateKey,
|
|
|
|
PublicKey)
|
|
|
|
from coincurve.dleag import (
|
|
|
|
verify_secp256k1_point)
|
|
|
|
from coincurve.ecdsaotves import (
|
|
|
|
ecdsaotves_enc_sign,
|
|
|
|
ecdsaotves_enc_verify,
|
|
|
|
ecdsaotves_dec_sig,
|
|
|
|
ecdsaotves_rec_enc_key)
|
|
|
|
|
|
|
|
from basicswap.contrib.test_framework.messages import (
|
|
|
|
COIN,
|
|
|
|
COutPoint,
|
|
|
|
CTransaction,
|
|
|
|
CTxIn,
|
|
|
|
CTxInWitness,
|
|
|
|
CTxOut,
|
|
|
|
FromHex)
|
|
|
|
|
|
|
|
from basicswap.contrib.test_framework.script import (
|
|
|
|
CScript, CScriptOp,
|
|
|
|
OP_IF, OP_ELSE, OP_ENDIF,
|
|
|
|
OP_0, OP_2,
|
|
|
|
OP_CHECKSIG,
|
|
|
|
OP_CHECKMULTISIG,
|
|
|
|
OP_CHECKSEQUENCEVERIFY,
|
|
|
|
OP_DROP,
|
|
|
|
SIGHASH_ALL,
|
|
|
|
SegwitV0SignatureHash,
|
|
|
|
hash160)
|
|
|
|
|
|
|
|
from basicswap.basicswap_util import (
|
|
|
|
TxLockTypes)
|
|
|
|
|
|
|
|
from basicswap.chainparams import CoinInterface, Coins
|
|
|
|
from basicswap.rpc import make_rpc_func, openrpc
|
|
|
|
|
|
|
|
|
|
|
|
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
|
|
|
|
SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22)
|
|
|
|
SEQUENCE_LOCKTIME_MASK = 0x0000ffff
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_op(v, err_string='Bad opcode'):
|
|
|
|
ensure(v, err_string)
|
|
|
|
|
|
|
|
|
|
|
|
def findOutput(tx, script_pk):
|
|
|
|
for i in range(len(tx.vout)):
|
|
|
|
if tx.vout[i].scriptPubKey == script_pk:
|
|
|
|
return i
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def find_vout_for_address_from_txobj(tx_obj, addr):
|
|
|
|
"""
|
|
|
|
Locate the vout index of the given transaction sending to the
|
|
|
|
given address. Raises runtime error exception if not found.
|
|
|
|
"""
|
|
|
|
for i in range(len(tx_obj["vout"])):
|
|
|
|
scriptPubKey = tx_obj["vout"][i]["scriptPubKey"]
|
|
|
|
if "addresses" in scriptPubKey:
|
|
|
|
if any([addr == a for a in scriptPubKey["addresses"]]):
|
|
|
|
return i
|
|
|
|
elif "address" in scriptPubKey:
|
|
|
|
if addr == scriptPubKey["address"]:
|
|
|
|
return i
|
|
|
|
raise RuntimeError("Vout not found for address: txid={}, addr={}".format(tx_obj['txid'], addr))
|
|
|
|
|
|
|
|
|
|
|
|
class BTCInterface(CoinInterface):
|
|
|
|
@staticmethod
|
|
|
|
def coin_type():
|
|
|
|
return Coins.BTC
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def COIN():
|
|
|
|
return COIN
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def exp() -> int:
|
|
|
|
return 8
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def nbk() -> int:
|
|
|
|
return 32
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def nbK() -> int: # No. of bytes requires to encode a public key
|
|
|
|
return 33
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def witnessScaleFactor() -> int:
|
|
|
|
return 4
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def txVersion() -> int:
|
|
|
|
return 2
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getTxOutputValue(tx):
|
|
|
|
rv = 0
|
|
|
|
for output in tx.vout:
|
|
|
|
rv += output.nValue
|
|
|
|
return rv
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def compareFeeRates(a, b) -> bool:
|
|
|
|
return abs(a - b) < 20
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def xmr_swap_alock_spend_tx_vsize() -> int:
|
|
|
|
return 147
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def txoType():
|
|
|
|
return CTxOut
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getExpectedSequence(lockType, lockVal):
|
|
|
|
assert (lockVal >= 1), 'Bad lockVal'
|
|
|
|
if lockType == TxLockTypes.SEQUENCE_LOCK_BLOCKS:
|
|
|
|
return lockVal
|
|
|
|
if lockType == TxLockTypes.SEQUENCE_LOCK_TIME:
|
|
|
|
secondsLocked = lockVal
|
|
|
|
# Ensure the locked time is never less than lockVal
|
|
|
|
if secondsLocked % (1 << SEQUENCE_LOCKTIME_GRANULARITY) != 0:
|
|
|
|
secondsLocked += (1 << SEQUENCE_LOCKTIME_GRANULARITY)
|
|
|
|
secondsLocked >>= SEQUENCE_LOCKTIME_GRANULARITY
|
|
|
|
return secondsLocked | SEQUENCE_LOCKTIME_TYPE_FLAG
|
|
|
|
raise ValueError('Unknown lock type')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def decodeSequence(lock_value):
|
|
|
|
# Return the raw value
|
|
|
|
if lock_value & SEQUENCE_LOCKTIME_TYPE_FLAG:
|
|
|
|
return (lock_value & SEQUENCE_LOCKTIME_MASK) << SEQUENCE_LOCKTIME_GRANULARITY
|
|
|
|
return lock_value & SEQUENCE_LOCKTIME_MASK
|
|
|
|
|
|
|
|
def __init__(self, coin_settings, network, swap_client=None):
|
|
|
|
super().__init__(network)
|
|
|
|
self._rpc_host = coin_settings.get('rpchost', '127.0.0.1')
|
|
|
|
self._rpcport = coin_settings['rpcport']
|
|
|
|
self._rpcauth = coin_settings['rpcauth']
|
|
|
|
self.rpc_callback = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
|
|
|
|
self.blocks_confirmed = coin_settings['blocks_confirmed']
|
|
|
|
self.setConfTarget(coin_settings['conf_target'])
|
|
|
|
self._use_segwit = coin_settings['use_segwit']
|
|
|
|
self._connection_type = coin_settings['connection_type']
|
|
|
|
self._sc = swap_client
|
|
|
|
self._log = self._sc.log if self._sc and self._sc.log else logging
|
|
|
|
|
|
|
|
def using_segwit(self):
|
|
|
|
return self._use_segwit
|
|
|
|
|
|
|
|
def get_connection_type(self):
|
|
|
|
return self._connection_type
|
|
|
|
|
|
|
|
def open_rpc(self, wallet=None):
|
|
|
|
return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host)
|
|
|
|
|
|
|
|
def json_request(self, rpc_conn, method, params):
|
|
|
|
try:
|
|
|
|
v = rpc_conn.json_request(method, params)
|
|
|
|
r = json.loads(v.decode('utf-8'))
|
|
|
|
except Exception as ex:
|
|
|
|
traceback.print_exc()
|
|
|
|
raise ValueError('RPC Server Error ' + str(ex))
|
|
|
|
|
|
|
|
if 'error' in r and r['error'] is not None:
|
|
|
|
raise ValueError('RPC error ' + str(r['error']))
|
|
|
|
|
|
|
|
return r['result']
|
|
|
|
|
|
|
|
def close_rpc(self, rpc_conn):
|
|
|
|
rpc_conn.close()
|
|
|
|
|
|
|
|
def setConfTarget(self, new_conf_target):
|
|
|
|
ensure(new_conf_target >= 1 and new_conf_target < 33, 'Invalid conf_target value')
|
|
|
|
self._conf_target = new_conf_target
|
|
|
|
|
|
|
|
def testDaemonRPC(self, with_wallet=True):
|
|
|
|
if with_wallet:
|
|
|
|
self.rpc_callback('getwalletinfo', [])
|
|
|
|
else:
|
|
|
|
self.rpc_callback('getblockchaininfo', [])
|
|
|
|
|
|
|
|
def getDaemonVersion(self):
|
|
|
|
return self.rpc_callback('getnetworkinfo')['version']
|
|
|
|
|
|
|
|
def getBlockchainInfo(self):
|
|
|
|
return self.rpc_callback('getblockchaininfo')
|
|
|
|
|
|
|
|
def getChainHeight(self):
|
|
|
|
return self.rpc_callback('getblockcount')
|
|
|
|
|
|
|
|
def getMempoolTx(self, txid):
|
|
|
|
return self.rpc_callback('getrawtransaction', [txid.hex()])
|
|
|
|
|
|
|
|
def getBlockHeaderFromHeight(self, height):
|
|
|
|
block_hash = self.rpc_callback('getblockhash', [height])
|
|
|
|
return self.rpc_callback('getblockheader', [block_hash])
|
|
|
|
|
|
|
|
def getBlockHeader(self, block_hash):
|
|
|
|
return self.rpc_callback('getblockheader', [block_hash])
|
|
|
|
|
|
|
|
def initialiseWallet(self, key_bytes):
|
|
|
|
key_wif = self.encodeKey(key_bytes)
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.rpc_callback('sethdseed', [True, key_wif])
|
|
|
|
except Exception as e:
|
|
|
|
# < 0.21: Cannot set a new HD seed while still in Initial Block Download.
|
|
|
|
self._log.error('sethdseed failed: {}'.format(str(e)))
|
|
|
|
|
|
|
|
def getWalletInfo(self):
|
|
|
|
return self.rpc_callback('getwalletinfo')
|
|
|
|
|
|
|
|
def walletRestoreHeight(self):
|
|
|
|
return self._restore_height
|
|
|
|
|
|
|
|
def getWalletRestoreHeight(self):
|
|
|
|
start_time = self.rpc_callback('getwalletinfo')['keypoololdest']
|
|
|
|
|
|
|
|
blockchaininfo = self.rpc_callback('getblockchaininfo')
|
|
|
|
best_block = blockchaininfo['bestblockhash']
|
|
|
|
|
|
|
|
chain_synced = round(blockchaininfo['verificationprogress'], 3)
|
|
|
|
if chain_synced < 1.0:
|
|
|
|
raise ValueError('{} chain isn\'t synced.'.format(self.coin_name()))
|
|
|
|
|
|
|
|
self._log.debug('Finding block at time: {}'.format(start_time))
|
|
|
|
|
|
|
|
rpc_conn = self.open_rpc()
|
|
|
|
try:
|
|
|
|
block_hash = best_block
|
|
|
|
while True:
|
|
|
|
block_header = self.json_request(rpc_conn, 'getblockheader', [block_hash])
|
|
|
|
if block_header['time'] < start_time:
|
|
|
|
return block_header['height']
|
|
|
|
block_hash = block_header['previousblockhash']
|
|
|
|
finally:
|
|
|
|
self.close_rpc(rpc_conn)
|
|
|
|
|
|
|
|
def getWalletSeedID(self):
|
|
|
|
return self.rpc_callback('getwalletinfo')['hdseedid']
|
|
|
|
|
|
|
|
def getNewAddress(self, use_segwit, label='swap_receive'):
|
|
|
|
args = [label]
|
|
|
|
if use_segwit:
|
|
|
|
args.append('bech32')
|
|
|
|
return self.rpc_callback('getnewaddress', args)
|
|
|
|
|
|
|
|
def get_fee_rate(self, conf_target=2):
|
|
|
|
try:
|
|
|
|
fee_rate = self.rpc_callback('estimatesmartfee', [conf_target])['feerate']
|
|
|
|
assert (fee_rate > 0.0), 'Non positive feerate'
|
|
|
|
return fee_rate, 'estimatesmartfee'
|
|
|
|
except Exception:
|
|
|
|
try:
|
|
|
|
fee_rate = self.rpc_callback('getwalletinfo')['paytxfee']
|
|
|
|
assert (fee_rate > 0.0), 'Non positive feerate'
|
|
|
|
return fee_rate, 'paytxfee'
|
|
|
|
except Exception:
|
|
|
|
return self.rpc_callback('getnetworkinfo')['relayfee'], 'relayfee'
|
|
|
|
|
|
|
|
def isSegwitAddress(self, address):
|
|
|
|
return address.startswith(self.chainparams_network()['hrp'] + '1')
|
|
|
|
|
|
|
|
def decodeAddress(self, address):
|
|
|
|
bech32_prefix = self.chainparams_network()['hrp']
|
|
|
|
if address.startswith(bech32_prefix + '1'):
|
|
|
|
return bytes(segwit_addr.decode(bech32_prefix, address)[1])
|
|
|
|
return decodeAddress(address)[1:]
|
|
|
|
|
|
|
|
def pubkey_to_segwit_address(self, pk):
|
|
|
|
bech32_prefix = self.chainparams_network()['hrp']
|
|
|
|
version = 0
|
|
|
|
pkh = hash160(pk)
|
|
|
|
return segwit_addr.encode(bech32_prefix, version, pkh)
|
|
|
|
|
|
|
|
def pkh_to_address(self, pkh):
|
|
|
|
# pkh is hash160(pk)
|
|
|
|
assert (len(pkh) == 20)
|
|
|
|
prefix = self.chainparams_network()['pubkey_address']
|
|
|
|
data = bytes((prefix,)) + pkh
|
|
|
|
checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest()
|
|
|
|
return b58encode(data + checksum[0:4])
|
|
|
|
|
|
|
|
def encode_p2wsh(self, script):
|
|
|
|
bech32_prefix = self.chainparams_network()['hrp']
|
|
|
|
version = 0
|
|
|
|
program = script[2:] # strip version and length
|
|
|
|
return segwit_addr.encode(bech32_prefix, version, program)
|
|
|
|
|
|
|
|
def encode_p2sh(self, script):
|
|
|
|
return pubkeyToAddress(self.chainparams_network()['script_address'], script)
|
|
|
|
|
|
|
|
def pubkey_to_address(self, pk):
|
|
|
|
assert (len(pk) == 33)
|
|
|
|
return self.pkh_to_address(hash160(pk))
|
|
|
|
|
|
|
|
def getNewSecretKey(self):
|
|
|
|
return getSecretInt()
|
|
|
|
|
|
|
|
def getPubkey(self, privkey):
|
|
|
|
return PublicKey.from_secret(privkey).format()
|
|
|
|
|
|
|
|
def getAddressHashFromKey(self, key):
|
|
|
|
pk = self.getPubkey(key)
|
|
|
|
return hash160(pk)
|
|
|
|
|
|
|
|
def verifyKey(self, k):
|
|
|
|
i = b2i(k)
|
|
|
|
return (i < ep.o and i > 0)
|
|
|
|
|
|
|
|
def verifyPubkey(self, pubkey_bytes):
|
|
|
|
return verify_secp256k1_point(pubkey_bytes)
|
|
|
|
|
|
|
|
def encodeKey(self, key_bytes):
|
|
|
|
wif_prefix = self.chainparams_network()['key_prefix']
|
|
|
|
return toWIF(wif_prefix, key_bytes)
|
|
|
|
|
|
|
|
def encodePubkey(self, pk):
|
|
|
|
return pointToCPK(pk)
|
|
|
|
|
|
|
|
def decodePubkey(self, pke):
|
|
|
|
return CPKToPoint(pke)
|
|
|
|
|
|
|
|
def decodeKey(self, k):
|
|
|
|
return decodeWif(k)
|
|
|
|
|
|
|
|
def sumKeys(self, ka, kb):
|
|
|
|
# TODO: Add to coincurve
|
|
|
|
return i2b((b2i(ka) + b2i(kb)) % ep.o)
|
|
|
|
|
|
|
|
def sumPubkeys(self, Ka, Kb):
|
|
|
|
return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format()
|
|
|
|
|
|
|
|
def getScriptForPubkeyHash(self, pkh):
|
|
|
|
return CScript([OP_0, pkh])
|
|
|
|
|
|
|
|
def extractScriptLockScriptValues(self, script_bytes):
|
|
|
|
script_len = len(script_bytes)
|
|
|
|
ensure(script_len == 71, 'Bad script length')
|
|
|
|
o = 0
|
|
|
|
ensure_op(script_bytes[o] == OP_2)
|
|
|
|
ensure_op(script_bytes[o + 1] == 33)
|
|
|
|
o += 2
|
|
|
|
pk1 = script_bytes[o: o + 33]
|
|
|
|
o += 33
|
|
|
|
ensure_op(script_bytes[o] == 33)
|
|
|
|
o += 1
|
|
|
|
pk2 = script_bytes[o: o + 33]
|
|
|
|
o += 33
|
|
|
|
ensure_op(script_bytes[o] == OP_2)
|
|
|
|
ensure_op(script_bytes[o + 1] == OP_CHECKMULTISIG)
|
|
|
|
|
|
|
|
return pk1, pk2
|
|
|
|
|
|
|
|
def genScriptLockTxScript(self, Kal, Kaf):
|
|
|
|
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
|
|
|
|
Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf)
|
|
|
|
|
|
|
|
return CScript([2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG)])
|
|
|
|
|
|
|
|
def createScriptLockTx(self, value, Kal, Kaf, vkbv=None):
|
|
|
|
script = self.genScriptLockTxScript(Kal, Kaf)
|
|
|
|
tx = CTransaction()
|
|
|
|
tx.nVersion = self.txVersion()
|
|
|
|
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
|
|
|
|
|
|
|
|
return tx.serialize(), script
|
|
|
|
|
|
|
|
def fundScriptLockTx(self, tx_bytes, feerate, vkbv=None):
|
|
|
|
return self.fundTx(tx_bytes, feerate)
|
|
|
|
|
|
|
|
def extractScriptLockRefundScriptValues(self, script_bytes):
|
|
|
|
script_len = len(script_bytes)
|
|
|
|
ensure(script_len > 73, 'Bad script length')
|
|
|
|
ensure_op(script_bytes[0] == OP_IF)
|
|
|
|
ensure_op(script_bytes[1] == OP_2)
|
|
|
|
ensure_op(script_bytes[2] == 33)
|
|
|
|
pk1 = script_bytes[3: 3 + 33]
|
|
|
|
ensure_op(script_bytes[36] == 33)
|
|
|
|
pk2 = script_bytes[37: 37 + 33]
|
|
|
|
ensure_op(script_bytes[70] == OP_2)
|
|
|
|
ensure_op(script_bytes[71] == OP_CHECKMULTISIG)
|
|
|
|
ensure_op(script_bytes[72] == OP_ELSE)
|
|
|
|
o = 73
|
|
|
|
csv_val, nb = decodeScriptNum(script_bytes, o)
|
|
|
|
o += nb
|
|
|
|
|
|
|
|
ensure(script_len == o + 5 + 33, 'Bad script length') # Fails if script too long
|
|
|
|
ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY)
|
|
|
|
o += 1
|
|
|
|
ensure_op(script_bytes[o] == OP_DROP)
|
|
|
|
o += 1
|
|
|
|
ensure_op(script_bytes[o] == 33)
|
|
|
|
o += 1
|
|
|
|
pk3 = script_bytes[o: o + 33]
|
|
|
|
o += 33
|
|
|
|
ensure_op(script_bytes[o] == OP_CHECKSIG)
|
|
|
|
o += 1
|
|
|
|
ensure_op(script_bytes[o] == OP_ENDIF)
|
|
|
|
|
|
|
|
return pk1, pk2, csv_val, pk3
|
|
|
|
|
|
|
|
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val):
|
|
|
|
|
|
|
|
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
|
|
|
|
Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf)
|
|
|
|
|
|
|
|
return CScript([
|
|
|
|
CScriptOp(OP_IF),
|
|
|
|
2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG),
|
|
|
|
CScriptOp(OP_ELSE),
|
|
|
|
csv_val, CScriptOp(OP_CHECKSEQUENCEVERIFY), CScriptOp(OP_DROP),
|
|
|
|
Kaf_enc, CScriptOp(OP_CHECKSIG),
|
|
|
|
CScriptOp(OP_ENDIF)])
|
|
|
|
|
|
|
|
def createScriptLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv=None):
|
|
|
|
tx_lock = CTransaction()
|
|
|
|
tx_lock = FromHex(tx_lock, tx_lock_bytes.hex())
|
|
|
|
|
|
|
|
output_script = CScript([OP_0, hashlib.sha256(script_lock).digest()])
|
|
|
|
locked_n = findOutput(tx_lock, output_script)
|
|
|
|
ensure(locked_n is not None, 'Output not found in tx')
|
|
|
|
locked_coin = tx_lock.vout[locked_n].nValue
|
|
|
|
|
|
|
|
tx_lock.rehash()
|
|
|
|
tx_lock_id_int = tx_lock.sha256
|
|
|
|
|
|
|
|
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
|
|
|
|
tx = CTransaction()
|
|
|
|
tx.nVersion = self.txVersion()
|
|
|
|
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n), nSequence=lock1_value))
|
|
|
|
tx.vout.append(self.txoType()(locked_coin, CScript([OP_0, hashlib.sha256(refund_script).digest()])))
|
|
|
|
|
|
|
|
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
|
|
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
|
|
|
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
|
|
|
pay_fee = int(tx_fee_rate * vsize // 1000)
|
|
|
|
tx.vout[0].nValue = locked_coin - pay_fee
|
|
|
|
|
|
|
|
tx.rehash()
|
|
|
|
self._log.info('createScriptLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
|
|
|
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
|
|
|
|
|
|
|
return tx.serialize(), refund_script, tx.vout[0].nValue
|
|
|
|
|
|
|
|
def createScriptLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv=None):
|
|
|
|
# Returns the coinA locked coin to the leader
|
|
|
|
# The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey
|
|
|
|
# If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower
|
|
|
|
|
|
|
|
tx_lock_refund = self.loadTx(tx_lock_refund_bytes)
|
|
|
|
|
|
|
|
output_script = CScript([OP_0, hashlib.sha256(script_lock_refund).digest()])
|
|
|
|
locked_n = findOutput(tx_lock_refund, output_script)
|
|
|
|
ensure(locked_n is not None, 'Output not found in tx')
|
|
|
|
locked_coin = tx_lock_refund.vout[locked_n].nValue
|
|
|
|
|
|
|
|
tx_lock_refund.rehash()
|
|
|
|
tx_lock_refund_hash_int = tx_lock_refund.sha256
|
|
|
|
|
|
|
|
tx = CTransaction()
|
|
|
|
tx.nVersion = self.txVersion()
|
|
|
|
tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n), nSequence=0))
|
|
|
|
|
|
|
|
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to)))
|
|
|
|
|
|
|
|
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund)
|
|
|
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
|
|
|
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
|
|
|
pay_fee = int(tx_fee_rate * vsize // 1000)
|
|
|
|
tx.vout[0].nValue = locked_coin - pay_fee
|
|
|
|
|
|
|
|
tx.rehash()
|
|
|
|
self._log.info('createScriptLockRefundSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
|
|
|
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
|
|
|
|
|
|
|
return tx.serialize()
|
|
|
|
|
|
|
|
def createScriptLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None):
|
|
|
|
# lock refund swipe tx
|
|
|
|
# Sends the coinA locked coin to the follower
|
|
|
|
|
|
|
|
tx_lock_refund = self.loadTx(tx_lock_refund_bytes)
|
|
|
|
|
|
|
|
output_script = CScript([OP_0, hashlib.sha256(script_lock_refund).digest()])
|
|
|
|
locked_n = findOutput(tx_lock_refund, output_script)
|
|
|
|
ensure(locked_n is not None, 'Output not found in tx')
|
|
|
|
locked_coin = tx_lock_refund.vout[locked_n].nValue
|
|
|
|
|
|
|
|
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund)
|
|
|
|
|
|
|
|
tx_lock_refund.rehash()
|
|
|
|
tx_lock_refund_hash_int = tx_lock_refund.sha256
|
|
|
|
|
|
|
|
tx = CTransaction()
|
|
|
|
tx.nVersion = self.txVersion()
|
|
|
|
tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n), nSequence=lock2_value))
|
|
|
|
|
|
|
|
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
|
|
|
|
|
|
|
|
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
|
|
|
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
|
|
|
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
|
|
|
pay_fee = int(tx_fee_rate * vsize // 1000)
|
|
|
|
tx.vout[0].nValue = locked_coin - pay_fee
|
|
|
|
|
|
|
|
tx.rehash()
|
|
|
|
self._log.info('createScriptLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
|
|
|
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
|
|
|
|
|
|
|
return tx.serialize()
|
|
|
|
|
|
|
|
def createScriptLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None):
|
|
|
|
tx_lock = self.loadTx(tx_lock_bytes)
|
|
|
|
output_script = CScript([OP_0, hashlib.sha256(script_lock).digest()])
|
|
|
|
locked_n = findOutput(tx_lock, output_script)
|
|
|
|
ensure(locked_n is not None, 'Output not found in tx')
|
|
|
|
locked_coin = tx_lock.vout[locked_n].nValue
|
|
|
|
|
|
|
|
tx_lock.rehash()
|
|
|
|
tx_lock_id_int = tx_lock.sha256
|
|
|
|
|
|
|
|
tx = CTransaction()
|
|
|
|
tx.nVersion = self.txVersion()
|
|
|
|
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n)))
|
|
|
|
|
|
|
|
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
|
|
|
|
|
|
|
|
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
|
|
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
|
|
|
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
|
|
|
pay_fee = int(tx_fee_rate * vsize // 1000)
|
|
|
|
tx.vout[0].nValue = locked_coin - pay_fee
|
|
|
|
|
|
|
|
tx.rehash()
|
|
|
|
self._log.info('createScriptLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
|
|
|
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
|
|
|
|
|
|
|
return tx.serialize()
|
|
|
|
|
|
|
|
def verifyLockTx(self, tx_bytes, script_out,
|
|
|
|
swap_value,
|
|
|
|
Kal, Kaf,
|
|
|
|
feerate,
|
|
|
|
check_lock_tx_inputs, vkbv=None):
|
|
|
|
# Verify:
|
|
|
|
#
|
|
|
|
|
|
|
|
# Not necessary to check the lock txn is mineable, as protocol will wait for it to confirm
|
|
|
|
# However by checking early we can avoid wasting time processing unmineable txns
|
|
|
|
# Check fee is reasonable
|
|
|
|
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
txid = self.getTxid(tx)
|
|
|
|
self._log.info('Verifying lock tx: {}.'.format(b2h(txid)))
|
|
|
|
|
|
|
|
ensure(tx.nVersion == self.txVersion(), 'Bad version')
|
|
|
|
ensure(tx.nLockTime == 0, 'Bad nLockTime') # TODO match txns created by cores
|
|
|
|
|
|
|
|
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
|
|
|
|
locked_n = findOutput(tx, script_pk)
|
|
|
|
ensure(locked_n is not None, 'Output not found in tx')
|
|
|
|
locked_coin = tx.vout[locked_n].nValue
|
|
|
|
|
|
|
|
# Check value
|
|
|
|
ensure(locked_coin == swap_value, 'Bad locked value')
|
|
|
|
|
|
|
|
# Check script
|
|
|
|
A, B = self.extractScriptLockScriptValues(script_out)
|
|
|
|
ensure(A == Kal, 'Bad script pubkey')
|
|
|
|
ensure(B == Kaf, 'Bad script pubkey')
|
|
|
|
|
|
|
|
if check_lock_tx_inputs:
|
|
|
|
# TODO: Check that inputs are unspent
|
|
|
|
# Verify fee rate
|
|
|
|
inputs_value = 0
|
|
|
|
add_bytes = 0
|
|
|
|
add_witness_bytes = getCompactSizeLen(len(tx.vin))
|
|
|
|
for pi in tx.vin:
|
|
|
|
ptx = self.rpc_callback('getrawtransaction', [i2h(pi.prevout.hash), True])
|
|
|
|
prevout = ptx['vout'][pi.prevout.n]
|
|
|
|
inputs_value += make_int(prevout['value'])
|
|
|
|
|
|
|
|
prevout_type = prevout['scriptPubKey']['type']
|
|
|
|
if prevout_type == 'witness_v0_keyhash':
|
|
|
|
add_witness_bytes += 107 # sig 72, pk 33 and 2 size bytes
|
|
|
|
add_witness_bytes += getCompactSizeLen(107)
|
|
|
|
else:
|
|
|
|
# Assume P2PKH, TODO more types
|
|
|
|
add_bytes += 107 # OP_PUSH72 <ecdsa_signature> OP_PUSH33 <public_key>
|
|
|
|
|
|
|
|
outputs_value = 0
|
|
|
|
for txo in tx.vout:
|
|
|
|
outputs_value += txo.nValue
|
|
|
|
fee_paid = inputs_value - outputs_value
|
|
|
|
assert (fee_paid > 0)
|
|
|
|
|
|
|
|
vsize = self.getTxVSize(tx, add_bytes, add_witness_bytes)
|
|
|
|
fee_rate_paid = fee_paid * 1000 // vsize
|
|
|
|
|
|
|
|
self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_coin, vsize, fee_rate_paid)
|
|
|
|
|
|
|
|
if not self.compareFeeRates(fee_rate_paid, feerate):
|
|
|
|
self._log.warning('feerate paid doesn\'t match expected: %ld, %ld', fee_rate_paid, feerate)
|
|
|
|
# TODO: Display warning to user
|
|
|
|
|
|
|
|
return txid, locked_n
|
|
|
|
|
|
|
|
def verifyLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out,
|
|
|
|
prevout_id, prevout_n, prevout_seq, prevout_script,
|
|
|
|
Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv=None):
|
|
|
|
# Verify:
|
|
|
|
# Must have only one input with correct prevout and sequence
|
|
|
|
# Must have only one output to the p2wsh of the lock refund script
|
|
|
|
# Output value must be locked_coin - lock tx fee
|
|
|
|
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
txid = self.getTxid(tx)
|
|
|
|
self._log.info('Verifying lock refund tx: {}.'.format(b2h(txid)))
|
|
|
|
|
|
|
|
ensure(tx.nVersion == self.txVersion(), 'Bad version')
|
|
|
|
ensure(tx.nLockTime == 0, 'nLockTime not 0')
|
|
|
|
ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
|
|
|
|
|
|
|
|
ensure(tx.vin[0].nSequence == prevout_seq, 'Bad input nSequence')
|
|
|
|
ensure(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty')
|
|
|
|
ensure(tx.vin[0].prevout.hash == b2i(prevout_id) and tx.vin[0].prevout.n == prevout_n, 'Input prevout mismatch')
|
|
|
|
|
|
|
|
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
|
|
|
|
|
|
|
|
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
|
|
|
|
locked_n = findOutput(tx, script_pk)
|
|
|
|
ensure(locked_n is not None, 'Output not found in tx')
|
|
|
|
locked_coin = tx.vout[locked_n].nValue
|
|
|
|
|
|
|
|
# Check script and values
|
|
|
|
A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out)
|
|
|
|
ensure(A == Kal, 'Bad script pubkey')
|
|
|
|
ensure(B == Kaf, 'Bad script pubkey')
|
|
|
|
ensure(csv_val == csv_val_expect, 'Bad script csv value')
|
|
|
|
ensure(C == Kaf, 'Bad script pubkey')
|
|
|
|
|
|
|
|
fee_paid = swap_value - locked_coin
|
|
|
|
assert (fee_paid > 0)
|
|
|
|
|
|
|
|
dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script)
|
|
|
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
|
|
|
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
|
|
|
fee_rate_paid = fee_paid * 1000 // vsize
|
|
|
|
|
|
|
|
self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_coin, vsize, fee_rate_paid)
|
|
|
|
|
|
|
|
if not self.compareFeeRates(fee_rate_paid, feerate):
|
|
|
|
raise ValueError('Bad fee rate, expected: {}'.format(feerate))
|
|
|
|
|
|
|
|
return txid, locked_coin, locked_n
|
|
|
|
|
|
|
|
def verifyLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes,
|
|
|
|
lock_refund_tx_id, prevout_script,
|
|
|
|
Kal,
|
|
|
|
prevout_n, prevout_value, feerate, vkbv=None):
|
|
|
|
# Verify:
|
|
|
|
# Must have only one input with correct prevout (n is always 0) and sequence
|
|
|
|
# Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
txid = self.getTxid(tx)
|
|
|
|
self._log.info('Verifying lock refund spend tx: {}.'.format(b2h(txid)))
|
|
|
|
|
|
|
|
ensure(tx.nVersion == self.txVersion(), 'Bad version')
|
|
|
|
ensure(tx.nLockTime == 0, 'nLockTime not 0')
|
|
|
|
ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
|
|
|
|
|
|
|
|
ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence')
|
|
|
|
ensure(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty')
|
|
|
|
ensure(tx.vin[0].prevout.hash == b2i(lock_refund_tx_id) and tx.vin[0].prevout.n == 0, 'Input prevout mismatch')
|
|
|
|
|
|
|
|
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
|
|
|
|
|
|
|
|
# Destination doesn't matter to the follower
|
|
|
|
'''
|
|
|
|
p2wpkh = CScript([OP_0, hash160(Kal)])
|
|
|
|
locked_n = findOutput(tx, p2wpkh)
|
|
|
|
ensure(locked_n is not None, 'Output not found in lock refund spend tx')
|
|
|
|
'''
|
|
|
|
tx_value = tx.vout[0].nValue
|
|
|
|
|
|
|
|
fee_paid = prevout_value - tx_value
|
|
|
|
assert (fee_paid > 0)
|
|
|
|
|
|
|
|
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script)
|
|
|
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
|
|
|
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
|
|
|
fee_rate_paid = fee_paid * 1000 // vsize
|
|
|
|
|
|
|
|
self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', tx_value, vsize, fee_rate_paid)
|
|
|
|
|
|
|
|
if not self.compareFeeRates(fee_rate_paid, feerate):
|
|
|
|
raise ValueError('Bad fee rate, expected: {}'.format(feerate))
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def verifyLockSpendTx(self, tx_bytes,
|
|
|
|
lock_tx_bytes, lock_tx_script,
|
|
|
|
a_pkhash_f, feerate, vkbv=None):
|
|
|
|
# Verify:
|
|
|
|
# Must have only one input with correct prevout (n is always 0) and sequence
|
|
|
|
# Must have only one output with destination and amount
|
|
|
|
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
txid = self.getTxid(tx)
|
|
|
|
self._log.info('Verifying lock spend tx: {}.'.format(b2h(txid)))
|
|
|
|
|
|
|
|
ensure(tx.nVersion == self.txVersion(), 'Bad version')
|
|
|
|
ensure(tx.nLockTime == 0, 'nLockTime not 0')
|
|
|
|
ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
|
|
|
|
|
|
|
|
lock_tx = self.loadTx(lock_tx_bytes)
|
|
|
|
lock_tx_id = self.getTxid(lock_tx)
|
|
|
|
|
|
|
|
output_script = CScript([OP_0, hashlib.sha256(lock_tx_script).digest()])
|
|
|
|
locked_n = findOutput(lock_tx, output_script)
|
|
|
|
ensure(locked_n is not None, 'Output not found in tx')
|
|
|
|
locked_coin = lock_tx.vout[locked_n].nValue
|
|
|
|
|
|
|
|
ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence')
|
|
|
|
ensure(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty')
|
|
|
|
ensure(tx.vin[0].prevout.hash == b2i(lock_tx_id) and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch')
|
|
|
|
|
|
|
|
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
|
|
|
|
p2wpkh = self.getScriptForPubkeyHash(a_pkhash_f)
|
|
|
|
ensure(tx.vout[0].scriptPubKey == p2wpkh, 'Bad output destination')
|
|
|
|
|
|
|
|
# The value of the lock tx output should already be verified, if the fee is as expected the difference will be the correct amount
|
|
|
|
fee_paid = locked_coin - tx.vout[0].nValue
|
|
|
|
assert (fee_paid > 0)
|
|
|
|
|
|
|
|
dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script)
|
|
|
|
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
|
|
|
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
|
|
|
fee_rate_paid = fee_paid * 1000 // vsize
|
|
|
|
|
|
|
|
self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', tx.vout[0].nValue, vsize, fee_rate_paid)
|
|
|
|
|
|
|
|
if not self.compareFeeRates(fee_rate_paid, feerate):
|
|
|
|
raise ValueError('Bad fee rate, expected: {}'.format(feerate))
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def signTx(self, key_bytes, tx_bytes, input_n, prevout_script, prevout_value):
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
|
|
|
|
|
|
|
|
eck = PrivateKey(key_bytes)
|
|
|
|
return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,))
|
|
|
|
|
|
|
|
def signTxOtVES(self, key_sign, pubkey_encrypt, tx_bytes, input_n, prevout_script, prevout_value):
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
|
|
|
|
|
|
|
|
return ecdsaotves_enc_sign(key_sign, pubkey_encrypt, sig_hash)
|
|
|
|
|
|
|
|
def verifyTxOtVES(self, tx_bytes, ct, Ks, Ke, input_n, prevout_script, prevout_value):
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
|
|
|
|
return ecdsaotves_enc_verify(Ks, Ke, sig_hash, ct)
|
|
|
|
|
|
|
|
def decryptOtVES(self, k, esig):
|
|
|
|
return ecdsaotves_dec_sig(k, esig) + bytes((SIGHASH_ALL,))
|
|
|
|
|
|
|
|
def verifyTxSig(self, tx_bytes, sig, K, input_n, prevout_script, prevout_value):
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
|
|
|
|
|
|
|
|
pubkey = PublicKey(K)
|
|
|
|
return pubkey.verify(sig[: -1], sig_hash, hasher=None) # Pop the hashtype byte
|
|
|
|
|
|
|
|
def verifySig(self, pubkey, signed_hash, sig):
|
|
|
|
pubkey = PublicKey(pubkey)
|
|
|
|
return pubkey.verify(sig, signed_hash, hasher=None)
|
|
|
|
|
|
|
|
def fundTx(self, tx, feerate):
|
|
|
|
feerate_str = self.format_amount(feerate)
|
|
|
|
# TODO: unlock unspents if bid cancelled
|
|
|
|
options = {
|
|
|
|
'lockUnspents': True,
|
|
|
|
'feeRate': feerate_str,
|
|
|
|
}
|
|
|
|
rv = self.rpc_callback('fundrawtransaction', [tx.hex(), options])
|
|
|
|
return bytes.fromhex(rv['hex'])
|
|
|
|
|
|
|
|
def listInputs(self, tx_bytes):
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
|
|
|
|
all_locked = self.rpc_callback('listlockunspent')
|
|
|
|
inputs = []
|
|
|
|
for pi in tx.vin:
|
|
|
|
txid_hex = i2h(pi.prevout.hash)
|
|
|
|
islocked = any([txid_hex == a['txid'] and pi.prevout.n == a['vout'] for a in all_locked])
|
|
|
|
inputs.append({'txid': txid_hex, 'vout': pi.prevout.n, 'islocked': islocked})
|
|
|
|
return inputs
|
|
|
|
|
|
|
|
def unlockInputs(self, tx_bytes):
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
|
|
|
|
inputs = []
|
|
|
|
for pi in tx.vin:
|
|
|
|
inputs.append({'txid': i2h(pi.prevout.hash), 'vout': pi.prevout.n})
|
|
|
|
self.rpc_callback('lockunspent', [True, inputs])
|
|
|
|
|
|
|
|
def signTxWithWallet(self, tx):
|
|
|
|
rv = self.rpc_callback('signrawtransactionwithwallet', [tx.hex()])
|
|
|
|
return bytes.fromhex(rv['hex'])
|
|
|
|
|
|
|
|
def publishTx(self, tx):
|
|
|
|
return self.rpc_callback('sendrawtransaction', [tx.hex()])
|
|
|
|
|
|
|
|
def encodeTx(self, tx):
|
|
|
|
return tx.serialize()
|
|
|
|
|
|
|
|
def loadTx(self, tx_bytes):
|
|
|
|
# Load tx from bytes to internal representation
|
|
|
|
tx = CTransaction()
|
|
|
|
tx.deserialize(BytesIO(tx_bytes))
|
|
|
|
return tx
|
|
|
|
|
|
|
|
def getTxid(self, tx):
|
|
|
|
if isinstance(tx, str):
|
|
|
|
tx = bytes.fromhex(tx)
|
|
|
|
if isinstance(tx, bytes):
|
|
|
|
tx = self.loadTx(tx)
|
|
|
|
tx.rehash()
|
|
|
|
return i2b(tx.sha256)
|
|
|
|
|
|
|
|
def getTxOutputPos(self, tx, script):
|
|
|
|
if isinstance(tx, bytes):
|
|
|
|
tx = self.loadTx(tx)
|
|
|
|
script_pk = CScript([OP_0, hashlib.sha256(script).digest()])
|
|
|
|
return findOutput(tx, script_pk)
|
|
|
|
|
|
|
|
def getPubkeyHash(self, K):
|
|
|
|
return hash160(self.encodePubkey(K))
|
|
|
|
|
|
|
|
def getScriptDest(self, script):
|
|
|
|
return CScript([OP_0, hashlib.sha256(script).digest()])
|
|
|
|
|
|
|
|
def getPkDest(self, K):
|
|
|
|
return self.getScriptForPubkeyHash(self.getPubkeyHash(K))
|
|
|
|
|
|
|
|
def scanTxOutset(self, dest):
|
|
|
|
return self.rpc_callback('scantxoutset', ['start', ['raw({})'.format(dest.hex())]])
|
|
|
|
|
|
|
|
def getTransaction(self, txid):
|
|
|
|
try:
|
|
|
|
return bytes.fromhex(self.rpc_callback('getrawtransaction', [txid.hex()]))
|
|
|
|
except Exception as ex:
|
|
|
|
# TODO: filter errors
|
|
|
|
return None
|
|
|
|
|
|
|
|
def getWalletTransaction(self, txid):
|
|
|
|
try:
|
|
|
|
return bytes.fromhex(self.rpc_callback('gettransaction', [txid.hex()]))
|
|
|
|
except Exception as ex:
|
|
|
|
# TODO: filter errors
|
|
|
|
return None
|
|
|
|
|
|
|
|
def setTxSignature(self, tx_bytes, stack):
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
tx.wit.vtxinwit.clear()
|
|
|
|
tx.wit.vtxinwit.append(CTxInWitness())
|
|
|
|
tx.wit.vtxinwit[0].scriptWitness.stack = stack
|
|
|
|
return tx.serialize()
|
|
|
|
|
|
|
|
def stripTxSignature(self, tx_bytes):
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
tx.wit.vtxinwit.clear()
|
|
|
|
return tx.serialize()
|
|
|
|
|
|
|
|
def extractLeaderSig(self, tx_bytes):
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
return tx.wit.vtxinwit[0].scriptWitness.stack[1]
|
|
|
|
|
|
|
|
def extractFollowerSig(self, tx_bytes):
|
|
|
|
tx = self.loadTx(tx_bytes)
|
|
|
|
return tx.wit.vtxinwit[0].scriptWitness.stack[2]
|
|
|
|
|
|
|
|
def createBLockTx(self, Kbs, output_amount):
|
|
|
|
tx = CTransaction()
|
|
|
|
tx.nVersion = self.txVersion()
|
|
|
|
p2wpkh = self.getPkDest(Kbs)
|
|
|
|
tx.vout.append(self.txoType()(output_amount, p2wpkh))
|
|
|
|
return tx.serialize()
|
|
|
|
|
|
|
|
def encodeSharedAddress(self, Kbv, Kbs):
|
|
|
|
return self.pubkey_to_segwit_address(Kbs)
|
|
|
|
|
|
|
|
def publishBLockTx(self, Kbv, Kbs, output_amount, feerate):
|
|
|
|
b_lock_tx = self.createBLockTx(Kbs, output_amount)
|
|
|
|
|
|
|
|
b_lock_tx = self.fundTx(b_lock_tx, feerate)
|
|
|
|
b_lock_tx_id = self.getTxid(b_lock_tx)
|
|
|
|
b_lock_tx = self.signTxWithWallet(b_lock_tx)
|
|
|
|
|
|
|
|
return self.publishTx(b_lock_tx)
|
|
|
|
|
|
|
|
def recoverEncKey(self, esig, sig, K):
|
|
|
|
return ecdsaotves_rec_enc_key(K, esig, sig[:-1]) # Strip sighash type
|
|
|
|
|
|
|
|
def getTxVSize(self, tx, add_bytes=0, add_witness_bytes=0):
|
|
|
|
wsf = self.witnessScaleFactor()
|
|
|
|
len_full = len(tx.serialize_with_witness()) + add_bytes + add_witness_bytes
|
|
|
|
len_nwit = len(tx.serialize_without_witness()) + add_bytes
|
|
|
|
weight = len_nwit * (wsf - 1) + len_full
|
|
|
|
return (weight + wsf - 1) // wsf
|
|
|
|
|
|
|
|
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender):
|
|
|
|
raw_dest = self.getPkDest(Kbs)
|
|
|
|
|
|
|
|
rv = self.scanTxOutset(raw_dest)
|
|
|
|
print('scanTxOutset', dumpj(rv))
|
|
|
|
|
|
|
|
for utxo in rv['unspents']:
|
|
|
|
if 'height' in utxo and utxo['height'] > 0 and rv['height'] - utxo['height'] > cb_block_confirmed:
|
|
|
|
if self.make_int(utxo['amount']) != cb_swap_value:
|
|
|
|
self._log.warning('Found output to lock tx pubkey of incorrect value: %s', str(utxo['amount']))
|
|
|
|
else:
|
|
|
|
return {'txid': utxo['txid'], 'vout': utxo['vout'], 'amount': utxo['amount'], 'height': utxo['height']}
|
|
|
|
return None
|
|
|
|
|
|
|
|
def waitForLockTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed):
|
|
|
|
raw_dest = self.getPkDest(Kbs)
|
|
|
|
|
|
|
|
for i in range(20):
|
|
|
|
time.sleep(1)
|
|
|
|
rv = self.scanTxOutset(raw_dest)
|
|
|
|
print('scanTxOutset', dumpj(rv))
|
|
|
|
|
|
|
|
for utxo in rv['unspents']:
|
|
|
|
if 'height' in utxo and utxo['height'] > 0 and rv['height'] - utxo['height'] > cb_block_confirmed:
|
|
|
|
|
|
|
|
if self.make_int(utxo['amount']) != cb_swap_value:
|
|
|
|
self._log.warning('Found output to lock tx pubkey of incorrect value: %s', str(utxo['amount']))
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height):
|
|
|
|
raise ValueError('TODO')
|
|
|
|
|
|
|
|
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index=False):
|
|
|
|
# Add watchonly address and rescan if required
|
|
|
|
addr_info = self.rpc_callback('getaddressinfo', [dest_address])
|
|
|
|
if not addr_info['iswatchonly']:
|
|
|
|
ro = self.rpc_callback('importaddress', [dest_address, 'bid', False])
|
|
|
|
self._log.info('Imported watch-only addr: {}'.format(dest_address))
|
|
|
|
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
|
|
|
|
self.rpc_callback('rescanblockchain', [rescan_from])
|
|
|
|
|
|
|
|
return_txid = True if txid is None else False
|
|
|
|
if txid is None:
|
|
|
|
txns = self.rpc_callback('listunspent', [0, 9999999, [dest_address, ]])
|
|
|
|
|
|
|
|
for tx in txns:
|
|
|
|
if self.make_int(tx['amount']) == bid_amount:
|
|
|
|
txid = bytes.fromhex(tx['txid'])
|
|
|
|
break
|
|
|
|
|
|
|
|
if txid is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
try:
|
|
|
|
tx = self.rpc_callback('gettransaction', [txid.hex()])
|
|
|
|
|
|
|
|
block_height = 0
|
|
|
|
if 'blockhash' in tx:
|
|
|
|
block_header = self.rpc_callback('getblockheader', [tx['blockhash']])
|
|
|
|
block_height = block_header['height']
|
|
|
|
|
|
|
|
rv = {
|
|
|
|
'depth': 0 if 'confirmations' not in tx else tx['confirmations'],
|
|
|
|
'height': block_height}
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e))
|
|
|
|
return None
|
|
|
|
|
|
|
|
if find_index:
|
|
|
|
tx_obj = self.rpc_callback('decoderawtransaction', [tx['hex']])
|
|
|
|
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
|
|
|
|
|
|
|
|
if return_txid:
|
|
|
|
rv['txid'] = txid.hex()
|
|
|
|
|
|
|
|
return rv
|
|
|
|
|
|
|
|
def getOutput(self, txid, dest_script, expect_value, xmr_swap=None):
|
|
|
|
# TODO: Use getrawtransaction if txindex is active
|
|
|
|
utxos = self.rpc_callback('scantxoutset', ['start', ['raw({})'.format(dest_script.hex())]])
|
|
|
|
if 'height' in utxos: # chain_height not returned by v18 codebase
|
|
|
|
chain_height = utxos['height']
|
|
|
|
else:
|
|
|
|
chain_height = self.getChainHeight()
|
|
|
|
rv = []
|
|
|
|
for utxo in utxos['unspents']:
|
|
|
|
if txid and txid.hex() != utxo['txid']:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if expect_value != self.make_int(utxo['amount']):
|
|
|
|
continue
|
|
|
|
|
|
|
|
rv.append({
|
|
|
|
'depth': 0 if 'height' not in utxo else (chain_height - utxo['height']) + 1,
|
|
|
|
'height': 0 if 'height' not in utxo else utxo['height'],
|
|
|
|
'amount': self.make_int(utxo['amount']),
|
|
|
|
'txid': utxo['txid'],
|
|
|
|
'vout': utxo['vout']})
|
|
|
|
return rv, chain_height
|
|
|
|
|
|
|
|
def withdrawCoin(self, value, addr_to, subfee):
|
|
|
|
params = [addr_to, value, '', '', subfee, True, self._conf_target]
|
|
|
|
return self.rpc_callback('sendtoaddress', params)
|
|
|
|
|
|
|
|
def signCompact(self, k, message):
|
|
|
|
message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest()
|
|
|
|
|
|
|
|
privkey = PrivateKey(k)
|
|
|
|
return privkey.sign_recoverable(message_hash, hasher=None)[:64]
|
|
|
|
|
|
|
|
def verifyCompact(self, K, message, sig):
|
|
|
|
message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest()
|
|
|
|
pubkey = PublicKey(K)
|
|
|
|
rv = pubkey.verify_compact(sig, message_hash, hasher=None)
|
|
|
|
assert (rv is True)
|
|
|
|
|
|
|
|
def verifyMessage(self, address, message, signature, message_magic=None) -> bool:
|
|
|
|
if message_magic is None:
|
|
|
|
message_magic = self.chainparams_network()['message_magic']
|
|
|
|
|
|
|
|
message_bytes = SerialiseNumCompact(len(message_magic)) + bytes(message_magic, 'utf-8') + SerialiseNumCompact(len(message)) + bytes(message, 'utf-8')
|
|
|
|
message_hash = hashlib.sha256(hashlib.sha256(message_bytes).digest()).digest()
|
|
|
|
signature_bytes = base64.b64decode(signature)
|
|
|
|
rec_id = (signature_bytes[0] - 27) & 3
|
|
|
|
signature_bytes = signature_bytes[1:] + bytes((rec_id,))
|
|
|
|
try:
|
|
|
|
pubkey = PublicKey.from_signature_and_message(signature_bytes, message_hash, hasher=None)
|
|
|
|
except Exception as e:
|
|
|
|
self._log.info('verifyMessage failed: ' + str(e))
|
|
|
|
return False
|
|
|
|
|
|
|
|
address_hash = self.decodeAddress(address)
|
|
|
|
pubkey_hash = hash160(pubkey.format())
|
|
|
|
|
|
|
|
return True if address_hash == pubkey_hash else False
|
|
|
|
|
|
|
|
def showLockTransfers(self, Kbv, Kbs):
|
|
|
|
raise ValueError('Unimplemented')
|
|
|
|
|
|
|
|
def getLockTxSwapOutputValue(self, bid, xmr_swap):
|
|
|
|
return bid.amount
|
|
|
|
|
|
|
|
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap):
|
|
|
|
return xmr_swap.a_swap_refund_value
|
|
|
|
|
|
|
|
def getLockRefundTxSwapOutput(self, xmr_swap):
|
|
|
|
# Only one prevout exists
|
|
|
|
return 0
|
|
|
|
|
|
|
|
def getScriptLockTxDummyWitness(self, script):
|
|
|
|
return [
|
|
|
|
b''.hex(),
|
|
|
|
bytes(72).hex(),
|
|
|
|
bytes(72).hex(),
|
|
|
|
bytes(len(script)).hex()
|
|
|
|
]
|
|
|
|
|
|
|
|
def getScriptLockRefundSpendTxDummyWitness(self, script):
|
|
|
|
return [
|
|
|
|
b''.hex(),
|
|
|
|
bytes(72).hex(),
|
|
|
|
bytes(72).hex(),
|
|
|
|
bytes((1,)).hex(),
|
|
|
|
bytes(len(script)).hex()
|
|
|
|
]
|
|
|
|
|
|
|
|
def getScriptLockRefundSwipeTxDummyWitness(self, script):
|
|
|
|
return [
|
|
|
|
bytes(72).hex(),
|
|
|
|
b''.hex(),
|
|
|
|
bytes(len(script)).hex()
|
|
|
|
]
|
|
|
|
|
|
|
|
def getWitnessStackSerialisedLength(self, witness_stack):
|
|
|
|
length = getCompactSizeLen(len(witness_stack))
|
|
|
|
for e in witness_stack:
|
|
|
|
length += getWitnessElementLen(len(e) // 2) # hex -> bytes
|
|
|
|
|
|
|
|
# See core SerializeTransaction
|
|
|
|
length += 32 + 4 + 1 + 4 # vinDummy
|
|
|
|
length += 1 # flags
|
|
|
|
return length
|
|
|
|
|
|
|
|
def describeTx(self, tx_hex):
|
|
|
|
return self.rpc_callback('decoderawtransaction', [tx_hex])
|
|
|
|
|
|
|
|
def getSpendableBalance(self):
|
|
|
|
return self.make_int(self.rpc_callback('getbalances')['mine']['trusted'])
|
|
|
|
|
|
|
|
def createUTXO(self, value_sats):
|
|
|
|
# Create a new address and send value_sats to it
|
|
|
|
|
|
|
|
spendable_balance = self.getSpendableBalance()
|
|
|
|
if spendable_balance < value_sats:
|
|
|
|
raise ValueError('Balance too low')
|
|
|
|
|
|
|
|
address = self.getNewAddress(self._use_segwit, 'create_utxo')
|
|
|
|
return self.withdrawCoin(self.format_amount(value_sats), address, False), address
|
|
|
|
|
|
|
|
def createRawSignedTransaction(self, addr_to, amount):
|
|
|
|
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
|
|
|
|
|
|
|
options = {
|
|
|
|
'lockUnspents': True,
|
|
|
|
'conf_target': self._conf_target,
|
|
|
|
}
|
|
|
|
txn_funded = self.rpc_callback('fundrawtransaction', [txn, options])['hex']
|
|
|
|
txn_signed = self.rpc_callback('signrawtransactionwithwallet', [txn_funded])['hex']
|
|
|
|
return txn_signed
|
|
|
|
|
|
|
|
def getBlockWithTxns(self, block_hash):
|
|
|
|
return self.rpc_callback('getblock', [block_hash, 2])
|
|
|
|
|
|
|
|
|
|
|
|
def testBTCInterface():
|
|
|
|
print('testBTCInterface')
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
testBTCInterface()
|