def unvault_vaults(self, vaults, destinations, feerate): """ Unvault these {vaults}, advertizing a Spend tx spending to these {destinations} (mapping of addresses to amounts) """ man = self.man(0) deposits = [] deriv_indexes = [] for v in vaults: deposits.append(f"{v['txid']}:{v['vout']}") deriv_indexes.append(v["derivation_index"]) man.wait_for_active_vaults(deposits) spend_tx = man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"] for man in self.mans(): spend_tx = man.man_keychain.sign_spend_psbt( spend_tx, deriv_indexes) man.rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() man.rpc.setspendtx(spend_psbt.tx.hash) self.bitcoind.generate_block(1, wait_for_mempool=len(deposits)) for w in self.participants(): wait_for(lambda: len( w.rpc.listvaults(["unvaulted"], deposits)["vaults"]) == len( deposits))
def psbt_add_input(psbt_str): psbt = serializations.PSBT() psbt.deserialize(psbt_str) assert len(psbt.inputs) == 1 psbt.inputs.append(serializations.PartiallySignedInput()) psbt.inputs[1].witness_utxo = copy.copy(psbt.inputs[0].witness_utxo) psbt.inputs[1].witness_utxo.nValue = 12398 psbt.inputs[1].witness_script = psbt.inputs[0].witness_script psbt.tx.vin.append(serializations.CTxIn()) return psbt.serialize()
def get_unvault_txids(wallet, vaults): unvault_txids = [] for vault in vaults: deposit = f"{vault['txid']}:{vault['vout']}" unvault_psbt = serializations.PSBT() unvault_b64 = wallet.rpc.listpresignedtransactions( [deposit])["presigned_transactions"][0]["unvault"]["psbt"] unvault_psbt.deserialize(unvault_b64) unvault_psbt.tx.calc_sha256() unvault_txids.append(unvault_psbt.tx.hash) return unvault_txids
def psbt_add_invalid_sig(psbt_str): psbt = serializations.PSBT() psbt.deserialize(psbt_str) assert len(psbt.inputs) == 1 pk = bytes.fromhex( "02c83dc7fb3ed0a5dd33cf35d891ba4fcbde" "90ede809a0b247a46f4d989dd14411" ) sig = bytes.fromhex( "3045022100894f5c61d1c297227a9a094ea471fd9d84b" "61d4fc78eb71376621758df8c4946022073f5c11e62add56c4c9" "10bc90d0eadb154919e0c6c67b909897bda13cae3620d" ) psbt.inputs[0].partial_sigs[pk] = sig return psbt.serialize()
def spend_vaults_unconfirmed(self, vaults, destinations, feerate, priority=False): """ Spend these {vaults} to these {destinations} (mapping of addresses to amounts), not confirming the Spend transaction. Make sure to call this only with revault deployment with a low (<500) CSV, or you'll encounter an ugly timeout from bitcoinlib. :return: the list of spent deposits along with the Spend PSBT. """ assert len(vaults) > 0 man = self.man(0) deposits = [] deriv_indexes = [] for v in vaults: deposits.append(f"{v['txid']}:{v['vout']}") deriv_indexes.append(v["derivation_index"]) for man in self.mans(): man.wait_for_active_vaults(deposits) spend_tx = man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"] for man in self.mans(): spend_tx = man.man_keychain.sign_spend_psbt(spend_tx, deriv_indexes) man.rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() man.rpc.setspendtx(spend_psbt.tx.hash, priority) self.bitcoind.generate_block(1, wait_for_mempool=len(deposits)) self.bitcoind.generate_block(self.csv) man.wait_for_log( f"Succesfully broadcasted Spend tx '{spend_psbt.tx.hash}'", ) for w in self.participants(): wait_for( lambda: len(w.rpc.listvaults(["spending"], deposits)["vaults"]) == len(deposits) ) return deposits, spend_psbt
def sign_unvault_psbt(self, psbt_str, deriv_index): """Attach an ALL signature to the PSBT with the key at {deriv_index}""" assert isinstance(psbt_str, str) psbt = serializations.PSBT() psbt.deserialize(psbt_str) assert len(psbt.inputs) == 1, "Invalid Unvault PSBT" assert (serializations.make_p2wsh(psbt.inputs[0].witness_script) == psbt.inputs[0].witness_utxo.scriptPubKey) script_code = psbt.inputs[0].witness_script sighash = serializations.sighash_all_witness(script_code, psbt, 0) privkey = coincurve.PrivateKey( self.hd.get_privkey_from_path([deriv_index])) sig = privkey.sign(sighash, hasher=None) + b"\x01" # ALL pubkey = self.hd.get_pubkey_from_path([deriv_index]) psbt.inputs[0].partial_sigs[pubkey] = sig return psbt.serialize()
def sign_spend_psbt(self, psbt_str, deriv_indexes): """Attach an ALL signature to each PSBT input with the keys at {deriv_indexes}""" assert isinstance(psbt_str, str) assert isinstance(deriv_indexes, list) psbt = serializations.PSBT() psbt.deserialize(psbt_str) assert len( psbt.inputs) == len(deriv_indexes), "Not enough derivation indexes" for (i, psbtin) in enumerate(psbt.inputs): script_code = psbtin.witness_script sighash = serializations.sighash_all_witness(script_code, psbt, i) privkey = coincurve.PrivateKey( self.hd.get_privkey_from_path([deriv_indexes[i]])) sig = privkey.sign(sighash, hasher=None) + b"\x01" # ALL pubkey = self.hd.get_pubkey_from_path([deriv_indexes[i]]) psbtin.partial_sigs[pubkey] = sig return psbt.serialize()
def spend_vaults_unconfirmed(self, vaults, destinations, feerate): """ Spend these {vaults} to these {destinations} (mapping of addresses to amounts), not confirming the Spend transaction. :return: the list of spent deposits along with the Spend PSBT. """ man = self.man(0) deposits = [] deriv_indexes = [] for v in vaults: deposits.append(f"{v['txid']}:{v['vout']}") deriv_indexes.append(v["derivation_index"]) for man in self.mans(): man.wait_for_active_vaults(deposits) spend_tx = man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"] for man in self.mans(): spend_tx = man.man_keychain.sign_spend_psbt( spend_tx, deriv_indexes) man.rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() man.rpc.setspendtx(spend_psbt.tx.hash) self.bitcoind.generate_block(1, wait_for_mempool=len(deposits)) self.bitcoind.generate_block(self.csv) man.wait_for_log( f"Succesfully broadcasted Spend tx '{spend_psbt.tx.hash}'", ) wait_for(lambda: len( self.man(0).rpc.listvaults(["spending"], deposits)["vaults"]) == len(deposits)) return deposits, spend_psbt
def broadcast_unvaults(self, vaults, destinations, feerate, priority=False): """ Broadcast the Unvault transactions for these {vaults}, advertizing a Spend tx spending to these {destinations} (mapping of addresses to amounts) """ man = self.man(0) deposits = [] deriv_indexes = [] for v in vaults: deposits.append(f"{v['txid']}:{v['vout']}") deriv_indexes.append(v["derivation_index"]) man.wait_for_active_vaults(deposits) spend_tx = man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"] for man in self.mans(): spend_tx = man.man_keychain.sign_spend_psbt(spend_tx, deriv_indexes) man.rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() man.rpc.setspendtx(spend_psbt.tx.hash, priority) return spend_psbt
def test_spends_conflicting(revault_network, bitcoind): """ Here we test two spends which spends 2 vaults each, with one shared and all vaults being created from the same Deposit transaction. """ # Get some more coins bitcoind.generate_block(12) CSV = 112 revault_network.deploy(5, 3, csv=CSV) man = revault_network.man(0) amounts = [0.1, 64, 410] vaults = revault_network.fundmany(amounts) assert len(vaults) == len(amounts) # Edge case: bitcoind can actually mess up with the amounts amounts = [] deposits = [] deriv_indexes = [] for v in vaults: revault_network.secure_vault(v) revault_network.activate_vault(v) deposits.append(f"{v['txid']}:{v['vout']}") deriv_indexes.append(v["derivation_index"]) amounts.append(v["amount"]) (deposits_a, deposits_b) = (deposits[:2], deposits[1:]) (amounts_a, amounts_b) = (amounts[:2], amounts[1:]) (indexes_a, indexes_b) = (deriv_indexes[:2], deriv_indexes[1:]) feerate = 5_000 fees = revault_network.compute_spendtx_fees(feerate, len(deposits_a), 1) destinations = {bitcoind.rpc.getnewaddress(): sum(amounts_a) - fees} spend_tx_a = man.rpc.getspendtx(deposits_a, destinations, 1)["spend_tx"] for man in revault_network.mans(): spend_tx_a = man.man_keychain.sign_spend_psbt(spend_tx_a, indexes_a) man.rpc.updatespendtx(spend_tx_a) feerate = 10_000 fees = revault_network.compute_spendtx_fees(feerate, len(deposits_b), 1, True) destinations = {bitcoind.rpc.getnewaddress(): (sum(amounts_b) - fees) // 2} spend_tx_b = man.rpc.getspendtx(deposits_b, destinations, 1)["spend_tx"] for man in revault_network.mans(): spend_tx_b = man.man_keychain.sign_spend_psbt(spend_tx_b, indexes_b) man.rpc.updatespendtx(spend_tx_b) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx_a) spend_psbt.tx.calc_sha256() spend_txid_a = spend_psbt.tx.hash man.rpc.setspendtx(spend_txid_a) # We can ask the Cosigning Servers their signature again for the very same Spend man.rpc.setspendtx(spend_txid_a) # The two Spend have conflicting inputs, therefore the Cosigning Server won't # accept to sign the second one. spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx_b) spend_psbt.tx.calc_sha256() with pytest.raises( RpcError, match= "one Cosigning Server already signed a Spend transaction spending one of these vaults", ): man.rpc.setspendtx(spend_psbt.tx.hash) wait_for( lambda: len(man.rpc.listvaults(["unvaulting"], deposits_a)["vaults"] ) == len(deposits_a)) # We need a single confirmation to consider the Unvault transaction confirmed bitcoind.generate_block(1, wait_for_mempool=len(deposits_a)) wait_for( lambda: len(man.rpc.listvaults(["unvaulted"], deposits_a)["vaults"] ) == len(deposits_a)) # We'll broadcast the Spend transaction as soon as it's valid bitcoind.generate_block(CSV - 1) man.wait_for_log(f"Succesfully broadcasted Spend tx '{spend_txid_a}'", ) wait_for( lambda: len(man.rpc.listvaults(["spending"], deposits_a)["vaults"] ) == len(deposits_a)) # And will mark it as spent after a single confirmation of the Spend tx bitcoind.generate_block(1, wait_for_mempool=[spend_txid_a]) wait_for(lambda: len(man.rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits_a)) for vault in man.rpc.listvaults(["spent"], deposits)["vaults"]: assert vault["moved_at"] is not None
def test_getspendtx(revault_network, bitcoind): revault_network.deploy(2, 1) man = revault_network.man(0) amount = 32.67890 vault = revault_network.fund(amount) deposit = f"{vault['txid']}:{vault['vout']}" addr = bitcoind.rpc.getnewaddress() spent_vaults = [deposit] feerate = 2 fees = revault_network.compute_spendtx_fees(feerate, len(spent_vaults), 1) destination = {addr: vault["amount"] - fees} revault_network.secure_vault(vault) # If the vault isn't active, it'll fail with pytest.raises(RpcError, match="Invalid vault status"): man.rpc.getspendtx(spent_vaults, destination, feerate) revault_network.activate_vault(vault) # If we are not a manager, it'll fail with pytest.raises(RpcError, match="This is a manager command"): revault_network.stk(0).rpc.getspendtx(spent_vaults, destination, feerate) # The amount was not enough to afford a change output, everything went to # fees. psbt = serializations.PSBT() psbt.deserialize(man.rpc.getspendtx(spent_vaults, destination, feerate)["spend_tx"]) assert len(psbt.inputs) == 1 and len(psbt.outputs) == 2 # But if we decrease it enough, it'll create a change output destinations = {addr: vault["amount"] - fees - 1_000_000} psbt = serializations.PSBT() psbt.deserialize( man.rpc.getspendtx(spent_vaults, destinations, feerate)["spend_tx"] ) assert len(psbt.inputs) == 1 and len(psbt.outputs) == 3 # Asking for an impossible feerate will error with pytest.raises( RpcError, match="Required feerate .* is significantly higher than actual feerate", ): man.rpc.getspendtx(spent_vaults, destinations, 100_000) # We'll stubbornly refuse they shoot themselves in the foot with pytest.raises( RpcError, match="Fees larger than 20000000 sats", ): destinations = {addr: vault["amount"] // 10} man.rpc.getspendtx(spent_vaults, destinations, 100_000) # We can spend many vaults deposits = [deposit] amounts = [vault["amount"]] for _ in range(10): amount = round(random.random() * 10**8 % 50, 7) vault = revault_network.fund(amount) revault_network.secure_vault(vault) revault_network.activate_vault(vault) deposit = f"{vault['txid']}:{vault['vout']}" amount_sat = vault["amount"] deposits.append(deposit) amounts.append(amount_sat) # Note that it passes even with 100k/vb if you disable insane fees # sanity checks :) feerate = random.randint(1, 10_000) sent_amount = sum(amounts) - revault_network.compute_spendtx_fees( feerate, len(deposits), 1 ) destinations = {addr: sent_amount} psbt = serializations.PSBT() psbt.deserialize( man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"] ) assert ( len(psbt.inputs) == len(deposits) and len(psbt.outputs) == 2 ), "unexpected change output" # And we can spend to many destinations deposits = [deposit] destinations = {} for _ in range(10): feerate = random.randint(1, 1_000) destinations[bitcoind.rpc.getnewaddress()] = vault["amount"] // 20 psbt = serializations.PSBT() psbt.deserialize( man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"] ) assert ( len(psbt.inputs) == len(deposits) # destinations + CPFP + change and len(psbt.outputs) == len(destinations.keys()) + 1 + 1 ), "expected a change output" # And we can do both deposits = [] destinations = {} for vault in man.rpc.listvaults(["active"])["vaults"]: deposits.append(f"{vault['txid']}:{vault['vout']}") destinations[bitcoind.rpc.getnewaddress()] = vault["amount"] // 2 psbt = serializations.PSBT() psbt.deserialize(man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"]) assert ( len(psbt.inputs) == len(deposits) # destinations + CPFP + change and len(psbt.outputs) == len(destinations.keys()) + 1 + 1 ), "expected a change output" # We can't create an insanely large transaction destinations = {bitcoind.rpc.getnewaddress(): 200_001 for _ in range(10_000)} with pytest.raises( RpcError, match="Transaction too large: satisfied it could be >400k weight units", ): man.rpc.getspendtx(deposits, destinations, feerate)
def test_revault_command(revault_network, bitcoind, executor): """ Here we manually broadcast the unvualt_tx, followed by the cancel_tx """ revault_network.deploy(3, 1) man = revault_network.man(0) stks = revault_network.stks() vault = revault_network.fund(18) deposit = f"{vault['txid']}:{vault['vout']}" # Can't cancel an unconfirmed deposit with pytest.raises( RpcError, match="Invalid vault status: 'funded'. Need 'unvaulting'" ): stks[0].rpc.revault(deposit) # A manager gets the same error: both parties can revault with pytest.raises( RpcError, match="Invalid vault status: 'funded'. Need 'unvaulting'" ): man.rpc.revault(deposit) revault_network.secure_vault(vault) # Secured is not good enough though with pytest.raises( RpcError, match="Invalid vault status: 'secured'. Need 'unvaulting'" ): stks[0].rpc.revault(deposit) revault_network.activate_vault(vault) # Active? Not enough! with pytest.raises( RpcError, match="Invalid vault status: 'active'. Need 'unvaulting'" ): stks[0].rpc.revault(deposit) # Now we want to broadcast the unvault tx without having an associated spend tx # First of all, we need the unvault psbt finalized unvault_psbt = stks[0].rpc.listpresignedtransactions([deposit])[ "presigned_transactions" ][0]["unvault"]["psbt"] unvault_tx = bitcoind.rpc.finalizepsbt(unvault_psbt)["hex"] bitcoind.rpc.sendrawtransaction(unvault_tx) # Unvaulting! And there's no associated spend tx! Is revault broken? for w in stks + [man]: wait_for( lambda: w.rpc.listvaults([], [deposit])["vaults"][0]["status"] == "unvaulting" ) # Nah it's not, just broadcast the cancel man.rpc.revault(deposit) # Not confirmed yet... for w in stks + [man]: w.wait_for_log("Unvault transaction at .* is now being canceled") wait_for( lambda: w.rpc.listvaults([], [deposit])["vaults"][0]["status"] == "canceling" ) bitcoind.generate_block(1, wait_for_mempool=1) # Funds are safe, we happy for w in stks + [man]: wait_for(lambda: w.rpc.call("getinfo")["blockheight"] == 108) w.wait_for_log("Cancel tx .* was confirmed at height '108'") wait_for( lambda: w.rpc.listvaults([], [deposit])["vaults"][0]["status"] == "canceled" ) w.rpc.listvaults([], [deposit])["vaults"][0]["moved_at"] is not None # Now, do the same process with two new vaults (thus with different derivation indexes) # at the same time, and with the Unvault not being mined yet vault_a = revault_network.fund(12) vault_b = revault_network.fund(0.4) revault_network.secure_vault(vault_a) revault_network.secure_vault(vault_b) revault_network.activate_vault(vault_a) revault_network.activate_vault(vault_b) for v in [vault_a, vault_b]: deposit = f"{v['txid']}:{v['vout']}" unvault_tx = man.rpc.listpresignedtransactions([deposit])[ "presigned_transactions" ][0]["unvault"]["hex"] bitcoind.rpc.sendrawtransaction(unvault_tx) # On purpose, only wait for the one we want to revault with, to trigger some race conditions wait_for( lambda: len(stks[0].rpc.listvaults(["unvaulting"], [deposit])["vaults"]) == 1 ) stks[0].rpc.revault(deposit) for w in stks + [man]: wait_for(lambda: len(stks[0].rpc.listvaults(["canceling"])["vaults"]) == 2) bitcoind.generate_block(1, wait_for_mempool=4) for w in stks + [man]: # 3 cause the first part of the test already had one canceled. wait_for(lambda: len(stks[0].rpc.listvaults(["canceled"])["vaults"]) == 3) # We have as many new deposits as canceled vaults bitcoind.generate_block(6) wait_for( lambda: len(stks[0].rpc.listvaults(["canceled"])["vaults"]) == len(stks[0].rpc.listvaults(["funded"])["vaults"]) ) # And the deposit txid is the Cancel txid for v in stks[0].rpc.listvaults(["canceled"])["vaults"]: deposit = f"{v['txid']}:{v['vout']}" cancel_psbt = serializations.PSBT() cancel_b64 = stks[0].rpc.listpresignedtransactions([deposit])[ "presigned_transactions" ][0]["cancel"]["psbt"] cancel_psbt.deserialize(cancel_b64) cancel_psbt.tx.calc_sha256() cancel_txid = cancel_psbt.tx.hash new_deposits = stks[0].rpc.listvaults(["funded"])["vaults"] assert cancel_txid in [v["txid"] for v in new_deposits]
def test_retrieve_vault_status(revault_network, bitcoind): """Test we keep track of coins that moved without us actively noticing it.""" CSV = 3 revault_network.deploy(2, 2, csv=CSV) stks = revault_network.stk_wallets # We don't use mans() here as we need a reference to the actual list in order to # modify it. mans = revault_network.man_wallets # Create a new deposit, makes everyone aware of it. Then stop one of the # wallets for it to not notice anything from now on. vault = revault_network.fund(0.05) man = mans.pop(0) man.stop() # Now activate and Spend the vault, the manager does not acknowledge it (yet) revault_network.secure_vault(vault) revault_network.activate_vault(vault) deposits = [f"{vault['txid']}:{vault['vout']}"] destinations = {bitcoind.rpc.getnewaddress(): vault["amount"] // 2} spend_tx = mans[0].rpc.getspendtx(deposits, destinations, 1)["spend_tx"] for m in [man] + mans: spend_tx = m.man_keychain.sign_spend_psbt(spend_tx, [vault["derivation_index"]]) mans[0].rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() mans[0].rpc.setspendtx(spend_psbt.tx.hash) bitcoind.generate_block(1, wait_for_mempool=len(deposits)) bitcoind.generate_block(CSV) mans[0].wait_for_log( f"Succesfully broadcasted Spend tx '{spend_psbt.tx.hash}'", ) wait_for(lambda: len(mans[0].rpc.listvaults(["spending"], deposits)[ "vaults"]) == 1) # The manager should restart, and acknowledge the vault as being "spending" mans.insert(0, man) mans[0].start() deposit = f"{vault['txid']}:{vault['vout']}" wait_for(lambda: len(mans[0].rpc.listvaults(["spending"], deposits)[ "vaults"]) == len(deposits)) # And if we mine it now everyone will see it as "spent" bitcoind.generate_block(1, wait_for_mempool=spend_psbt.tx.hash) for w in mans + revault_network.stks(): wait_for(lambda: len(w.rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits)) # Now do the same dance with a "spent" vault vault = revault_network.fund(0.14) man = mans.pop(0) man.stop() revault_network.secure_vault(vault) revault_network.activate_vault(vault) deposits = [f"{vault['txid']}:{vault['vout']}"] destinations = {bitcoind.rpc.getnewaddress(): vault["amount"] // 2} spend_tx = mans[0].rpc.getspendtx(deposits, destinations, 1)["spend_tx"] for m in [man] + mans: spend_tx = m.man_keychain.sign_spend_psbt(spend_tx, [vault["derivation_index"]]) mans[0].rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() mans[0].rpc.setspendtx(spend_psbt.tx.hash) bitcoind.generate_block(1, wait_for_mempool=len(deposits)) bitcoind.generate_block(CSV) mans[0].wait_for_log( f"Succesfully broadcasted Spend tx '{spend_psbt.tx.hash}'", ) bitcoind.generate_block(1, wait_for_mempool=spend_psbt.tx.hash) for w in mans + revault_network.stks(): wait_for(lambda: len(w.rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits)) # The manager should restart, and acknowledge the vault as being "spent" mans.insert(0, man) mans[0].start() deposit = f"{vault['txid']}:{vault['vout']}" wait_for(lambda: len(mans[0].rpc.listvaults(["spent"], [deposit])["vaults"] ) == len(deposits)) # Now do the same dance with a "canceling" vault vault = revault_network.fund(8) man = mans.pop(0) man.stop() revault_network.secure_vault(vault) revault_network.activate_vault(vault) deposits = [f"{vault['txid']}:{vault['vout']}"] destinations = {bitcoind.rpc.getnewaddress(): vault["amount"] // 2} spend_tx = mans[0].rpc.getspendtx(deposits, destinations, 1)["spend_tx"] for m in [man] + mans: spend_tx = m.man_keychain.sign_spend_psbt(spend_tx, [vault["derivation_index"]]) mans[0].rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() mans[0].rpc.setspendtx(spend_psbt.tx.hash) bitcoind.generate_block(1, wait_for_mempool=len(deposits)) # Cancel it for w in mans + revault_network.stks(): wait_for( lambda: len(w.rpc.listvaults(["unvaulted"], deposits)["vaults"] ) == len(deposits)) mans[0].rpc.revault(deposits[0]) for w in mans + revault_network.stks(): wait_for( lambda: len(w.rpc.listvaults(["canceling"], deposits)["vaults"] ) == len(deposits)) # The manager should restart, and acknowledge the vault as being "canceling" mans.insert(0, man) mans[0].start() deposit = f"{vault['txid']}:{vault['vout']}" wait_for(lambda: len(mans[0].rpc.listvaults(["canceling"], [deposit])[ "vaults"]) == len(deposits)) # Now do the same dance with a "canceled" vault vault = revault_network.fund(19) man = mans.pop(0) man.stop() revault_network.secure_vault(vault) revault_network.activate_vault(vault) deposits = [f"{vault['txid']}:{vault['vout']}"] destinations = {bitcoind.rpc.getnewaddress(): vault["amount"] // 2} spend_tx = mans[0].rpc.getspendtx(deposits, destinations, 1)["spend_tx"] for m in [man] + mans: spend_tx = m.man_keychain.sign_spend_psbt(spend_tx, [vault["derivation_index"]]) mans[0].rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() mans[0].rpc.setspendtx(spend_psbt.tx.hash) bitcoind.generate_block(1, wait_for_mempool=len(deposits)) # Cancel it for w in mans + revault_network.stks(): wait_for( lambda: len(w.rpc.listvaults(["unvaulted"], deposits)["vaults"] ) == len(deposits)) mans[0].rpc.revault(deposits[0]) bitcoind.generate_block(1, wait_for_mempool=1) for w in mans + revault_network.stks(): wait_for( lambda: len(w.rpc.listvaults(["canceled"], deposits)["vaults"] ) == len(deposits)) # The manager should restart, and acknowledge the vault as being "canceled" mans.insert(0, man) mans[0].start() deposit = f"{vault['txid']}:{vault['vout']}" wait_for(lambda: len(mans[0].rpc.listvaults(["canceled"], [deposit])[ "vaults"]) == len(deposits)) # Now do the same dance with a "unvaulting" vault vault = revault_network.fund(41) man = mans.pop(0) man.stop() revault_network.secure_vault(vault) revault_network.activate_vault(vault) deposits = [f"{vault['txid']}:{vault['vout']}"] destinations = {bitcoind.rpc.getnewaddress(): vault["amount"] // 2} spend_tx = mans[0].rpc.getspendtx(deposits, destinations, 1)["spend_tx"] for m in [man] + mans: spend_tx = m.man_keychain.sign_spend_psbt(spend_tx, [vault["derivation_index"]]) mans[0].rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() mans[0].rpc.setspendtx(spend_psbt.tx.hash) for w in mans + revault_network.stks(): wait_for( lambda: len(w.rpc.listvaults(["unvaulting"], deposits)["vaults"] ) == len(deposits)) # The manager should restart, and acknowledge the vault as being "unvaulting" mans.insert(0, man) mans[0].start() deposit = f"{vault['txid']}:{vault['vout']}" wait_for(lambda: len(mans[0].rpc.listvaults(["unvaulting"], [deposit])[ "vaults"]) == len(deposits)) # Now do the same dance with a "unvaulted" vault vault = revault_network.fund(99) man = mans.pop(0) man.stop() revault_network.secure_vault(vault) revault_network.activate_vault(vault) deposits = [f"{vault['txid']}:{vault['vout']}"] destinations = {bitcoind.rpc.getnewaddress(): vault["amount"] // 2} spend_tx = mans[0].rpc.getspendtx(deposits, destinations, 1)["spend_tx"] for m in [man] + mans: spend_tx = m.man_keychain.sign_spend_psbt(spend_tx, [vault["derivation_index"]]) mans[0].rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() mans[0].rpc.setspendtx(spend_psbt.tx.hash) bitcoind.generate_block(1, wait_for_mempool=len(deposits)) for w in mans + revault_network.stks(): wait_for( lambda: len(w.rpc.listvaults(["unvaulted"], deposits)["vaults"] ) == len(deposits)) # The manager should restart, and acknowledge the vault as being "unvaulted" mans.insert(0, man) mans[0].start() deposit = f"{vault['txid']}:{vault['vout']}" wait_for(lambda: len(mans[0].rpc.listvaults(["unvaulted"], [deposit])[ "vaults"]) == len(deposits)) # Now do the same dance with an "active" vault vault = revault_network.fund(0.0556789) man = mans.pop(0) man.stop() revault_network.secure_vault(vault) revault_network.activate_vault(vault) # The manager should restart, and acknowledge the vault as being "active" mans.insert(0, man) mans[0].start() deposit = f"{vault['txid']}:{vault['vout']}" mans[0].wait_for_active_vaults([deposit]) # Now do the same dance with a "secured" vault vault = revault_network.fund(0.123456) man = mans.pop(0) man.stop() revault_network.secure_vault(vault) # The manager should restart, and acknowledge the vault as being "secured" mans.insert(0, man) mans[0].start() deposit = f"{vault['txid']}:{vault['vout']}" mans[0].wait_for_secured_vaults([deposit]) # Now do the same dance with an "emergencyvaulting" vault vault = revault_network.fund(0.98634) deposit = f"{vault['txid']}:{vault['vout']}" revault_network.secure_vault(vault) stk = stks.pop(0) stk.stop() stks[0].rpc.emergency() wait_for(lambda: len(stks[0].rpc.listvaults(["emergencyvaulting"], [deposit])["vaults"]) == 1) # The stakeholder should restart, and acknowledge the vault as being "emergencyvaulting" stks.insert(0, stk) stks[0].start() deposit = f"{vault['txid']}:{vault['vout']}" wait_for(lambda: len(stks[0].rpc.listvaults(["emergencyvaulting"], [deposit])["vaults"]) == 1) # Now do the same dance with an "unvaultemergencyvaulting" vault vault = revault_network.fund(1.64329) deposit = f"{vault['txid']}:{vault['vout']}" revault_network.activate_fresh_vaults([vault]) revault_network.unvault_vaults_anyhow([vault]) stk = stks.pop(0) stk.stop() stks[0].rpc.emergency() wait_for(lambda: len(stks[0].rpc.listvaults(["unvaultemergencyvaulting"], [deposit])["vaults"]) == 1) # The stakeholder should restart, and acknowledge the vault as being "emergencyvaulting" stks.insert(0, stk) stks[0].start() deposit = f"{vault['txid']}:{vault['vout']}" wait_for(lambda: len(stks[0].rpc.listvaults(["unvaultemergencyvaulting"], [deposit])["vaults"]) == 1)
def test_not_announceable_spend(revault_network, bitcoind, executor): CSV = 4 revault_network.deploy(5, 7, csv=CSV) man = revault_network.man(0) vaults = [] deposits = [] deriv_indexes = [] amounts = [(i + 1) / 100 for i in range(20)] total_amount = sum(amounts) * COIN vaults = revault_network.fundmany(amounts) deposits = [f"{v['txid']}:{v['vout']}" for v in vaults] deriv_indexes = [v["derivation_index"] for v in vaults] revault_network.activate_fresh_vaults(vaults) feerate = 1 n_outputs = 588 fees = revault_network.compute_spendtx_fees(feerate, len(deposits), n_outputs) output_value = int((total_amount - fees) // n_outputs) destinations = { bitcoind.rpc.getnewaddress(): output_value for _ in range(n_outputs) } # Hey, this spend is huge! with pytest.raises( RpcError, match="Spend transaction is too large, try spending less outpoints'" ): man.rpc.getspendtx(deposits, destinations, feerate) # One less spent outpoint is ok though deposits.pop() deriv_indexes.pop() amounts.pop() total_amount = sum(amounts) * COIN fees = revault_network.compute_spendtx_fees(feerate, len(deposits), n_outputs) output_value = int((total_amount - fees) // n_outputs) for addr in destinations: destinations[addr] = output_value spend_tx = man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"] for man in revault_network.mans(): spend_tx = man.man_keychain.sign_spend_psbt(spend_tx, deriv_indexes) man.rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() spend_txid = spend_psbt.tx.hash man.rpc.setspendtx(spend_txid) wait_for( lambda: len(man.rpc.listvaults(["unvaulting"], deposits)["vaults"] ) == len(deposits)) # We need a single confirmation to consider the Unvault transaction confirmed bitcoind.generate_block(1, wait_for_mempool=len(deposits)) wait_for( lambda: len(man.rpc.listvaults(["unvaulted"], deposits)["vaults"] ) == len(deposits)) # We'll broadcast the Spend transaction as soon as it's valid bitcoind.generate_block(CSV - 1) man.wait_for_log(f"Succesfully broadcasted Spend tx '{spend_txid}'") wait_for(lambda: len(man.rpc.listvaults(["spending"], deposits)["vaults"]) == len(deposits)) # And will mark it as spent after a single confirmation of the Spend tx bitcoind.generate_block(1, wait_for_mempool=[spend_psbt.tx.hash]) wait_for(lambda: len(man.rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits)) for vault in man.rpc.listvaults(["spent"], deposits)["vaults"]: assert vault["moved_at"] is not None
def test_large_spends(revault_network, bitcoind, executor): CSV = 2016 # 2 weeks :tm: revault_network.deploy(17, 8, csv=CSV) man = revault_network.man(0) # Get some more funds bitcoind.generate_block(1) vaults = [] deposits = [] deriv_indexes = [] total_amount = 0 for i in range(10): amount = random.randint(5, 5000) / 100 vaults.append(revault_network.fund(amount)) deposits.append(f"{vaults[i]['txid']}:{vaults[i]['vout']}") deriv_indexes.append(vaults[i]["derivation_index"]) total_amount += vaults[i]["amount"] revault_network.activate_fresh_vaults(vaults) feerate = 1 n_outputs = random.randint(1, 3) fees = revault_network.compute_spendtx_fees(feerate, len(deposits), n_outputs) destinations = { bitcoind.rpc.getnewaddress(): (total_amount - fees) // n_outputs for _ in range(n_outputs) } spend_tx = man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"] for man in revault_network.mans(): spend_tx = man.man_keychain.sign_spend_psbt(spend_tx, deriv_indexes) man.rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() man.rpc.setspendtx(spend_psbt.tx.hash) # Killing the daemon and restart it while unvaulting shouldn't cause # any issue for man in revault_network.mans(): man.stop() man.start() wait_for( lambda: len(man.rpc.listvaults(["unvaulting"], deposits)["vaults"] ) == len(deposits)) # We need a single confirmation to consider the Unvault transaction confirmed bitcoind.generate_block(1, wait_for_mempool=len(deposits)) wait_for( lambda: len(man.rpc.listvaults(["unvaulted"], deposits)["vaults"] ) == len(deposits)) # We'll broadcast the Spend transaction as soon as it's valid # Note that bitcoind's RPC socket may timeout if it needs to generate too many # blocks at once. So, spread them a bit. for _ in range(10): bitcoind.generate_block(CSV // 10) bitcoind.generate_block(CSV % 10 - 1) man.wait_for_log( f"Succesfully broadcasted Spend tx '{spend_psbt.tx.hash}'", ) wait_for(lambda: len(man.rpc.listvaults(["spending"], deposits)["vaults"]) == len(deposits)) # And will mark it as spent after a single confirmation of the Spend tx bitcoind.generate_block(1, wait_for_mempool=[spend_psbt.tx.hash]) wait_for(lambda: len(man.rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits)) for vault in man.rpc.listvaults(["spent"], deposits)["vaults"]: assert vault["moved_at"] is not None
def test_spends_concurrent(revault_network, bitcoind): """ Here we test the creation and succesful broadcast of both Spend transaction concurrently handled but non conflicting. """ CSV = 1024 revault_network.deploy(3, 2, csv=CSV) man = revault_network.man(1) # FIXME: there is something up with higher number and the test framework fee # computation amounts = [0.22, 16, 3, 21] vaults = revault_network.fundmany(amounts) # Edge case: bitcoind can actually mess up with the amounts amounts = [] deposits = [] deriv_indexes = [] for v in vaults: revault_network.secure_vault(v) revault_network.activate_vault(v) deposits.append(f"{v['txid']}:{v['vout']}") deriv_indexes.append(v["derivation_index"]) amounts.append(v["amount"]) (deposits_a, deposits_b) = (deposits[:2], deposits[2:]) (amounts_a, amounts_b) = (amounts[:2], amounts[2:]) (indexes_a, indexes_b) = (deriv_indexes[:2], deriv_indexes[2:]) # Spending to a P2WSH (effectively a change but hey), with a change output destinations = { man.rpc.getdepositaddress()["address"]: sum(amounts_a) // 2 } spend_tx_a = man.rpc.getspendtx(deposits_a, destinations, 1)["spend_tx"] for man in revault_network.mans(): spend_tx_a = man.man_keychain.sign_spend_psbt(spend_tx_a, indexes_a) man.rpc.updatespendtx(spend_tx_a) # Spending to a P2WPKH, with a change output destinations = {bitcoind.rpc.getnewaddress(): sum(amounts_b) // 2} spend_tx_b = man.rpc.getspendtx(deposits_b, destinations, 1)["spend_tx"] for man in revault_network.mans(): spend_tx_b = man.man_keychain.sign_spend_psbt(spend_tx_b, indexes_b) man.rpc.updatespendtx(spend_tx_b) # Of course, we can just stop and still broadcast the Spend man.stop() man.proc.wait(10) man.start() spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx_a) spend_psbt.tx.calc_sha256() spend_txid_a = spend_psbt.tx.hash man.rpc.setspendtx(spend_txid_a) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx_b) spend_psbt.tx.calc_sha256() spend_txid_b = spend_psbt.tx.hash man.rpc.setspendtx(spend_txid_b) for w in revault_network.participants(): wait_for( lambda: len(w.rpc.listvaults(["unvaulting"], deposits)["vaults"] ) == len(deposits)) # We need a single confirmation to consider the Unvault transaction confirmed bitcoind.generate_block(1, wait_for_mempool=len(deposits)) for w in revault_network.participants(): wait_for( lambda: len(w.rpc.listvaults(["unvaulted"], deposits)["vaults"] ) == len(deposits)) # We'll broadcast the Spend transaction as soon as it's valid bitcoind.generate_block(CSV - 1) man.wait_for_logs([ f"Succesfully broadcasted Spend tx '{spend_txid_a}'", f"Succesfully broadcasted Spend tx '{spend_txid_b}'", ]) for w in revault_network.participants(): wait_for( lambda: len(w.rpc.listvaults(["spending"], deposits)["vaults"] ) == len(deposits)) # And will mark it as spent after a single confirmation of the Spend tx bitcoind.generate_block(1, wait_for_mempool=[spend_txid_a, spend_txid_b]) for w in revault_network.participants(): wait_for(lambda: len(w.rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits))
def test_spendtx_management(revault_network, bitcoind): CSV = 12 revault_network.deploy(2, 1, n_stkmanagers=1, csv=CSV) man = revault_network.man(0) amount = 0.24 vault = revault_network.fund(amount) deposit = f"{vault['txid']}:{vault['vout']}" addr = bitcoind.rpc.getnewaddress() spent_vaults = [deposit] feerate = 2 fees = revault_network.compute_spendtx_fees(feerate, len(spent_vaults), 1) destination = {addr: vault["amount"] - fees} revault_network.secure_vault(vault) revault_network.activate_vault(vault) spend_tx = man.rpc.getspendtx(spent_vaults, destination, feerate)["spend_tx"] # If we are not a manager, it'll fail with pytest.raises(RpcError, match="This is a manager command"): revault_network.stk_wallets[0].rpc.updatespendtx(spend_tx) # But it won't if we are a stakeholder-manager revault_network.stkman_wallets[0].rpc.updatespendtx(spend_tx) # It will not accept a spend_tx which spends an unknown Unvault psbt = serializations.PSBT() psbt.deserialize(spend_tx) psbt.tx.vin[0].prevout.hash = 0 insane_spend_tx = psbt.serialize() with pytest.raises(RpcError, match="Spend transaction refers an unknown Unvault"): man.rpc.updatespendtx(insane_spend_tx) # First time, it'll be stored man.rpc.updatespendtx(spend_tx) man.wait_for_log("Storing new Spend transaction") # We can actually update it no matter if it's the same man.rpc.updatespendtx(spend_tx) man.wait_for_log("Updating Spend transaction") assert len(man.rpc.listspendtxs()["spend_txs"]) == 1 # If we delete it.. spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() man.rpc.delspendtx(spend_psbt.tx.hash) assert len(man.rpc.listspendtxs()["spend_txs"]) == 0 # When we update it it'll be treated as a new transaction man.rpc.updatespendtx(spend_tx) man.wait_for_log("Storing new Spend transaction") assert len(man.rpc.listspendtxs()["spend_txs"]) == 1 # Create another Spend transaction spending two vaults vault_b = revault_network.fund(amount) deposit_b = f"{vault_b['txid']}:{vault_b['vout']}" addr_b = bitcoind.rpc.getnewaddress() spent_vaults = [deposit, deposit_b] feerate = 50 fees = revault_network.compute_spendtx_fees(feerate, len(spent_vaults), 2) destination = { addr: (vault_b["amount"] - fees) // 2, addr_b: (vault_b["amount"] - fees) // 2, } revault_network.secure_vault(vault_b) revault_network.activate_vault(vault_b) spend_tx_b = man.rpc.getspendtx(spent_vaults, destination, feerate)["spend_tx"] man.rpc.updatespendtx(spend_tx_b) man.wait_for_log("Storing new Spend transaction") assert len(man.rpc.listspendtxs()["spend_txs"]) == 2 assert { "deposit_outpoints": [deposit], "psbt": spend_tx, "change_index": None, "cpfp_index": 0, } in man.rpc.listspendtxs()["spend_txs"] assert { "deposit_outpoints": [deposit, deposit_b], "psbt": spend_tx_b, "change_index": 3, "cpfp_index": 0, } in man.rpc.listspendtxs()["spend_txs"] # Now we could try to broadcast it.. # But we couldn't broadcast a random txid with pytest.raises(RpcError, match="Unknown Spend transaction"): man.rpc.setspendtx( "d5eb741a31ebf4d2f5d6ae223900f1bd996e209150d3604fca7d9fa5d6136337") # ..And even with an existing one we would have to sign it beforehand! spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx_b) spend_psbt.tx.calc_sha256() with pytest.raises( RpcError, match= f"Not enough signatures, needed: {len(revault_network.mans())}, current: 0", ): man.rpc.setspendtx(spend_psbt.tx.hash) # Now, sign the Spend we are going to broadcast deriv_indexes = [vault["derivation_index"], vault_b["derivation_index"]] for man in revault_network.mans(): spend_tx_b = man.man_keychain.sign_spend_psbt(spend_tx_b, deriv_indexes) # Just before broadcasting it, prepare a competing one to later try to make Cosigning Servers # sign twice vault_c = revault_network.fund(amount / 2) deposit_c = f"{vault_c['txid']}:{vault_c['vout']}" rogue_spent_vaults = [deposit, deposit_b, deposit_c] feerate = 50 fees = revault_network.compute_spendtx_fees(feerate, len(rogue_spent_vaults), 2) destination = { addr: (vault_b["amount"] - fees) // 2, addr_b: (vault_b["amount"] - fees) // 2, } revault_network.secure_vault(vault_c) revault_network.activate_vault(vault_c) rogue_spend_tx = man.rpc.getspendtx(rogue_spent_vaults, destination, feerate)["spend_tx"] deriv_indexes = deriv_indexes + [vault_c["derivation_index"]] for man in revault_network.mans(): rogue_spend_tx = man.man_keychain.sign_spend_psbt( rogue_spend_tx, deriv_indexes) man.rpc.updatespendtx(rogue_spend_tx) # Then broadcast the actual Spend spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx_b) spend_psbt.tx.calc_sha256() spend_tx_b = spend_psbt.serialize() man.rpc.updatespendtx(spend_tx_b) man.rpc.setspendtx(spend_psbt.tx.hash) # If we show good faith (ask again for the same set of outpoints), Cosigning Servers will # try to be helpful. man.rpc.setspendtx(spend_psbt.tx.hash) # However, they won't let us trying to sneak in another outpoint rogue_spend_psbt = serializations.PSBT() rogue_spend_psbt.deserialize(rogue_spend_tx) rogue_spend_psbt.tx.calc_sha256() with pytest.raises( RpcError, match= "one Cosigning Server already signed a Spend transaction spending one of these vaults", ): man.rpc.setspendtx(rogue_spend_psbt.tx.hash) # It gets marked as in the process of being unvaulted immediately (next bitcoind # poll), and will get marked as succesfully unvaulted after a single confirmation. wait_for(lambda: len( man.rpc.listvaults(["unvaulting"], spent_vaults)["vaults"]) == len( spent_vaults)) bitcoind.generate_block(1, wait_for_mempool=len(spent_vaults)) wait_for( lambda: len(man.rpc.listvaults(["unvaulted"], spent_vaults)["vaults"] ) == len(spent_vaults)) # We'll broadcast the Spend transaction as soon as it's valid bitcoind.generate_block(CSV - 1) man.wait_for_log( f"Succesfully broadcasted Spend tx '{spend_psbt.tx.hash}'") wait_for( lambda: len(man.rpc.listvaults(["spending"], spent_vaults)["vaults"] ) == len(spent_vaults)) # And the vault we tried to sneak in wasn't even unvaulted assert len(man.rpc.listvaults(["active"], [deposit_c])["vaults"]) == 1
def test_spend_threshold(revault_network, bitcoind, executor): CSV = 20 managers_threshold = 2 revault_network.deploy(4, 3, csv=CSV, managers_threshold=managers_threshold) man = revault_network.man(0) # Get some more funds bitcoind.generate_block(1) vaults = [] deposits = [] deriv_indexes = [] total_amount = 0 for i in range(5): amount = random.randint(5, 5000) / 100 vaults.append(revault_network.fund(amount)) deposits.append(f"{vaults[i]['txid']}:{vaults[i]['vout']}") deriv_indexes.append(vaults[i]["derivation_index"]) total_amount += vaults[i]["amount"] revault_network.activate_fresh_vaults(vaults) feerate = 1 n_outputs = 3 fees = revault_network.compute_spendtx_fees(feerate, len(deposits), n_outputs) destinations = { bitcoind.rpc.getnewaddress(): (total_amount - fees) // n_outputs for _ in range(n_outputs) } spend_tx = man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"] # Trying to broadcast when managers_threshold - 1 managers signed for man in revault_network.mans()[:managers_threshold - 1]: spend_tx = man.man_keychain.sign_spend_psbt(spend_tx, deriv_indexes) man.rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() # Revaultd didn't like it with pytest.raises( RpcError, match= f"Not enough signatures, needed: {managers_threshold}, current: {managers_threshold - 1}'", ): man.rpc.setspendtx(spend_psbt.tx.hash) # Killing the daemon and restart shouldn't cause any issue for m in revault_network.mans(): m.stop() m.start() # Alright, I'll make the last manager sign... man = revault_network.mans()[managers_threshold] spend_tx = man.man_keychain.sign_spend_psbt(spend_tx, deriv_indexes) man.rpc.updatespendtx(spend_tx) spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() # All good now? man.rpc.setspendtx(spend_psbt.tx.hash) for m in revault_network.mans(): wait_for( lambda: len(m.rpc.listvaults(["unvaulting"], deposits)["vaults"] ) == len(deposits)) # Killing the daemon and restart it while unvaulting shouldn't cause # any issue for m in revault_network.mans(): m.stop() m.start() # We need a single confirmation to consider the Unvault transaction confirmed bitcoind.generate_block(1, wait_for_mempool=len(deposits)) for m in revault_network.mans(): wait_for( lambda: len(m.rpc.listvaults(["unvaulted"], deposits)["vaults"] ) == len(deposits)) # We'll broadcast the Spend transaction as soon as it's valid bitcoind.generate_block(CSV) man.wait_for_log( f"Succesfully broadcasted Spend tx '{spend_psbt.tx.hash}'", ) for m in revault_network.mans(): wait_for( lambda: len(m.rpc.listvaults(["spending"], deposits)["vaults"] ) == len(deposits)) # And will mark it as spent after a single confirmation of the Spend tx bitcoind.generate_block(1, wait_for_mempool=[spend_psbt.tx.hash]) for m in revault_network.mans(): wait_for(lambda: len(m.rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits)) for vault in m.rpc.listvaults(["spent"], deposits)["vaults"]: assert vault["moved_at"] is not None
def test_listspendtxs(revault_network, bitcoind): rn = revault_network rn.deploy(n_stakeholders=2, n_managers=2, n_stkmanagers=0, csv=5) man = rn.man(0) vaults = rn.fundmany([1, 2, 3, 1]) for v in vaults: rn.secure_vault(v) rn.activate_vault(v) # _any_spend_data never creates change destinations, feerate = rn._any_spend_data(vaults) deposits = [] deriv_indexes = [] for v in vaults: deposits.append(f"{v['txid']}:{v['vout']}") deriv_indexes.append(v["derivation_index"]) man.wait_for_active_vaults(deposits) spend_tx = man.rpc.getspendtx(deposits, destinations, feerate)["spend_tx"] for man in rn.mans(): spend_tx = man.man_keychain.sign_spend_psbt(spend_tx, deriv_indexes) man.rpc.updatespendtx(spend_tx) spend_txs = man.rpc.listspendtxs(["non_final"])["spend_txs"] assert len(spend_txs) == 1 assert spend_txs[0]["change_index"] is None assert spend_txs[0]["cpfp_index"] is not None spend_psbt = serializations.PSBT() spend_psbt.deserialize(spend_tx) spend_psbt.tx.calc_sha256() man.rpc.setspendtx(spend_psbt.tx.hash) # The initiator will see the spend as pending for w in rn.participants(): wait_for( lambda: len(w.rpc.listvaults(["unvaulting"], deposits)["vaults"]) == len(deposits) ) spend_txs = man.rpc.listspendtxs(["pending"])["spend_txs"] assert len(spend_txs) == 1 assert spend_txs[0]["change_index"] is None assert spend_txs[0]["cpfp_index"] is not None rn.bitcoind.generate_block(rn.csv - 1, wait_for_mempool=len(deposits)) # Still pending... for w in rn.participants(): wait_for( lambda: len(w.rpc.listvaults(["unvaulted"], deposits)["vaults"]) == len(deposits) ) spend_txs = man.rpc.listspendtxs(["pending"])["spend_txs"] assert len(spend_txs) == 1 assert spend_txs[0]["change_index"] is None assert spend_txs[0]["cpfp_index"] is not None rn.bitcoind.generate_block(1) # Broadcasted! for w in rn.participants(): wait_for( lambda: len(w.rpc.listvaults(["spending"], deposits)["vaults"]) == len(deposits) ) spend_txs = man.rpc.listspendtxs(["broadcasted"])["spend_txs"] assert len(spend_txs) == 1 assert spend_txs[0]["change_index"] is None assert spend_txs[0]["cpfp_index"] is not None rn.bitcoind.generate_block(1, wait_for_mempool=[spend_psbt.tx.hash]) # Transaction is spent, the status is "broadcasted" spend_txs = man.rpc.listspendtxs(["broadcasted"])["spend_txs"] assert len(spend_txs) == 1 assert spend_txs[0]["change_index"] is None assert spend_txs[0]["cpfp_index"] is not None for w in rn.participants(): wait_for( lambda: len(w.rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits) ) vaults = rn.fundmany([3, 4, 5]) for v in vaults: rn.secure_vault(v) rn.activate_vault(v) rn.unvault_vaults_anyhow(vaults) rn.cancel_vault(vaults[0]) # Transaction is canceled, the status is still "pending" as we never # broadcasted it # (Keep in mind that in the utilities under tests/revault_network.py # we usually use the last manager for broadcasting the transactions) assert len(rn.man(1).rpc.listspendtxs(["pending"])["spend_txs"]) == 1 v = rn.fund(6) rn.secure_vault(v) rn.activate_vault(v) rn.spend_vaults_anyhow_unconfirmed([v]) assert len(rn.man(1).rpc.listspendtxs(["broadcasted"])["spend_txs"]) == 2 rn.cancel_vault(v) # Status of the spend is still broadcasted, even if the transaction is canceled assert len(rn.man(1).rpc.listspendtxs(["broadcasted"])["spend_txs"]) == 2