def from_utxo(txid_in: str, tx_index_in: int, sats: int, privkey: str, fee: int, local_node_privkey: str, local_funding_privkey: str, remote_node_privkey: str, remote_funding_privkey: str, chain_hash: str = regtest_hash) -> Tuple['Funding', str]: """Make a funding transaction by spending this utxo using privkey: return Funding, tx.""" # Create dummy one to start: we will fill in txid at the end. funding = Funding('', 0, sats - fee, local_node_privkey, local_funding_privkey, remote_node_privkey, remote_funding_privkey, chain_hash) # input private key. inkey = privkey_expand(privkey) inkey_pub = coincurve.PublicKey.from_secret(inkey.secret) # use RBF'able input (requirement for dual-funded things) txin = CTxIn(COutPoint(bytes.fromhex(txid_in), tx_index_in), nSequence=0xFFFFFFFD) txout = CTxOut( sats - fee, CScript([script.OP_0, sha256(funding.redeemscript()).digest()])) tx = CMutableTransaction([txin], [txout], nVersion=2, nLockTime=funding.locktime) # now fill in funding txid. funding.txid = tx.GetTxid().hex() funding.tx = tx # while we're here, sign the transaction. address = P2WPKHBitcoinAddress.from_scriptPubKey( CScript([script.OP_0, Hash160(inkey_pub.format())])) sighash = script.SignatureHash(address.to_redeemScript(), tx, 0, script.SIGHASH_ALL, amount=sats, sigversion=script.SIGVERSION_WITNESS_V0) sig = inkey.sign(sighash, hasher=None) + bytes([script.SIGHASH_ALL]) tx.wit = CTxWitness( [CTxInWitness(CScriptWitness([sig, inkey_pub.format()]))]) return funding, tx.serialize().hex()
# is the redeemScript, not the scriptPubKey. That's because when the CHECKSIG # operation happens EvalScript() will be evaluating the redeemScript, so the # corresponding SignatureHash() function will use that same script when it # replaces the scriptSig in the transaction being hashed with the script being # executed. sighash = SignatureHash(txin_redeemScript, tx, 0, SIGHASH_ALL) print("hash:", b2x(sighash)) print(b2x(bytes([SIGHASH_ALL]))) # Now sign it. We have to append the type of signature we want to the end, in # this case the usual SIGHASH_ALL. sig = seckey.sign(sighash) + bytes([SIGHASH_ALL]) print("sig:", b2x(sig)) # Set the scriptSig of our transaction input appropriately. txin.scriptSig = CScript([sig, txin_redeemScript]) # Verify the signature worked. This calls EvalScript() and actually executes # the opcodes in the scripts to see if everything worked out. If it doesn't an # exception will be raised. VerifyScript(txin.scriptSig, txin_scriptPubKey, tx, 0, (SCRIPT_VERIFY_P2SH, )) # Done! Print the transaction to standard output with the bytes-to-hex # function. txn = tx.serialize() print("raw size:", len(txn)) print("txid:", b2lx(tx.GetTxid())) txn = b2x(txn) # txn = get_actual_txn() print(txn)
def htlc_tx(self, commit_tx: CMutableTransaction, outnum: int, side: Side, amount_sat: int, locktime: int) -> CMutableTransaction: # BOLT #3: # ## HTLC-Timeout and HTLC-Success Transactions # # These HTLC transactions are almost identical, except the # HTLC-timeout transaction is timelocked. Both # HTLC-timeout/HTLC-success transactions can be spent by a valid # penalty transaction. # BOLT #3: # ## HTLC-Timeout and HTLC-Success Transactions # ... # * txin count: 1 # * `txin[0]` outpoint: `txid` of the commitment transaction and # `output_index` of the matching HTLC output for the HTLC transaction # * `txin[0]` sequence: `0` # * `txin[0]` script bytes: `0` txin = CTxIn(COutPoint(commit_tx.GetTxid(), outnum), nSequence=0x0) # BOLT #3: # ## HTLC-Timeout and HTLC-Success Transactions # ... # * txout count: 1 # * `txout[0]` amount: the HTLC amount minus fees (see [Fee # Calculation](#fee-calculation)) # * `txout[0]` script: version-0 P2WSH with witness script as shown below # # The witness script for the output is: # OP_IF # # Penalty transaction # <revocationpubkey> # OP_ELSE # `to_self_delay` # OP_CHECKSEQUENCEVERIFY # OP_DROP # <local_delayedpubkey> # OP_ENDIF # OP_CHECKSIG redeemscript = script.CScript([ script.OP_IF, self.revocation_pubkey(side).format(), script.OP_ELSE, self.self_delay[side], script.OP_CHECKSEQUENCEVERIFY, script.OP_DROP, self.delayed_pubkey(side).format(), script.OP_ENDIF, script.OP_CHECKSIG ]) print("htlc redeemscript = {}".format(redeemscript.hex())) txout = CTxOut(amount_sat, CScript([script.OP_0, sha256(redeemscript).digest()])) # BOLT #3: # ## HTLC-Timeout and HTLC-Success Transactions # ... # * version: 2 # * locktime: `0` for HTLC-success, `cltv_expiry` for HTLC-timeout return CMutableTransaction(vin=[txin], vout=[txout], nVersion=2, nLockTime=locktime)
import hashlib from bitcoin.core import b2x, lx, COIN, CMutableTxOut, CMutableTxIn, CMutableTransaction, CBlock from bitcoin.wallet import P2PKHBitcoinAddress receiver_pkh = bytes.fromhex('346753e81b93e3f1567a16f3009c7c65c768d865') print('receiver_pkh: "%s"' % receiver_pkh.hex()) receiver = P2PKHBitcoinAddress.from_bytes(receiver_pkh) tx = CMutableTransaction() tx.vin.append(CMutableTxIn()) tx.vout.append(CMutableTxOut(10, receiver.to_scriptPubKey())) print('tx: "%s"' % tx.serialize().hex()) print('txid: "%s"' % tx.GetTxid().hex()) blk = CBlock(vtx=[tx]) blk_mtr = blk.calc_merkle_root() print('block_mtr: "%s"' % blk_mtr.hex()) mmr = hashlib.sha256(b'\x00' + blk_mtr).digest() print('mmr: "%s"' % mmr.hex())
class Funding(object): def __init__(self, funding_txid: str, funding_output_index: int, funding_amount: int, local_node_privkey: str, local_funding_privkey: str, remote_node_privkey: str, remote_funding_privkey: str, chain_hash: str = regtest_hash, locktime: int = 0): self.chain_hash = chain_hash self.txid = funding_txid self.output_index = funding_output_index self.amount = funding_amount self.bitcoin_privkeys = [privkey_expand(local_funding_privkey), privkey_expand(remote_funding_privkey)] self.node_privkeys = [privkey_expand(local_node_privkey), privkey_expand(remote_node_privkey)] self.tx = None self.locktime = locktime self.outputs: List[Dict[str, Any]] = [] self.inputs: List[Dict[str, Any]] = [] def tx_hex(self) -> str: if not self.tx: return '' return self.tx.serialize().hex() @staticmethod def sort_by_keys(key_one: coincurve.PublicKey, key_two: coincurve.PublicKey, val_one: Any, val_two: Any) -> Tuple[Any, Any]: """In many places we have to sort elements into key or nodeid order""" # BOLT #3: # ## Funding Transaction Output # # * The funding output script is a P2WSH to: `2 <pubkey1> <pubkey2> 2 # OP_CHECKMULTISIG` # * Where `pubkey1` is the lexicographically lesser of the two # `funding_pubkey` in compressed format, and where `pubkey2` is the # lexicographically greater of the two. if key_one.format() < key_two.format(): return val_one, val_two else: return val_two, val_one def node_id_sort(self, local: Any, remote: Any) -> Tuple[Any, Any]: """Sorts these two items into lexicographical node id order""" # BOLT #7: # - MUST set `node_id_1` and `node_id_2` to the public keys of the two # nodes operating the channel, such that `node_id_1` is the # lexicographically-lesser of the two compressed keys sorted in # ascending lexicographic order. return self.sort_by_keys(self.node_id(Side.local), self.node_id(Side.remote), local, remote) @staticmethod def redeemscript_keys(key_one: coincurve.PublicKey, key_two: coincurve.PublicKey) -> CScript: return CScript([script.OP_2] + [k.format() for k in Funding.sort_by_keys(key_one, key_two, key_one, key_two)] + [script.OP_2, script.OP_CHECKMULTISIG]) def redeemscript(self) -> CScript: key_a, key_b = self.funding_pubkeys_for_tx() return self.redeemscript_keys(key_a, key_b) @staticmethod def locking_script_keys(key_one: coincurve.PublicKey, key_two: coincurve.PublicKey) -> CScript: return CScript([script.OP_0, sha256(Funding.redeemscript_keys(key_one, key_two)).digest()]) def locking_script(self) -> CScript: a, b = self.funding_pubkeys_for_tx() return self.locking_script_keys(a, b) @staticmethod def start(local_node_privkey: str, local_funding_privkey: str, remote_node_privkey: str, remote_funding_privkey: str, funding_sats: int, locktime: int, chain_hash: str = regtest_hash) -> 'Funding': # Create dummy one to start: we will fill in txid at the end return Funding('', 0, funding_sats, local_node_privkey, local_funding_privkey, remote_node_privkey, remote_funding_privkey, chain_hash, locktime) def add_input(self, serial_id: int, prevtx: str, prevtx_vout: int, script_sig: str, sequence: int, privkey: str = None) -> None: # the dummy runner sends empty info, skip if len(prevtx) == 0: return # Find the txid of the transaction prev_tx = CTransaction.deserialize(bytes.fromhex(prevtx)) txin = CTxIn(COutPoint(prev_tx.GetTxid(), prevtx_vout), nSequence=sequence) # Get the previous output for its outscript + value prev_vout = prev_tx.vout[prevtx_vout] self.inputs.append({'input': txin, 'serial_id': serial_id, 'sats': prev_vout.nValue, 'prev_outscript': prev_vout.scriptPubKey.hex(), 'redeemscript': script_sig, 'privkey': privkey, }) def add_output(self, serial_id: int, script: str, sats: int) -> None: txout = CTxOut(sats, CScript(bytes.fromhex(script))) self.outputs.append({'output': txout, 'serial_id': serial_id}) def our_witnesses(self) -> str: """ Extract expected witness data for our node """ witnesses = [] # these were sorted in `build_tx` for _in in self.inputs: if not _in['privkey']: continue wit = _in['sig'] print('witness is ... ', wit) elems = [] for e in wit.scriptWitness.stack: elems.append('{{witness={0}}}'.format(e.hex())) witnesses.append('{{witness_element=[{0}]}}'.format(','.join(elems))) val = '[{}]'.format(','.join(witnesses)) print('witnesses are', val) return val def sign_our_inputs(self) -> None: assert self.tx is not None for idx, _in in enumerate(self.inputs): privkey = _in['privkey'] if privkey and 'sig' not in _in: print('signing our input for tx', self.tx.serialize().hex()) inkey = privkey_expand(privkey) inkey_pub = coincurve.PublicKey.from_secret(inkey.secret) # Really horrid hack to produce a signature for the # multisig utxo in tests/helpers.py if privkey == '38204720bc4f9647fd58c6d0a4bd3a6dd2be16d8e4273c4d1bdd5774e8c51eaf': redeemscript = bytes.fromhex('51210253cdf835e328346a4f19de099cf3d42d4a7041e073cd4057a1c4fd7cdbb1228f2103ae903722f21f85e651b8f9b18fc854084fb90eeb76452bdcfd0cb43a16a382a221036c264d68a9727afdc75949f7d7fa71910ae9ae8001a1fbffa6f7ce000976597c21036429fa8a4ef0b2b1d5cb553e34eeb90a32ab19fae1f0024f332ab4f74283a7282103d4232f19ea85051e7b76bf5f01d03e17eea8751463dee36d71413a739de1a92755ae') else: address = P2WPKHBitcoinAddress.from_scriptPubKey(CScript([script.OP_0, Hash160(inkey_pub.format())])) redeemscript = address.to_redeemScript() sighash = script.SignatureHash(redeemscript, self.tx, idx, script.SIGHASH_ALL, amount=_in['sats'], sigversion=script.SIGVERSION_WITNESS_V0) sig = inkey.sign(sighash, hasher=None) + bytes([script.SIGHASH_ALL]) if privkey == '38204720bc4f9647fd58c6d0a4bd3a6dd2be16d8e4273c4d1bdd5774e8c51eaf': _in['sig'] = CTxInWitness(CScriptWitness([bytes([]), sig, redeemscript])) else: _in['sig'] = CTxInWitness(CScriptWitness([sig, inkey_pub.format()])) def add_witnesses(self, witness_stack: List[Dict[str, Any]]) -> str: assert self.tx is not None wits = [] for idx, _in in enumerate(self.inputs): if 'sig' in _in: wits.append(_in['sig']) continue if not len(witness_stack): continue elems = witness_stack.pop(0)['witness_element'] stack = [] for elem in elems: stack.append(bytes.fromhex(elem['witness'])) wits.append(CTxInWitness(CScriptWitness(stack))) self.tx.wit = CTxWitness(wits) return self.tx.serialize().hex() def build_tx(self) -> str: # Sort inputs/outputs by serial number self.inputs = sorted(self.inputs, key=lambda k: k['serial_id']) self.outputs = sorted(self.outputs, key=lambda k: k['serial_id']) self.tx = CMutableTransaction([i['input'] for i in self.inputs], [o['output'] for o in self.outputs], nVersion=2, nLockTime=self.locktime) assert self.tx is not None self.txid = self.tx.GetTxid().hex() # Set the output index for the funding output locking_script = self.locking_script() for i, out in enumerate([o['output'] for o in self.outputs]): if out.scriptPubKey == locking_script: self.output_index = i self.amount = out.nValue return self.tx.serialize().hex() @staticmethod def from_utxo(txid_in: str, tx_index_in: int, sats: int, privkey: str, fee: int, local_node_privkey: str, local_funding_privkey: str, remote_node_privkey: str, remote_funding_privkey: str, chain_hash: str = regtest_hash) -> Tuple['Funding', str]: """Make a funding transaction by spending this utxo using privkey: return Funding, tx.""" # Create dummy one to start: we will fill in txid at the end. funding = Funding('', 0, sats - fee, local_node_privkey, local_funding_privkey, remote_node_privkey, remote_funding_privkey, chain_hash) # input private key. inkey = privkey_expand(privkey) inkey_pub = coincurve.PublicKey.from_secret(inkey.secret) # use RBF'able input (requirement for dual-funded things) txin = CTxIn(COutPoint(bytes.fromhex(txid_in), tx_index_in), nSequence=0xFFFFFFFD) txout = CTxOut(sats - fee, CScript([script.OP_0, sha256(funding.redeemscript()).digest()])) tx = CMutableTransaction([txin], [txout], nVersion=2, nLockTime=funding.locktime) # now fill in funding txid. funding.txid = tx.GetTxid().hex() funding.tx = tx # while we're here, sign the transaction. address = P2WPKHBitcoinAddress.from_scriptPubKey(CScript([script.OP_0, Hash160(inkey_pub.format())])) sighash = script.SignatureHash(address.to_redeemScript(), tx, 0, script.SIGHASH_ALL, amount=sats, sigversion=script.SIGVERSION_WITNESS_V0) sig = inkey.sign(sighash, hasher=None) + bytes([script.SIGHASH_ALL]) tx.wit = CTxWitness([CTxInWitness(CScriptWitness([sig, inkey_pub.format()]))]) return funding, tx.serialize().hex() def channel_id(self) -> str: # BOLT #2: This message introduces the `channel_id` to identify the # channel. It's derived from the funding transaction by combining the # `funding_txid` and the `funding_output_index`, using big-endian # exclusive-OR (i.e. `funding_output_index` alters the last 2 bytes). chanid = bytearray.fromhex(self.txid) chanid[-1] ^= self.output_index % 256 chanid[-2] ^= self.output_index // 256 return chanid.hex() @staticmethod def funding_pubkey_key(privkey: coincurve.PrivateKey) -> coincurve.PublicKey: return coincurve.PublicKey.from_secret(privkey.secret) def funding_pubkey(self, side: Side) -> coincurve.PublicKey: return self.funding_pubkey_key(self.bitcoin_privkeys[side]) def funding_pubkeys_for_tx(self) -> Tuple[coincurve.PublicKey, coincurve.PublicKey]: """Returns funding pubkeys, in tx order""" # BOLT #3: # ## Funding Transaction Output # # * The funding output script is a P2WSH to: `2 <pubkey1> <pubkey2> 2 # OP_CHECKMULTISIG` # * Where `pubkey1` is the lexicographically lesser of the two # `funding_pubkey` in compressed format, and where `pubkey2` is the # lexicographically greater of the two. return self.sort_by_keys(self.funding_pubkey(Side.local), self.funding_pubkey(Side.remote), self.funding_pubkey(Side.local), self.funding_pubkey(Side.remote)) def funding_privkeys_for_tx(self) -> Tuple[coincurve.PrivateKey, coincurve.PrivateKey]: """Returns funding private keys, in tx order""" return self.sort_by_keys(self.funding_pubkey(Side.local), self.funding_pubkey(Side.remote), self.bitcoin_privkeys[Side.local], self.bitcoin_privkeys[Side.remote]) def node_id(self, side: Side) -> coincurve.PublicKey: return coincurve.PublicKey.from_secret(self.node_privkeys[side].secret) def node_ids(self) -> Tuple[coincurve.PublicKey, coincurve.PublicKey]: """Returns node pubkeys, in order""" return self.node_id_sort(self.node_id(Side.local), self.node_id(Side.remote)) def node_id_privkeys(self) -> Tuple[coincurve.PrivateKey, coincurve.PrivateKey]: """Returns node private keys, in order""" return self.node_id_sort(self.node_privkeys[Side.local], self.node_privkeys[Side.remote]) def funding_pubkeys_for_gossip(self) -> Tuple[coincurve.PublicKey, coincurve.PublicKey]: """Returns funding public keys, in gossip order""" return self.node_id_sort(self.funding_pubkey(Side.local), self.funding_pubkey(Side.remote)) def funding_privkeys_for_gossip(self) -> Tuple[coincurve.PublicKey, coincurve.PublicKey]: """Returns funding private keys, in gossip order""" return self.node_id_sort(self.bitcoin_privkeys[Side.local], self.bitcoin_privkeys[Side.remote]) def _unsigned_channel_announcment(self, features: str, short_channel_id: str) -> Message: """Produce a channel_announcement message with dummy sigs""" node_ids = self.node_ids() bitcoin_keys = self.funding_pubkeys_for_gossip() return Message(namespace().get_msgtype('channel_announcement'), node_signature_1=Sig(bytes(64)), node_signature_2=Sig(bytes(64)), bitcoin_signature_1=Sig(bytes(64)), bitcoin_signature_2=Sig(bytes(64)), features=features, chain_hash=self.chain_hash, short_channel_id=short_channel_id, node_id_1=node_ids[0].format(), node_id_2=node_ids[1].format(), bitcoin_key_1=bitcoin_keys[0].format(), bitcoin_key_2=bitcoin_keys[1].format()) def channel_announcement(self, short_channel_id: str, features: str) -> Message: """Produce a (signed) channel_announcement message""" ann = self._unsigned_channel_announcment(features, short_channel_id) # BOLT #7: # - MUST compute the double-SHA256 hash `h` of the message, beginning # at offset 256, up to the end of the message. # - Note: the hash skips the 4 signatures but hashes the rest of the # message, including any future fields appended to the end. buf = io.BytesIO() ann.write(buf) # Note the first two 'type' bytes! h = sha256(sha256(buf.getvalue()[2 + 256:]).digest()).digest() # BOLT #7: # - MUST set `node_signature_1` and `node_signature_2` to valid # signatures of the hash `h` (using `node_id_1` and `node_id_2`'s # respective secrets). node_privkeys = self.node_id_privkeys() ann.set_field('node_signature_1', Sig(node_privkeys[0].secret.hex(), h.hex())) ann.set_field('node_signature_2', Sig(node_privkeys[1].secret.hex(), h.hex())) bitcoin_privkeys = self.funding_privkeys_for_gossip() # - MUST set `bitcoin_signature_1` and `bitcoin_signature_2` to valid # signatures of the hash `h` (using `bitcoin_key_1` and # `bitcoin_key_2`'s respective secrets). ann.set_field('bitcoin_signature_1', Sig(bitcoin_privkeys[0].secret.hex(), h.hex())) ann.set_field('bitcoin_signature_2', Sig(bitcoin_privkeys[1].secret.hex(), h.hex())) return ann def channel_update(self, short_channel_id: str, side: Side, disable: bool, cltv_expiry_delta: int, htlc_minimum_msat: int, fee_base_msat: int, fee_proportional_millionths: int, timestamp: int, htlc_maximum_msat: Optional[int]) -> Message: # BOLT #7: The `channel_flags` bitfield is used to indicate the # direction of the channel: it identifies the node that this update # originated from and signals various options concerning the # channel. The following table specifies the meaning of its individual # bits: # # | Bit Position | Name | Meaning | # | ------------- | ----------- | -------------------------------- | # | 0 | `direction` | Direction this update refers to. | # | 1 | `disable` | Disable the channel. | # BOLT #7: # - if the origin node is `node_id_1` in the message: # - MUST set the `direction` bit of `channel_flags` to 0. # - otherwise: # - MUST set the `direction` bit of `channel_flags` to 1. if self.funding_pubkey(side) == self.funding_pubkeys_for_gossip()[0]: channel_flags = 0 else: channel_flags = 1 if disable: channel_flags |= 2 # BOLT #7: The `message_flags` bitfield is used to indicate the # presence of optional fields in the `channel_update` message: # # | Bit Position | Name | Field | # | ------------- | ------------------------- | -------------------------------- | # | 0 | `option_channel_htlc_max` | `htlc_maximum_msat` | message_flags = 0 if htlc_maximum_msat: message_flags |= 1 # Begin with a fake signature. update = Message(namespace().get_msgtype('channel_update'), short_channel_id=short_channel_id, signature=Sig(bytes(64)), chain_hash=self.chain_hash, timestamp=timestamp, message_flags=message_flags, channel_flags=channel_flags, cltv_expiry_delta=cltv_expiry_delta, htlc_minimum_msat=htlc_minimum_msat, fee_base_msat=fee_base_msat, fee_proportional_millionths=fee_proportional_millionths) if htlc_maximum_msat: update.set_field('htlc_maximum_msat', htlc_maximum_msat) # BOLT #7: # - MUST set `signature` to the signature of the double-SHA256 of the # entire remaining packet after `signature`, using its own `node_id`. buf = io.BytesIO() update.write(buf) # Note the first two 'type' bytes! h = sha256(sha256(buf.getvalue()[2 + 64:]).digest()).digest() update.set_field('signature', Sig(self.node_privkeys[side].secret.hex(), h.hex())) return update def node_announcement(self, side: Side, features: str, rgb_color: Tuple[int, int, int], alias: str, addresses: bytes, timestamp: int) -> Message: # Begin with a fake signature. ann = Message(namespace().get_msgtype('node_announcement'), signature=Sig(bytes(64)), features=features, timestamp=timestamp, node_id=self.node_id(side).format().hex(), rgb_color=bytes(rgb_color).hex(), alias=bytes(alias, encoding='utf-8').zfill(32), addresses=addresses) # BOLT #7: # - MUST set `signature` to the signature of the double-SHA256 of the entire # remaining packet after `signature` (using the key given by `node_id`). buf = io.BytesIO() ann.write(buf) # Note the first two 'type' bytes! h = sha256(sha256(buf.getvalue()[2 + 64:]).digest()).digest() ann.set_field('signature', Sig(self.node_privkeys[side].secret.hex(), h.hex())) return ann def close_tx(self, fee: int, privkey_dest: str) -> str: """Create a (mutual) close tx""" txin = CTxIn(COutPoint(bytes.fromhex(self.txid), self.output_index)) out_privkey = privkey_expand(privkey_dest) txout = CTxOut(self.amount - fee, CScript([script.OP_0, Hash160(coincurve.PublicKey.from_secret(out_privkey.secret).format())])) tx = CMutableTransaction(vin=[txin], vout=[txout]) sighash = script.SignatureHash(self.redeemscript(), tx, inIdx=0, hashtype=script.SIGHASH_ALL, amount=self.amount, sigversion=script.SIGVERSION_WITNESS_V0) sigs = [key.sign(sighash, hasher=None) for key in self.funding_privkeys_for_tx()] # BOLT #3: # ## Closing Transaction # ... # * `txin[0]` witness: `0 <signature_for_pubkey1> <signature_for_pubkey2>` witness = CScriptWitness([bytes(), sigs[0] + bytes([script.SIGHASH_ALL]), sigs[1] + bytes([script.SIGHASH_ALL]), self.redeemscript()]) tx.wit = CTxWitness([CTxInWitness(witness)]) return tx.serialize().hex()
class Funding(object): def __init__(self, funding_txid: str, funding_output_index: int, funding_amount: int, local_node_privkey: str, local_funding_privkey: str, remote_node_privkey: str, remote_funding_privkey: str, chain_hash: str = regtest_hash, locktime: int = 0): self.chain_hash = chain_hash self.txid = funding_txid self.output_index = funding_output_index self.amount = funding_amount self.bitcoin_privkeys = [ privkey_expand(local_funding_privkey), privkey_expand(remote_funding_privkey) ] self.node_privkeys = [ privkey_expand(local_node_privkey), privkey_expand(remote_node_privkey) ] self.tx = None self.locktime = locktime self.outputs = [] self.inputs = [] def tx_hex(self) -> str: if not self.tx: return '' return self.tx.serialize().hex() @staticmethod def sort_by_keys(key_one: coincurve.PublicKey, key_two: coincurve.PublicKey, val_one: Any, val_two: Any) -> Tuple[Any, Any]: """In many places we have to sort elements into key or nodeid order""" # BOLT #3: # ## Funding Transaction Output # # * The funding output script is a P2WSH to: `2 <pubkey1> <pubkey2> 2 # OP_CHECKMULTISIG` # * Where `pubkey1` is the lexicographically lesser of the two # `funding_pubkey` in compressed format, and where `pubkey2` is the # lexicographically greater of the two. if key_one.format() < key_two.format(): return val_one, val_two else: return val_two, val_one def node_id_sort(self, local: Any, remote: Any) -> Tuple[Any, Any]: """Sorts these two items into lexicographical node id order""" # BOLT #7: # - MUST set `node_id_1` and `node_id_2` to the public keys of the two # nodes operating the channel, such that `node_id_1` is the # lexicographically-lesser of the two compressed keys sorted in # ascending lexicographic order. return self.sort_by_keys(self.node_id(Side.local), self.node_id(Side.remote), local, remote) @staticmethod def redeemscript_keys(key_one: coincurve.PublicKey, key_two: coincurve.PublicKey) -> CScript: return CScript([script.OP_2] + [ k.format() for k in Funding.sort_by_keys(key_one, key_two, key_one, key_two) ] + [script.OP_2, script.OP_CHECKMULTISIG]) def redeemscript(self) -> CScript: key_a, key_b = self.funding_pubkeys_for_tx() return self.redeemscript_keys(key_a, key_b) @staticmethod def locking_script_keys(key_one: coincurve.PublicKey, key_two: coincurve.PublicKey) -> CScript: return CScript([ script.OP_0, sha256(Funding.redeemscript_keys(key_one, key_two)).digest() ]) def locking_script(self) -> CScript: a, b = self.funding_pubkeys_for_tx() return self.locking_script_keys(a, b) @staticmethod def start(local_node_privkey: str, local_funding_privkey: str, remote_node_privkey: str, remote_funding_privkey: str, funding_sats: int, locktime: int, chain_hash: str = regtest_hash) -> 'Funding': # Create dummy one to start: we will fill in txid at the end return Funding('', 0, funding_sats, local_node_privkey, local_funding_privkey, remote_node_privkey, remote_funding_privkey, chain_hash, locktime) def add_input(self, serial_id: int, prevtx: str, prevtx_vout: int, max_witness_len: int, script: str, sequence: int, privkey: str = None) -> None: # Find the txid of the transaction prev_tx = CTransaction.deserialize(bytes.fromhex(prevtx)) txin = CTxIn(COutPoint(prev_tx.GetTxid(), prevtx_vout), nSequence=sequence) # Get the previous output for its outscript + value prev_vout = prev_tx.vout[prevtx_vout] self.inputs.append({ 'input': txin, 'serial_id': serial_id, 'sats': prev_vout.nValue, 'prev_outscript': prev_vout.scriptPubKey.hex(), 'redeemscript': script, 'max_witness_len': max_witness_len, 'privkey': privkey, }) def add_output(self, serial_id: int, script: str, sats: int) -> None: txout = CTxOut(sats, CScript(bytes.fromhex(script))) self.outputs.append({'output': txout, 'serial_id': serial_id}) def add_witnesses(self, witness_stack) -> str: wits = [] for idx, _in in enumerate(self.inputs): privkey = _in['privkey'] serial_id = _in['serial_id'] if privkey: inkey = privkey_expand(privkey) inkey_pub = coincurve.PublicKey.from_secret(inkey.secret) address = P2WPKHBitcoinAddress.from_scriptPubKey( CScript([script.OP_0, Hash160(inkey_pub.format())])) sighash = script.SignatureHash( address.to_redeemScript(), self.tx, idx, script.SIGHASH_ALL, amount=_in['sats'], sigversion=script.SIGVERSION_WITNESS_V0) sig = inkey.sign(sighash, hasher=None) + bytes( [script.SIGHASH_ALL]) wits.append( CTxInWitness(CScriptWitness([sig, inkey_pub.format()]))) continue # Every input from the witness stack will be the accepter's # which is always an odd serial assert (serial_id % 2 == 1) elems = witness_stack.pop(0)['witness_element'] stack = [] for elem in elems: stack.append(bytes.fromhex(elem['witness'])) wits.append(CTxInWitness(CScriptWitness(stack))) self.tx.wit = CTxWitness(wits) return self.tx.serialize().hex() def build_tx(self) -> str: # Sort inputs by serial number ins = [ x['input'] for x in sorted(self.inputs, key=lambda k: k['serial_id']) ] # Sort outputs by serial number outs = [ x['output'] for x in sorted(self.outputs, key=lambda k: k['serial_id']) ] self.tx = CMutableTransaction(ins, outs, nVersion=2, nLockTime=self.locktime) self.txid = self.tx.GetTxid().hex() return self.tx.serialize().hex() @staticmethod def from_utxo(txid_in: str, tx_index_in: int, sats: int, privkey: str, fee: int, local_node_privkey: str, local_funding_privkey: str, remote_node_privkey: str, remote_funding_privkey: str, chain_hash: str = regtest_hash) -> Tuple['Funding', str]: """Make a funding transaction by spending this utxo using privkey: return Funding, tx.""" # Create dummy one to start: we will fill in txid at the end. funding = Funding('', 0, sats - fee, local_node_privkey, local_funding_privkey, remote_node_privkey, remote_funding_privkey, chain_hash) # input private key. inkey = privkey_expand(privkey) inkey_pub = coincurve.PublicKey.from_secret(inkey.secret) # use RBF'able input (requirement for dual-funded things) txin = CTxIn(COutPoint(bytes.fromhex(txid_in), tx_index_in), nSequence=0xFFFFFFFD) txout = CTxOut( sats - fee, CScript([script.OP_0, sha256(funding.redeemscript()).digest()])) tx = CMutableTransaction([txin], [txout], nVersion=2, nLockTime=funding.locktime) # now fill in funding txid. funding.txid = tx.GetTxid().hex() funding.tx = tx # while we're here, sign the transaction. address = P2WPKHBitcoinAddress.from_scriptPubKey( CScript([script.OP_0, Hash160(inkey_pub.format())])) sighash = script.SignatureHash(address.to_redeemScript(), tx, 0, script.SIGHASH_ALL, amount=sats, sigversion=script.SIGVERSION_WITNESS_V0) sig = inkey.sign(sighash, hasher=None) + bytes([script.SIGHASH_ALL]) tx.wit = CTxWitness( [CTxInWitness(CScriptWitness([sig, inkey_pub.format()]))]) return funding, tx.serialize().hex() def channel_id(self) -> str: # BOLT #2: This message introduces the `channel_id` to identify the # channel. It's derived from the funding transaction by combining the # `funding_txid` and the `funding_output_index`, using big-endian # exclusive-OR (i.e. `funding_output_index` alters the last 2 bytes). chanid = bytearray.fromhex(self.txid) chanid[-1] ^= self.output_index % 256 chanid[-2] ^= self.output_index // 256 return chanid.hex() @staticmethod def funding_pubkey_key( privkey: coincurve.PrivateKey) -> coincurve.PublicKey: return coincurve.PublicKey.from_secret(privkey.secret) def funding_pubkey(self, side: Side) -> coincurve.PublicKey: return self.funding_pubkey_key(self.bitcoin_privkeys[side]) def funding_pubkeys_for_tx( self) -> Tuple[coincurve.PublicKey, coincurve.PublicKey]: """Returns funding pubkeys, in tx order""" # BOLT #3: # ## Funding Transaction Output # # * The funding output script is a P2WSH to: `2 <pubkey1> <pubkey2> 2 # OP_CHECKMULTISIG` # * Where `pubkey1` is the lexicographically lesser of the two # `funding_pubkey` in compressed format, and where `pubkey2` is the # lexicographically greater of the two. return self.sort_by_keys(self.funding_pubkey(Side.local), self.funding_pubkey(Side.remote), self.funding_pubkey(Side.local), self.funding_pubkey(Side.remote)) def funding_privkeys_for_tx( self) -> Tuple[coincurve.PrivateKey, coincurve.PrivateKey]: """Returns funding private keys, in tx order""" return self.sort_by_keys(self.funding_pubkey(Side.local), self.funding_pubkey(Side.remote), self.bitcoin_privkeys[Side.local], self.bitcoin_privkeys[Side.remote]) def node_id(self, side: Side) -> coincurve.PublicKey: return coincurve.PublicKey.from_secret(self.node_privkeys[side].secret) def node_ids(self) -> Tuple[coincurve.PublicKey, coincurve.PublicKey]: """Returns node pubkeys, in order""" return self.node_id_sort(self.node_id(Side.local), self.node_id(Side.remote)) def node_id_privkeys( self) -> Tuple[coincurve.PrivateKey, coincurve.PrivateKey]: """Returns node private keys, in order""" return self.node_id_sort(self.node_privkeys[Side.local], self.node_privkeys[Side.remote]) def funding_pubkeys_for_gossip( self) -> Tuple[coincurve.PublicKey, coincurve.PublicKey]: """Returns funding public keys, in gossip order""" return self.node_id_sort(self.funding_pubkey(Side.local), self.funding_pubkey(Side.remote)) def funding_privkeys_for_gossip( self) -> Tuple[coincurve.PublicKey, coincurve.PublicKey]: """Returns funding private keys, in gossip order""" return self.node_id_sort(self.bitcoin_privkeys[Side.local], self.bitcoin_privkeys[Side.remote]) def _unsigned_channel_announcment(self, features: str, short_channel_id: str) -> Message: """Produce a channel_announcement message with dummy sigs""" node_ids = self.node_ids() bitcoin_keys = self.funding_pubkeys_for_gossip() return Message(event_namespace.get_msgtype('channel_announcement'), node_signature_1=Sig(bytes(64)), node_signature_2=Sig(bytes(64)), bitcoin_signature_1=Sig(bytes(64)), bitcoin_signature_2=Sig(bytes(64)), features=features, chain_hash=self.chain_hash, short_channel_id=short_channel_id, node_id_1=node_ids[0].format(), node_id_2=node_ids[1].format(), bitcoin_key_1=bitcoin_keys[0].format(), bitcoin_key_2=bitcoin_keys[1].format()) def channel_announcement(self, short_channel_id: str, features: str) -> Message: """Produce a (signed) channel_announcement message""" ann = self._unsigned_channel_announcment(features, short_channel_id) # BOLT #7: # - MUST compute the double-SHA256 hash `h` of the message, beginning # at offset 256, up to the end of the message. # - Note: the hash skips the 4 signatures but hashes the rest of the # message, including any future fields appended to the end. buf = io.BytesIO() ann.write(buf) # Note the first two 'type' bytes! h = sha256(sha256(buf.getvalue()[2 + 256:]).digest()).digest() # BOLT #7: # - MUST set `node_signature_1` and `node_signature_2` to valid # signatures of the hash `h` (using `node_id_1` and `node_id_2`'s # respective secrets). node_privkeys = self.node_id_privkeys() ann.set_field('node_signature_1', Sig(node_privkeys[0].secret.hex(), h.hex())) ann.set_field('node_signature_2', Sig(node_privkeys[1].secret.hex(), h.hex())) bitcoin_privkeys = self.funding_privkeys_for_gossip() # - MUST set `bitcoin_signature_1` and `bitcoin_signature_2` to valid # signatures of the hash `h` (using `bitcoin_key_1` and # `bitcoin_key_2`'s respective secrets). ann.set_field('bitcoin_signature_1', Sig(bitcoin_privkeys[0].secret.hex(), h.hex())) ann.set_field('bitcoin_signature_2', Sig(bitcoin_privkeys[1].secret.hex(), h.hex())) return ann def channel_update(self, short_channel_id: str, side: Side, disable: bool, cltv_expiry_delta: int, htlc_minimum_msat: int, fee_base_msat: int, fee_proportional_millionths: int, timestamp: int, htlc_maximum_msat: Optional[int]) -> Message: # BOLT #7: The `channel_flags` bitfield is used to indicate the # direction of the channel: it identifies the node that this update # originated from and signals various options concerning the # channel. The following table specifies the meaning of its individual # bits: # # | Bit Position | Name | Meaning | # | ------------- | ----------- | -------------------------------- | # | 0 | `direction` | Direction this update refers to. | # | 1 | `disable` | Disable the channel. | # BOLT #7: # - if the origin node is `node_id_1` in the message: # - MUST set the `direction` bit of `channel_flags` to 0. # - otherwise: # - MUST set the `direction` bit of `channel_flags` to 1. if self.funding_pubkey(side) == self.funding_pubkeys_for_gossip()[0]: channel_flags = 0 else: channel_flags = 1 if disable: channel_flags |= 2 # BOLT #7: The `message_flags` bitfield is used to indicate the # presence of optional fields in the `channel_update` message: # # | Bit Position | Name | Field | # | ------------- | ------------------------- | -------------------------------- | # | 0 | `option_channel_htlc_max` | `htlc_maximum_msat` | message_flags = 0 if htlc_maximum_msat: message_flags |= 1 # Begin with a fake signature. update = Message( event_namespace.get_msgtype('channel_update'), short_channel_id=short_channel_id, signature=Sig(bytes(64)), chain_hash=self.chain_hash, timestamp=timestamp, message_flags=message_flags, channel_flags=channel_flags, cltv_expiry_delta=cltv_expiry_delta, htlc_minimum_msat=htlc_minimum_msat, fee_base_msat=fee_base_msat, fee_proportional_millionths=fee_proportional_millionths) if htlc_maximum_msat: update.set_field('htlc_maximum_msat', htlc_maximum_msat) # BOLT #7: # - MUST set `signature` to the signature of the double-SHA256 of the # entire remaining packet after `signature`, using its own `node_id`. buf = io.BytesIO() update.write(buf) # Note the first two 'type' bytes! h = sha256(sha256(buf.getvalue()[2 + 64:]).digest()).digest() update.set_field('signature', Sig(self.node_privkeys[side].secret.hex(), h.hex())) return update def node_announcement(self, side: Side, features: str, rgb_color: Tuple[int, int, int], alias: str, addresses: bytes, timestamp: int) -> Message: # Begin with a fake signature. ann = Message(event_namespace.get_msgtype('node_announcement'), signature=Sig(bytes(64)), features=features, timestamp=timestamp, node_id=self.node_id(side).format().hex(), rgb_color=bytes(rgb_color).hex(), alias=bytes(alias, encoding='utf-8').zfill(32), addresses=addresses) # BOLT #7: # - MUST set `signature` to the signature of the double-SHA256 of the entire # remaining packet after `signature` (using the key given by `node_id`). buf = io.BytesIO() ann.write(buf) # Note the first two 'type' bytes! h = sha256(sha256(buf.getvalue()[2 + 64:]).digest()).digest() ann.set_field('signature', Sig(self.node_privkeys[side].secret.hex(), h.hex())) return ann def close_tx(self, fee: int, privkey_dest: str) -> str: """Create a (mutual) close tx""" txin = CTxIn(COutPoint(bytes.fromhex(self.txid), self.output_index)) out_privkey = privkey_expand(privkey_dest) txout = CTxOut( self.amount - fee, CScript([ script.OP_0, Hash160( coincurve.PublicKey.from_secret( out_privkey.secret).format()) ])) tx = CMutableTransaction(vin=[txin], vout=[txout]) sighash = script.SignatureHash(self.redeemscript(), tx, inIdx=0, hashtype=script.SIGHASH_ALL, amount=self.amount, sigversion=script.SIGVERSION_WITNESS_V0) sigs = [ key.sign(sighash, hasher=None) for key in self.funding_privkeys_for_tx() ] # BOLT #3: # ## Closing Transaction # ... # * `txin[0]` witness: `0 <signature_for_pubkey1> <signature_for_pubkey2>` witness = CScriptWitness([ bytes(), sigs[0] + bytes([script.SIGHASH_ALL]), sigs[1] + bytes([script.SIGHASH_ALL]), self.redeemscript() ]) tx.wit = CTxWitness([CTxInWitness(witness)]) return tx.serialize().hex()