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 test_sigfetcher_secured_vaults(revault_network, bitcoind): """ Test that unvault sigs are retrieved and stored for secured vault even if the daemon user did not start activating it or is not a stakeholder. """ rn = revault_network rn.deploy( 3, 1, csv=1, with_cosigs=False, with_watchtowers=False, ) vault = rn.fund(1) rn.secure_vault(vault) stks = rn.stks() outpoint = f"{vault['txid']}:{vault['vout']}" unvault_psbt = stks[0].rpc.getunvaulttx(outpoint)["unvault_tx"] unvault_psbt = stks[0].stk_keychain.sign_unvault_psbt( unvault_psbt, vault["derivation_index"]) stks[0].rpc.unvaulttx(outpoint, unvault_psbt) for stk in rn.participants(): stk.wait_for_logs([ "Syncing Unvault signature", ]) wait_for(lambda: stk.rpc.listpresignedtransactions([outpoint])[ "presigned_transactions"][0]["unvault"]["psbt"] == unvault_psbt)
def wait_for_secured_vaults(self, outpoints): """ Polls listvaults until we acknowledge the 'secured' :tm: vaults at {outpoints} """ assert isinstance(outpoints, list) wait_for( lambda: len(self.rpc.listvaults(["secured"], outpoints)["vaults"] ) == len(outpoints))
def wait_for_active_vaults(self, outpoints): """ Polls listvaults until we acknowledge the active vaults at {outpoints} """ assert isinstance(outpoints, list) wait_for( lambda: len(self.rpc.listvaults(["active"], outpoints)["vaults"] ) == len(outpoints))
def wait_for_deposits(self, outpoints): """ Polls listvaults until we acknowledge the confirmed vaults at {outpoints} """ assert isinstance(outpoints, list) wait_for( lambda: len(self.rpc.listvaults(["funded"], outpoints)["vaults"] ) == len(outpoints))
def cancel_vault(self, vault): deposit = f"{vault['txid']}:{vault['vout']}" for w in self.participants(): wait_for(lambda: len( w.rpc.listvaults(["unvaulting", "unvaulted", "spending"], [deposit])["vaults"]) == 1) self.stk(0).rpc.revault(deposit) self.bitcoind.generate_block(1, wait_for_mempool=1) for w in self.participants(): wait_for(lambda: len( w.rpc.listvaults(["canceled"], [deposit])["vaults"]) == 1)
def test_revocation_sig_sharing(revault_network): revault_network.deploy(4, 2, n_stkmanagers=1) stks = revault_network.stks() mans = revault_network.mans() vault = revault_network.fund(10) deposit = f"{vault['txid']}:{vault['vout']}" child_index = vault["derivation_index"] # We can just get everyone to sign it out of band and a single one handing # it to the sync server. stks[0].wait_for_deposits([deposit]) psbts = stks[0].rpc.getrevocationtxs(deposit) cancel_psbt = psbts["cancel_tx"] emer_psbt = psbts["emergency_tx"] unemer_psbt = psbts["emergency_unvault_tx"] for stk in stks: cancel_psbt = stk.stk_keychain.sign_revocation_psbt( cancel_psbt, child_index) emer_psbt = stk.stk_keychain.sign_revocation_psbt( emer_psbt, child_index) unemer_psbt = stk.stk_keychain.sign_revocation_psbt( unemer_psbt, child_index) stks[0].rpc.revocationtxs(deposit, cancel_psbt, emer_psbt, unemer_psbt) assert stks[0].rpc.listvaults()["vaults"][0]["status"] == "secured" # Note that we can't pass it twice with pytest.raises(RpcError, match="Invalid vault status"): stks[0].rpc.revocationtxs(deposit, cancel_psbt, emer_psbt, unemer_psbt) # They must all have fetched the signatures, even the managers! for stk in stks + mans: wait_for(lambda: len( stk.rpc.listvaults(["secured"], [deposit])["vaults"]) > 0) vault = revault_network.fund(20) deposit = f"{vault['txid']}:{vault['vout']}" child_index = vault["derivation_index"] # Or everyone can sign on their end and push to the sync server for stk in stks: stk.wait_for_deposits([deposit]) psbts = stk.rpc.getrevocationtxs(deposit) cancel_psbt = stk.stk_keychain.sign_revocation_psbt( psbts["cancel_tx"], child_index) emer_psbt = stk.stk_keychain.sign_revocation_psbt( psbts["emergency_tx"], child_index) unemer_psbt = stk.stk_keychain.sign_revocation_psbt( psbts["emergency_unvault_tx"], child_index) stk.rpc.revocationtxs(deposit, cancel_psbt, emer_psbt, unemer_psbt) for stk in stks + mans: wait_for(lambda: len( stk.rpc.listvaults(["secured"], [deposit])["vaults"]) > 0)
def unvault_vaults(self, vaults, destinations, feerate, priority=False): """ Unvault these {vaults}, advertizing a Spend tx spending to these {destinations} (mapping of addresses to amounts) """ spend_psbt = self.broadcast_unvaults(vaults, destinations, feerate, priority) deposits = [f"{v['txid']}:{v['vout']}" for v in vaults] 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) ) return spend_psbt
def spend_vaults(self, vaults, destinations, feerate): """ Spend these {vaults} to these {destinations} (mapping of addresses to amounts). :return: the list of spent deposits along with the Spend PSBT. """ deposits, spend_psbt = self.spend_vaults_unconfirmed( vaults, destinations, feerate) self.bitcoind.generate_block(1, wait_for_mempool=[spend_psbt.tx.hash]) wait_for(lambda: len( self.man(0).rpc.listvaults(["spent"], deposits)["vaults"]) == len( deposits)) return deposits, spend_psbt.tx.hash
def test_sigfetcher_coordinator_dead(revault_network, bitcoind): rn = revault_network rn.deploy( 2, 1, csv=1, with_cosigs=False, with_watchtowers=False, ) vault = revault_network.fund(1) # We kill the coordinator for d in rn.daemons: if d not in rn.participants(): d.stop() # Now we secure the vault deposit = f"{vault['txid']}:{vault['vout']}" for stk in rn.stks(): stk.wait_for_deposits([deposit]) psbts = stk.rpc.getrevocationtxs(deposit) cancel_psbt = stk.stk_keychain.sign_revocation_psbt( psbts["cancel_tx"], vault["derivation_index"]) emer_psbt = stk.stk_keychain.sign_revocation_psbt( psbts["emergency_tx"], vault["derivation_index"]) unemer_psbt = stk.stk_keychain.sign_revocation_psbt( psbts["emergency_unvault_tx"], vault["derivation_index"]) # Revaultd complains because the coordinator is dead with pytest.raises(RpcError, match="Connection refused"): stk.rpc.revocationtxs(deposit, cancel_psbt, emer_psbt, unemer_psbt) # The sigfetcher tries to fetch the signatures, but fails stk.wait_for_log("Error while fetching signatures") wait_for(lambda: len(stk.rpc.listvaults(["securing"])["vaults"]) == 1) # Now we start the coordinator again, and the vault will be secured :) for d in rn.daemons: if d not in rn.participants(): d.start() for stk in rn.stks(): stk.wait_for_logs([ "Syncing Cancel signature", "Syncing Emergency signature", "Syncing Unvault Emergency signature", ]) stk.wait_for_secured_vaults([deposit])
def test_reorged_spend(revault_network, bitcoind): CSV = 12 revault_network.deploy(4, 2, csv=CSV, with_watchtowers=False) vaults = revault_network.fundmany([32, 3]) # Spend the vaults, record the spend time revault_network.activate_fresh_vaults(vaults) deposits, _ = revault_network.spend_vaults_anyhow(vaults) initial_moved_at = revault_network.stk(0).rpc.listvaults( ["spent"])["vaults"][0]["moved_at"] # Initial sanity checks.. for w in revault_network.participants(): wait_for(lambda: w.rpc.getinfo()["blockheight"] == bitcoind.rpc. getblockcount()) assert len(w.rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits) for vault in w.rpc.listvaults(["spent"], deposits)["vaults"]: for field in timestamps_from_status("spent"): assert vault[field] is not None, field for field in timestamps_from_status("spent", present=False): assert vault[field] is None, field # If we are 'spent' and the Spend gets unconfirmed, it'll get marked for # re-broadcast blockheight = bitcoind.rpc.getblockcount() bitcoind.simple_reorg(blockheight, shift=-1) for w in revault_network.participants(): w.wait_for_logs([ "Detected reorg", f"Vault {deposits[0]}'s Spend transaction got unconfirmed", f"Vault {deposits[1]}'s Spend transaction got unconfirmed", "Rescan of all vaults in db done.", ]) # All good if we re-confirm it bitcoind.generate_block(1, wait_for_mempool=1) for w in revault_network.participants(): wait_for(lambda: len(w.rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits)) for vault in w.rpc.listvaults(["spent"], deposits)["vaults"]: for field in timestamps_from_status("spent"): assert vault[field] is not None, field for field in timestamps_from_status("spent", present=False): assert vault[field] is None, field # It's in a new block, it shouldn't have the same timestamp! assert vault["moved_at"] != initial_moved_at
def test_getinfo(revaultd_manager, bitcoind): res = revaultd_manager.rpc.call("getinfo") assert res["network"] == "regtest" assert res["sync"] == 1.0 assert res["version"] == "0.3.1" assert res["vaults"] == 0 # revaultd_manager always deploys with N = 2, M = 3, threshold = M assert res["managers_threshold"] == 3 # test descriptors: RPC call & which Revaultd's were configured assert res["descriptors"]["cpfp"] == revaultd_manager.cpfp_desc assert res["descriptors"]["deposit"] == revaultd_manager.deposit_desc assert res["descriptors"]["unvault"] == revaultd_manager.unvault_desc wait_for(lambda: revaultd_manager.rpc.call("getinfo")["blockheight"] > 0) height = revaultd_manager.rpc.call("getinfo")["blockheight"] bitcoind.generate_block(1) wait_for(lambda: revaultd_manager.rpc.call("getinfo")["blockheight"] == height + 1)
def test_revaulted_spend(revault_network, bitcoind, executor): """ Revault an ongoing Spend transaction carried out by the managers, under misc circumstances. """ CSV = 12 revault_network.deploy(2, 2, n_stkmanagers=1, csv=CSV) mans = revault_network.mans() stks = revault_network.stks() # Simple case. Managers Spend a single vault. vault = revault_network.fund(0.05) revault_network.secure_vault(vault) revault_network.activate_vault(vault) revault_network.spend_vaults_anyhow_unconfirmed([vault]) revault_network.cancel_vault(vault) # Managers spend two vaults, both are canceled. vaults = [revault_network.fund(0.05), revault_network.fund(0.1)] for v in vaults: revault_network.secure_vault(v) revault_network.activate_vault(v) revault_network.unvault_vaults_anyhow(vaults) for vault in vaults: revault_network.cancel_vault(vault) # Managers spend three vaults, only a single one is canceled. And both of them were # created in the same deposit transaction. vaults = revault_network.fundmany([0.2, 0.08]) vaults.append(revault_network.fund(0.03)) for v in vaults: revault_network.secure_vault(v) revault_network.activate_vault(v) revault_network.unvault_vaults_anyhow(vaults) revault_network.cancel_vault(vaults[0]) # vaults[0] is canceled, therefore the Spend transaction is now invalid. The vaults # should be marked as unvaulted since they are not being spent anymore. deposits = [f"{v['txid']}:{v['vout']}" for v in vaults[1:]] for w in mans + stks: wait_for( lambda: len(w.rpc.listvaults(["unvaulted"], deposits)["vaults"] ) == len(deposits))
def spend_vaults(self, vaults, destinations, feerate): """ Spend these {vaults} to these {destinations} (mapping of addresses to amounts). 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. """ deposits, spend_psbt = self.spend_vaults_unconfirmed( vaults, destinations, feerate ) self.bitcoind.generate_block(1, wait_for_mempool=[spend_psbt.tx.hash]) wait_for( lambda: len(self.man(0).rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits) ) return deposits, spend_psbt.tx.hash
def test_sigfetcher(revault_network, bitcoind, executor): rn = revault_network rn.deploy(7, 3, n_stkmanagers=2) # First of all, activate a vault vault = revault_network.fund(0.05) revault_network.secure_vault(vault) revault_network.activate_vault(vault) # Stopping revaultd, deleting the database for w in rn.participants(): w.stop() datadir_db = os.path.join(w.datadir_with_network, "revaultd.sqlite3") os.remove(datadir_db) # Starting revaultd again for w in rn.participants(): # Manually starting it so that we can check that # the db is being created again TailableProc.start(w) w.wait_for_logs([ "No database at .*, creating a new one", "revaultd started on network regtest", "bitcoind now synced", "JSONRPC server started", "Signature fetcher thread started", ]) # They should all get back to the 'active' state, pulling sigs from the coordinator for w in rn.participants(): w.wait_for_log("Got a new unconfirmed deposit") wait_for(lambda: len(w.rpc.listvaults(["funded"], [])) == 1) for w in rn.stks(): w.wait_for_logs([ "Syncing Unvault Emergency signature", "Syncing Emergency signature", "Syncing Cancel signature", "Syncing Unvault signature", ]) for w in rn.man_wallets: w.wait_for_logs([ "Syncing Cancel signature", "Syncing Unvault signature", ])
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 test_coordinator_broadcast(revault_network, bitcoind, executor): """ Test that the coordinator broadcasts spend transactions when they become valid """ CSV = 12 revault_network.deploy(2, 2, n_stkmanagers=1, csv=CSV) vault = revault_network.fund(0.05) revault_network.secure_vault(vault) revault_network.activate_vault(vault) revault_network.unvault_vaults_anyhow([vault]) revault_network.stop_wallets() bitcoind.generate_block(CSV - 1) bitcoind.generate_block(1, wait_for_mempool=1) revault_network.start_wallets() for w in revault_network.participants(): wait_for(lambda: len(w.rpc.listvaults(["spent"])["vaults"]) == 1, )
def test_getrevocationtxs(revault_network, bitcoind): rn = revault_network rn.deploy(4, 2) stks = rn.stks() stk = stks[0] addr = stk.rpc.call("getdepositaddress")["address"] txid = bitcoind.rpc.sendtoaddress(addr, 0.22222) stk.wait_for_logs( ["Got a new unconfirmed deposit", "Incremented deposit derivation index"] ) vault = stk.rpc.listvaults()["vaults"][0] deposit = f"{vault['txid']}:{vault['vout']}" # If we are not a stakeholder, it'll fail with pytest.raises(RpcError, match="This is a stakeholder command"): rn.man(0).rpc.getrevocationtxs(deposit) # If the vault isn't confirmed, it'll fail for n in stks: wait_for(lambda: len(n.rpc.listvaults([], [deposit])["vaults"]) == 1) with pytest.raises(RpcError, match="Invalid vault status"): n.rpc.getrevocationtxs(deposit) # Now, get it confirmed. They all derived the same transactions bitcoind.generate_block(6, txid) wait_for(lambda: stk.rpc.listvaults()["vaults"][0]["status"] == "funded") txs = stk.rpc.getrevocationtxs(deposit) assert len(txs.keys()) == 3 remaining_stks = stks[1:] for n in remaining_stks: wait_for(lambda: n.rpc.listvaults()["vaults"][0]["status"] == "funded") assert txs == n.rpc.getrevocationtxs(deposit)
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 test_largewallets(revaultd_stakeholder, bitcoind): """Test a wallet with 1000 deposits and 10 dust deposits""" amount = 0.01 dust_amount = 0.00012345 bitcoind.generate_block(10) for i in range(10): txids = [] for i in range(100): addr = revaultd_stakeholder.rpc.call( "getdepositaddress")["address"] txids.append(bitcoind.rpc.sendtoaddress(addr, amount)) addr = revaultd_stakeholder.rpc.call("getdepositaddress")["address"] txids.append(bitcoind.rpc.sendtoaddress(addr, dust_amount)) bitcoind.generate_block(6, wait_for_mempool=txids) wait_for(lambda: revaultd_stakeholder.rpc.getinfo()["vaults"] == 10 * 100) assert len(revaultd_stakeholder.rpc.listvaults()["vaults"]) == 10 * 100 # We previously experienced crashes when calling listpresignedtransactions # with a large number of vaults revaultd_stakeholder.rpc.listpresignedtransactions()
def test_raw_broadcast_cancel(revault_network, bitcoind): """ Test broadcasting a dozen of pair of Unvault and Cancel for vaults with different derivation indexes. """ revault_network.deploy(3, 2, n_stkmanagers=2) stks = revault_network.stks() mans = revault_network.mans() for i in range(10): vault = revault_network.fund(10) assert (vault["derivation_index"] == i ), "Derivation index isn't increasing one by one?" deposit = f"{vault['txid']}:{vault['vout']}" revault_network.secure_vault(vault) revault_network.activate_vault(vault) unvault_tx = stks[0].rpc.listpresignedtransactions( [deposit])["presigned_transactions"][0]["unvault"]["hex"] txid = bitcoind.rpc.sendrawtransaction(unvault_tx) bitcoind.generate_block(1, wait_for_mempool=txid) for w in stks + mans: wait_for( lambda: len(w.rpc.listvaults(["unvaulted"], [deposit])) == 1) cancel_tx = stks[0].rpc.listpresignedtransactions( [deposit])["presigned_transactions"][0]["cancel"]["hex"] logging.debug(f"{cancel_tx}") txid = bitcoind.rpc.sendrawtransaction(cancel_tx) bitcoind.generate_block(1, wait_for_mempool=txid) for w in stks + mans: wait_for( lambda: len(w.rpc.listvaults(["canceled"], [deposit])) == 1)
def generate_block(self, numblocks=1, wait_for_mempool=0): if wait_for_mempool: if isinstance(wait_for_mempool, str): wait_for_mempool = [wait_for_mempool] if isinstance(wait_for_mempool, list): wait_for(lambda: all(txid in self.rpc.getrawmempool() for txid in wait_for_mempool)) else: wait_for( lambda: len(self.rpc.getrawmempool()) >= wait_for_mempool) old_blockcount = self.rpc.getblockcount() addr = self.rpc.getnewaddress() self.rpc.generatetoaddress(numblocks, addr) wait_for( lambda: self.rpc.getblockcount() == old_blockcount + numblocks)
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_wt_policy(directory, revault_network, bitcoind): """Test we "can't" breach the policies defined by the watchtowers""" rn = revault_network CSV = 3 rn.deploy(2, 1, csv=CSV) vaults = sorted(rn.fundmany([1, 2, 2, 4, 2, 2]), key=lambda v: v["amount"]) rn.activate_fresh_vaults(vaults) # By default the watchtowers are configured with a plugin enforcing no # spending policy. rn.spend_vaults_anyhow([vaults[0]]) # If we have a single watchtower preventing any unvault, we won't be able # to spend. revault_all_path = os.path.join(WT_PLUGINS_DIR, "revault_all.py") rn.stks()[0].watchtower.add_plugins([{"path": revault_all_path, "config": {}}]) rn.broadcast_unvaults_anyhow(vaults[1:3]) bitcoind.generate_block(1, 2) rn.stks()[0].watchtower.wait_for_log("Broadcasted Cancel transaction") for stk in rn.stks(): deposits = [f"{v['txid']}:{v['vout']}" for v in vaults[1:3]] wait_for( lambda: len(stk.rpc.listvaults(["canceling"], deposits)["vaults"]) == 2 ) bitcoind.generate_block(1) rn.stks()[0].watchtower.remove_plugins([revault_all_path]) # Test a policy limiting the amount we can unvault per day max_per_day_path = os.path.join(WT_PLUGINS_DIR, "max_value_per_day.py") datadir = os.path.join(directory, "max_per_day_plugin_datadir") plugin = { "path": max_per_day_path, "config": {"max_value": 5 * COIN, "data_dir": datadir}, } rn.stks()[1].watchtower.add_plugins([plugin]) # The first one will go through (4 < 5) v = vaults[-1] assert v["amount"] == 4 * COIN deposits, spend_psbt = rn.spend_vaults_anyhow_unconfirmed([v]) for stk in rn.stks(): stk.watchtower.wait_for_log( f"Got a confirmed Unvault UTXO for vault at '{v['txid']}:{v['vout']}'" ) bitcoind.generate_block(1, wait_for_mempool=[spend_psbt.tx.hash]) wait_for( lambda: len(rn.man(0).rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits) ) # The second one won't (4 + 2 > 5) v = vaults[-2] assert v["amount"] == 2 * COIN assert len(bitcoind.rpc.getrawmempool()) == 0 rn.broadcast_unvaults_anyhow([v]) bitcoind.generate_block(1, 1) rn.stks()[1].watchtower.wait_for_log("Broadcasted Cancel transaction") for stk in rn.stks(): deposit = f"{v['txid']}:{v['vout']}" wait_for( lambda: len(stk.rpc.listvaults(["canceling"], [deposit])["vaults"]) == 1 ) bitcoind.generate_block(1) # But it will if we wait till the next day, it'll go through bitcoind.generate_block(144) v = vaults[-3] assert v["amount"] == 2 * COIN rn.spend_vaults_anyhow([v])
def test_reorged_unvault(revault_network, bitcoind): """Test various scenarii with reorgs around the Unvault transaction of a vault.""" CSV = 12 revault_network.deploy(4, 2, csv=CSV, with_watchtowers=False) man = revault_network.man(0) vaults = revault_network.fundmany([32, 3]) deposits = [] amounts = [] for v in vaults: revault_network.secure_vault(v) revault_network.activate_vault(v) deposits.append(f"{v['txid']}:{v['vout']}") amounts.append(v["amount"]) addr = bitcoind.rpc.getnewaddress() amount = sum(amounts) feerate = 1 fee = revault_network.compute_spendtx_fees(feerate, len(vaults), 1) destinations = {addr: amount - fee} revault_network.unvault_vaults(vaults, destinations, feerate) bitcoind.generate_block(1) unvault_tx_a = man.rpc.listonchaintransactions( [deposits[0]])["onchain_transactions"][0]["unvault"] unvault_tx_b = man.rpc.listonchaintransactions( [deposits[1]])["onchain_transactions"][0]["unvault"] # Initial sanity checks.. assert unvault_tx_a["blockheight"] == unvault_tx_b["blockheight"] for w in revault_network.participants(): wait_for(lambda: w.rpc.getinfo()["blockheight"] == bitcoind.rpc. getblockcount()) assert len(w.rpc.listvaults(["unvaulted"], deposits)["vaults"]) == len(deposits) for vault in w.rpc.listvaults(["unvaulted"], deposits)["vaults"]: assert vault["moved_at"] is None for field in timestamps_from_status("unvaulted"): assert vault[field] is not None, field for field in timestamps_from_status("unvaulted", present=False): assert vault[field] is None, field # First, if we reorg but not up to the Unvault tx height, nothing will happen. bitcoind.simple_reorg(unvault_tx_a["blockheight"] + 1) height = bitcoind.rpc.getblockcount() new_tip = f"{height}.*{bitcoind.rpc.getblockhash(height)}" for w in revault_network.participants(): w.wait_for_logs([ "Detected reorg", f"{deposits[0]}.* First Stage transaction is still confirmed .*'{unvault_tx_a['blockheight']}'", f"{deposits[1]}.* First Stage transaction is still confirmed .*'{unvault_tx_b['blockheight']}'", "Rescan .*done", f"New tip.* {new_tip}", ]) assert len(w.rpc.listvaults(["unvaulted"], deposits)["vaults"]) == len(deposits) for vault in w.rpc.listvaults(["unvaulted"], deposits)["vaults"]: assert vault["moved_at"] is None for field in timestamps_from_status("unvaulted"): assert vault[field] is not None, field for field in timestamps_from_status("unvaulted", present=False): assert vault[field] is None, field # Now, if the Unvault tx moves we'll rewind up to the ancestor, rescan the chain # and get back to the 'unvaulted' state. bitcoind.simple_reorg(unvault_tx_a["blockheight"], shift=1) for w in revault_network.participants(): w.wait_for_logs([ "Detected reorg", f"Vault {deposits[0]}'s Unvault transaction .* got unconfirmed", f"Vault {deposits[1]}'s Unvault transaction .* got unconfirmed", "Rescan of all vaults in db done.", ]) for w in revault_network.participants(): wait_for(lambda: w.rpc.getinfo()["blockheight"] == bitcoind.rpc. getblockcount()) wait_for( lambda: len(w.rpc.listvaults(["unvaulted"], deposits)["vaults"] ) == len(deposits)) for vault in w.rpc.listvaults(["unvaulted"], deposits)["vaults"]: assert vault["moved_at"] is None for field in timestamps_from_status("unvaulted"): assert vault[field] is not None, field for field in timestamps_from_status("unvaulted", present=False): assert vault[field] is None, field # If it's not confirmed anymore, we'll detect it and mark the vault as unvaulting unvault_tx_a = man.rpc.listonchaintransactions( [deposits[0]])["onchain_transactions"][0]["unvault"] bitcoind.simple_reorg(unvault_tx_a["blockheight"], shift=-1) for w in revault_network.participants(): w.wait_for_logs([ "Detected reorg", f"Vault {deposits[0]}'s Unvault transaction .* got unconfirmed", f"Vault {deposits[1]}'s Unvault transaction .* got unconfirmed", "Rescan of all vaults in db done.", ]) for w in revault_network.participants(): wait_for(lambda: w.rpc.getinfo()["blockheight"] == bitcoind.rpc. getblockcount()) assert len(w.rpc.listvaults(["unvaulting"], deposits)["vaults"]) == len(deposits) for vault in w.rpc.listvaults(["unvaulting"], deposits)["vaults"]: assert vault["moved_at"] is None for field in timestamps_from_status("unvaulting"): assert vault[field] is not None, field for field in timestamps_from_status("unvaulting", present=False): assert vault[field] is None, field # Now if we are spending # unvault_vault() above actually registered the Spend transaction, so we can activate # it by generating enough block for it to be mature. # NOTE: this exercises the logic of "jump from unvaulting to spending state" assert len(bitcoind.rpc.getrawmempool()) == len(vaults) bitcoind.generate_block(1, wait_for_mempool=len(vaults)) bitcoind.generate_block(CSV - 1) for w in revault_network.participants(): wait_for(lambda: w.rpc.getinfo()["blockheight"] == bitcoind.rpc. getblockcount()) wait_for( lambda: len(w.rpc.listvaults(["spending"], deposits)["vaults"] ) == len(deposits)) for vault in w.rpc.listvaults(["spending"], deposits)["vaults"]: assert vault["moved_at"] is None for field in timestamps_from_status("spending"): assert vault[field] is not None, field for field in timestamps_from_status("spending", present=False): assert vault[field] is None, field # If we are 'spending' and the Unvault gets unconfirmed, we'll rewind, get back to # unvaulting, and mark the Spend for re-broadcast unvault_tx_a = man.rpc.listonchaintransactions( [deposits[0]])["onchain_transactions"][0]["unvault"] bitcoind.simple_reorg(unvault_tx_a["blockheight"], shift=-1) height = bitcoind.rpc.getblockcount() new_tip = f"{height}.*{bitcoind.rpc.getblockhash(height)}" for w in revault_network.participants(): w.wait_for_logs([ "Detected reorg", f"Vault {deposits[0]}'s Unvault transaction .* got unconfirmed", f"Vault {deposits[1]}'s Unvault transaction .* got unconfirmed", "Rescan of all vaults in db done.", f"New tip.* {new_tip}", ]) for w in revault_network.participants(): wait_for( lambda: len(w.rpc.listvaults(["unvaulting"], deposits)["vaults"] ) == len(deposits)) for vault in w.rpc.listvaults(["unvaulting"], deposits)["vaults"]: assert vault["moved_at"] is None for field in timestamps_from_status("unvaulting"): assert vault[field] is not None, field for field in timestamps_from_status("unvaulting", present=False): assert vault[field] is None, field # Get to re-broadcast the spend bitcoind.generate_block(1, wait_for_mempool=len(vaults)) bitcoind.generate_block(CSV - 1) for w in revault_network.participants(): wait_for( lambda: len(w.rpc.listvaults(["spending"], deposits)["vaults"] ) == len(deposits)) for vault in w.rpc.listvaults(["spending"], deposits)["vaults"]: assert vault["moved_at"] is None for field in timestamps_from_status("spending"): assert vault[field] is not None, field for field in timestamps_from_status("spending", present=False): assert vault[field] is None, field # And confirm it bitcoind.generate_block(1, wait_for_mempool=1) for w in revault_network.participants(): wait_for(lambda: len(w.rpc.listvaults(["spent"], deposits)["vaults"]) == len(deposits)) for vault in w.rpc.listvaults(["spent"], deposits)["vaults"]: for field in timestamps_from_status("spent"): assert vault[field] is not None, field for field in timestamps_from_status("spent", present=False): assert vault[field] is None, field
def test_reorged_cancel(revault_network, bitcoind): revault_network.deploy(4, 2, csv=12, with_watchtowers=False) stks = revault_network.stks() mans = revault_network.mans() vault = revault_network.fund(32) revault_network.secure_vault(vault) revault_network.activate_vault(vault) deposit = f"{vault['txid']}:{vault['vout']}" amount = vault["amount"] addr = bitcoind.rpc.getnewaddress() feerate = 1 fee = revault_network.compute_spendtx_fees(feerate, 1, 1) destinations = {addr: amount - fee} revault_network.unvault_vaults([vault], destinations, feerate) unvault_tx = mans[0].rpc.listonchaintransactions( [deposit])["onchain_transactions"][0]["unvault"] # Now let's cancel the spending revault_network.cancel_vault(vault) cancel_tx = mans[0].rpc.listonchaintransactions( [deposit])["onchain_transactions"][0]["cancel"] initial_moved_at = revault_network.stk( 0).rpc.listvaults()["vaults"][0]["moved_at"] # Reorging, but not unconfirming the cancel bitcoind.simple_reorg(cancel_tx["blockheight"]) for w in stks + mans: w.wait_for_logs([ "Detected reorg", f"Vault {deposit}'s Cancel transaction got unconfirmed", "Rescan of all vaults in db done.", ]) wait_for(lambda: w.rpc.getinfo()["blockheight"] == bitcoind.rpc. getblockcount()) # Let's unconfirm the cancel and check that the vault is now in 'canceling' state bitcoind.simple_reorg(cancel_tx["blockheight"], shift=-1) for w in stks + mans: w.wait_for_logs([ "Detected reorg", f"Vault {deposit}'s Cancel transaction got unconfirmed", "Rescan of all vaults in db done.", ]) wait_for(lambda: w.rpc.getinfo()["blockheight"] == bitcoind.rpc. getblockcount()) for w in stks + mans: wait_for(lambda: w.rpc.listvaults([], [deposit])["vaults"][0]["status"] == "canceling") vault = w.rpc.listvaults([], [deposit])["vaults"][0] assert vault["moved_at"] is None for field in timestamps_from_status("canceling"): assert vault[field] is not None, field for field in timestamps_from_status("canceling", present=False): assert vault[field] is None, field # Confirming the cancel again bitcoind.generate_block(1, wait_for_mempool=1) for w in stks + mans: w.wait_for_log("Cancel tx .* was confirmed at height .*") wait_for(lambda: w.rpc.listvaults([], [deposit])["vaults"][0]["status"] == "canceled") for field in timestamps_from_status("canceled"): vault = w.rpc.listvaults([], [deposit])["vaults"][0] assert vault[field] is not None, field for field in timestamps_from_status("canceled", present=False): assert vault[field] is None, field # It's in a new block, it shouldn't have the same timestamp! assert vault["moved_at"] != initial_moved_at # Let's unconfirm the unvault bitcoind.simple_reorg(unvault_tx["blockheight"], shift=-1) for w in stks + mans: w.wait_for_log( f"Vault {deposit}'s Unvault transaction .* got unconfirmed") # Here we go canceling everything again bitcoind.generate_block(1, wait_for_mempool=2) for w in stks + mans: wait_for(lambda: w.rpc.listvaults([], [deposit])["vaults"][0]["status"] == "canceled") for field in timestamps_from_status("canceled"): assert [field] is not None, field for field in timestamps_from_status("canceled", present=False): assert vault[field] is None, field
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_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_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 reorg_deposit(revault_network, bitcoind, deposit, stop_wallets, target_status): """Reorganize the chain around a deposit according to different scenarii. The deposit must refer to a vault that is at least confirmed. The `stop_wallets` parameter controls whether to stop the daemons during a reorg. The `target_status` parameter indicates the expected status of the vault if its deposit transaction gets unconfirmed then re-confirmed. """ vault = revault_network.stk(0).rpc.listvaults([], [deposit])["vaults"][0] initial_confs = bitcoind.rpc.getblockcount() - vault["blockheight"] + 1 logging.info( f"Initial vault blockheight {vault['blockheight']} ({initial_confs} confs)" ) # Sanity check the timestamps for field in timestamps_from_status(vault["status"]): assert vault[field] is not None, field for field in timestamps_from_status(vault["status"], present=False): assert vault[field] is None, field # Mine a block and reorg it, it should not affect us since the deposit would still # have more than 6 confs. bitcoind.generate_block(1) height = bitcoind.rpc.getblockcount() for w in revault_network.participants(): wait_for(lambda: w.rpc.getinfo()["blockheight"] == height) reorg(revault_network, bitcoind, stop_wallets, height) new_tip = f"{height + 1}.*{bitcoind.rpc.getblockhash(height + 1)}" for w in revault_network.participants(): w.wait_for_logs([ "Detected reorg", f"Found common ancestor at height {height - 1}", f"Vault deposit '{deposit}' still has {initial_confs} confirmations at common ancestor", "Rescan .*done", f"New tip.* {new_tip}", ]) v = w.rpc.listvaults([], [deposit])["vaults"][0] assert v["status"] == vault["status"] for field in timestamps_from_status(vault["status"]): assert v[field] is not None, field for field in timestamps_from_status(vault["status"], present=False): assert v[field] is None, field for w in revault_network.participants(): wait_for(lambda: w.rpc.getinfo()["blockheight"] == height + 1) height = bitcoind.rpc.getblockcount() vault = w.rpc.listvaults([], [deposit])["vaults"][0] confs = height + 1 - vault["blockheight"] logging.info( f"After first reorg. Vault blockheight {vault['blockheight']} ({confs} confs)" ) # Now actually shift it out. # It won't transition to 'funded'... reorg(revault_network, bitcoind, stop_wallets, vault["blockheight"], shift=-1) new_tip = f"{height + 1}.*{bitcoind.rpc.getblockhash(height + 1)}" for w in revault_network.participants(): w.wait_for_logs([ "Detected reorg", f"Found common ancestor at height {vault['blockheight'] - 1}", f"Vault deposit '{deposit}' has 0 confirmations at common ancestor", "Rescan .*done", f"New tip.* {new_tip}", ]) for w in revault_network.participants(): wait_for(lambda: w.rpc.getinfo()["blockheight"] == height + 1) for w in revault_network.participants(): wait_for(lambda: w.rpc.listvaults([], [deposit])["vaults"][0]["status"] == "unconfirmed") vault = w.rpc.listvaults([], [deposit])["vaults"][0] for field in ["funded_at", "secured_at", "delegated_at", "moved_at"]: assert vault[field] is None, field # ... But it will if we re-confirm it! bitcoind.generate_block(6, wait_for_mempool=vault["txid"]) for w in revault_network.participants(): wait_for(lambda: w.rpc.listvaults([], [deposit])["vaults"][0]["status"] == target_status) vault = w.rpc.listvaults([], [deposit])["vaults"][0] for field in timestamps_from_status(target_status): assert vault[field] is not None, field for field in timestamps_from_status(target_status, present=False): assert vault[field] is None, field height = bitcoind.rpc.getblockcount() vault = w.rpc.listvaults([], [deposit])["vaults"][0] confs = height + 1 - vault["blockheight"] logging.info( f"After second reorg. Vault blockheight {vault['blockheight']} ({confs} confs)" ) # Now reorg 1 block of the 6 making the vault funded. This should get the deposit under # the minimum number of confirmations threshold. # But since the newly connected chain has as many blocks, the vault will get back to # 'funded'. And since the deposit didn't change, the signatures on the coordinator are # still valid. It will re-download them and transition back to 'secured' / 'active'. Then # if some second-stage transactions were broadcasted, they will be re-broadcast. reorged_block_height = vault["blockheight"] + 5 reorg(revault_network, bitcoind, stop_wallets, reorged_block_height) new_tip = f"{height + 1}.*{bitcoind.rpc.getblockhash(height + 1)}" for w in revault_network.participants(): w.wait_for_logs([ "Detected reorg", f"Found common ancestor at height {reorged_block_height - 1}", f"Vault deposit '{deposit}' has 5 confirmations at common ancestor", "Rescan .*done", f"New tip.* {new_tip}", ]) for w in revault_network.participants(): wait_for(lambda: w.rpc.getinfo()["blockheight"] == height + 1) for w in revault_network.participants(): wait_for(lambda: w.rpc.listvaults([], [deposit])["vaults"][0]["status"] == target_status) vault = w.rpc.listvaults([], [deposit])["vaults"][0] for field in timestamps_from_status(target_status): assert vault[field] is not None, field for field in timestamps_from_status(target_status, present=False): assert vault[field] is None, field height = bitcoind.rpc.getblockcount() vault = w.rpc.listvaults([], [deposit])["vaults"][0] confs = height + 1 - vault["blockheight"] logging.info( f"After third reorg. Vault blockheight {vault['blockheight']} ({confs} confs)" ) # Now reorg up to the deposit. The same will happen. reorg(revault_network, bitcoind, stop_wallets, vault["blockheight"]) new_tip = f"{height + 1}.*{bitcoind.rpc.getblockhash(height + 1)}" for w in revault_network.participants(): w.wait_for_logs([ "Detected reorg", f"Found common ancestor at height {vault['blockheight'] - 1}", f"Vault deposit '{deposit}' has 0 confirmations at common ancestor", "Rescan .*done", f"New tip.* {new_tip}", ]) for w in revault_network.participants(): wait_for(lambda: w.rpc.getinfo()["blockheight"] == height + 1) for w in revault_network.participants(): wait_for(lambda: w.rpc.listvaults([], [deposit])["vaults"][0]["status"] == target_status) for field in timestamps_from_status(target_status): assert vault[field] is not None, field for field in timestamps_from_status(target_status, present=False): assert vault[field] is None, field height = bitcoind.rpc.getblockcount() vault = w.rpc.listvaults([], [deposit])["vaults"][0] confs = height + 1 - vault["blockheight"] logging.info( f"After fourth reorg. Vault blockheight {vault['blockheight']} ({confs} confs)" )