# (c) Copyright 2025 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # tests related to Single Signer Spending Policy feature (SSSP) # # run simulator without --eff # # import pytest, time, base64, random from psbt import BasicPSBT from ckcc.protocol import CCProtocolPacker @pytest.fixture def goto_sssp_menu(goto_home, pick_menu_item): def doit(): goto_home() pick_menu_item("Advanced/Tools") pick_menu_item("Spending Policy") pick_menu_item("Single-Signer") return doit @pytest.fixture def setup_sssp(goto_sssp_menu, pick_menu_item, cap_story, press_select, pass_word_quiz, is_q1, seed_story_to_words, cap_menu, OK, word_menu_entry, press_cancel, press_delete, enter_number, scan_a_qr, cap_screen, settings_get, need_keypress, microsd_path, master_settings_get, enter_pin, settings_remove, sim_exec): def doit(pin=None, mag=None, vel=None, whitelist=None, w2fa=None, has_violation=None, word_check=None, notes_and_pws=None, rel_keys=None): goto_sssp_menu() time.sleep(.1) title, story = cap_story() # it is possible that PIN was set beforehand if title == ("Spending Policy" if is_q1 else "Spend Policy"): assert "stops you from signing transactions unless conditions are met" in story assert "locked into a special mode" in story assert "First step is to define a new PIN" in story press_select() time.sleep(.1) scr = cap_screen() if "Spending Policy" in scr: what = "Enter first part of PIN" if is_q1 else "Enter PIN Prefix" assert what in scr enter_pin(pin) time.sleep(.1) scr = cap_screen() what = "Confirm PIN value"if is_q1 else "CONFIRM PIN VALUE" assert what in scr enter_pin(pin) time.sleep(.1) m = cap_menu() assert "Edit Policy..." in m if has_violation is not None: if has_violation: assert "Last Violation" in m else: assert "last Violation" not in m assert "Word Check" in m assert ("Allow Notes" in m) or not is_q1 assert "Related Keys" in m assert "Remove Policy" in m assert "Test Drive" in m assert "ACTIVATE" in m pick_menu_item("Edit Policy...") whitelist_mi = "Whitelist Addresses" if is_q1 else "Whitelist" mag_mi = "Max Magnitude" vel_mi = "Limit Velocity" mi_2fa = "Web 2FA" time.sleep(.1) m = cap_menu() assert mag_mi in m assert vel_mi in m assert whitelist_mi in m assert mi_2fa in m # setting above values here if mag: pick_menu_item(mag_mi) enter_number(mag) time.sleep(.1) title, story = cap_story() assert f"{mag} {'BTC' if int(mag) < 1000 else 'SATS'}" in story press_select() time.sleep(.1) assert settings_get("sssp")["pol"]["mag"] == mag if vel: if not settings_get("sssp")["pol"].get("mag", None): pick_menu_item(vel_mi) title, story = cap_story() assert 'Velocity limit requires' in story assert 'starting value' in story press_select() else: pick_menu_item(vel_mi) if vel == "Unlimited": target = 0 else: target = int(vel.split()[0]) pick_menu_item(vel) # actually a full menu item time.sleep(.3) assert settings_get("sssp")["pol"]["vel"] == target if whitelist: pick_menu_item(whitelist_mi) time.sleep(.1) m = cap_menu() assert "(none yet)" in m assert "Import from File" in m if is_q1: assert "Scan QR" in m pick_menu_item("Scan QR") for i, addr in enumerate(whitelist, start=1): scan_a_qr(addr) for _ in range(10): scr = cap_screen() if (f"Got {i} so far" in scr) and ("ENTER to apply" in scr): break time.sleep(.2) else: assert False, "updating whitelist failed" press_select() else: assert "Scan QR" not in m fname = "ccc_addrs.txt" with open(microsd_path(fname), "w") as f: for a in whitelist: f.write(f"{a}\n") pick_menu_item("Import from File") time.sleep(.1) _, story = cap_story() if "Press (1)" in story: need_keypress("1") pick_menu_item(fname) time.sleep(.1) _, story = cap_story() if len(whitelist) == 1: assert "Added new address to whitelist" in story else: assert f"Added {len(whitelist)} new addresses to whitelist" in story for addr in whitelist: assert addr in story # check menu correct press_select() time.sleep(.1) m = cap_menu() mi_addrs = [a for a in m if '⋯' in a] for mia, addr in zip(mi_addrs, reversed(whitelist)): _start, _end = mia.split('⋯') assert addr.startswith(_start) assert addr.endswith(_end) press_cancel() assert settings_get("sssp")["pol"]["addrs"] == whitelist if w2fa: pick_menu_item(mi_2fa) press_cancel() # leave Edit Policy... (shared settings with CCC) # now rest of sssp specific settings if word_check is not None: pick_menu_item("Word Check") time.sleep(.1) title, story = cap_story() assert "addition to special PIN" in story assert "provide the first and last seed words" in story if word_check: assert "Enable?" in story press_select() # confirm action time.sleep(.1) assert settings_get("sssp")["words"] else: assert "Disable?" in story pol = settings_get("sssp") if "words" in pol: assert not pol["words"] if notes_and_pws is not None: assert is_q1 pick_menu_item("Allow Notes") time.sleep(.1) title, story = cap_story() assert "Allow (read-only) access to secure notes and passwords?" in story if notes_and_pws: assert "Enable?" in story press_select() # confirm action time.sleep(.1) assert settings_get("sssp")["notes"] else: assert "Disable?" in story pol = settings_get("sssp") if "notes" in pol: assert not pol["notes"] if rel_keys is not None: pick_menu_item("Related Keys") time.sleep(.1) title, story = cap_story() assert "Allow access to BIP-39 passphrase wallets" in story assert "and Seed Vault (read-only)" in story if rel_keys: assert "Enable?" in story press_select() # confirm action time.sleep(.1) assert settings_get("sssp")["okeys"] else: assert "Disable?" in story pol = settings_get("sssp") if "okeys" in pol: assert not pol["okeys"] yield doit # cleanup code -- all users of this fixture will get this code settings_remove("sssp") sim_exec('from pincodes import pa;pa.hobbled_mode = False; from actions import goto_top_menu; goto_top_menu()') @pytest.fixture def policy_sign(start_sign, end_sign, cap_story, get_last_violation): def doit(wallet, psbt, violation=None): start_sign(base64.b64decode(psbt)) time.sleep(.1) title, story = cap_story() if violation: # assume SSSP cases assert title == "Failure" assert 'warning' not in story assert "Spending Policy violation." in story assert violation in get_last_violation() return assert 'OK TO SEND?' == title assert "warning" not in story signed = end_sign(accept=True) po = BasicPSBT().parse(signed) tx_hex = None if violation is None: assert not get_last_violation() assert len(po.inputs[0].part_sigs) or po.inputs[0].taproot_key_sig res = wallet.finalizepsbt(base64.b64encode(signed).decode()) assert res["complete"] tx_hex = res["hex"] res = wallet.testmempoolaccept([tx_hex]) assert res[0]["allowed"] res = wallet.sendrawtransaction(tx_hex) assert len(res) == 64 # tx id return signed, tx_hex return doit @pytest.mark.bitcoind @pytest.mark.parametrize("mag_ok", [True, False]) @pytest.mark.parametrize("mag", [1000000, 2]) def test_magnitude(mag_ok, mag, setup_sssp, bitcoind, settings_set, pick_menu_item, bitcoind_d_sim_watch, policy_sign, press_select, reset_seed_words, settings_path): wo = bitcoind_d_sim_watch settings_set("chain", "XRT") if mag_ok: # always try limit/border value if mag is None: to_send = 1 else: to_send = mag / 100000000 if mag > 1000 else mag else: if mag is None: to_send = 1.1 else: to_send = ((mag / 100000000)+1) if mag > 1000 else (mag+0.001) setup_sssp("11-11", mag=mag) pick_menu_item("ACTIVATE") press_select() addr = wo.getnewaddress() bitcoind.supply_wallet.sendtoaddress(address=addr, amount=5.0) bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) # create funded PSBT psbt_resp = wo.walletcreatefundedpsbt( [], [{bitcoind.supply_wallet.getnewaddress(): to_send}], 0, {"fee_rate": 2} ) psbt = psbt_resp.get("psbt") policy_sign(wo, psbt, violation=None if mag_ok else "magnitude") @pytest.mark.bitcoind @pytest.mark.parametrize("whitelist_ok", [True, False]) def test_whitelist(whitelist_ok, setup_sssp, bitcoind, settings_set, policy_sign, bitcoind_d_sim_watch, pick_menu_item, press_select): wo = bitcoind_d_sim_watch settings_set("chain", "XRT") whitelist = [ "bcrt1qqca9eefwz8tzn7rk6aumhwhapyf5vsrtrddxxp", "bcrt1q7nck280nje50gzjja3gyguhp2ds6astu5ndhkj", "bcrt1qhexpvdhwuerqq0h24j06g8y5eumjjdr28ng4vv", "bcrt1q3ylr55pk7rl0rc06d8th7h25zmcuvvg8wt0yl3", ] if whitelist_ok: send_to = whitelist[0] else: send_to = bitcoind.supply_wallet.getnewaddress() setup_sssp("11-11", whitelist=whitelist) pick_menu_item("ACTIVATE") press_select() multi_addr = wo.getnewaddress() bitcoind.supply_wallet.sendtoaddress(address=multi_addr, amount=5.0) bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) # create funded PSBT psbt_resp = wo.walletcreatefundedpsbt( [], [{send_to: 1}], 0, {"fee_rate": 2} ) psbt = psbt_resp.get("psbt") policy_sign(wo, psbt, violation=None if whitelist_ok else "whitelist") @pytest.mark.bitcoind @pytest.mark.parametrize("velocity_mi", ['6 blocks (hour)', '48 blocks (8h)']) def test_velocity(velocity_mi, setup_sssp, bitcoind, settings_set, pick_menu_item, policy_sign, settings_get, bitcoind_d_sim_watch, press_select): wo = bitcoind_d_sim_watch wo.keypoolrefill(20) settings_set("chain", "XRT") blocks = int(velocity_mi.split()[0]) setup_sssp("11-11", vel=velocity_mi) pick_menu_item("ACTIVATE") press_select() assert "block_h" not in settings_get("sssp")["pol"] multi_addr = wo.getnewaddress() bitcoind.supply_wallet.sendtoaddress(address=multi_addr, amount=49) bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) # create funded PSBT, first tx init_block_height = bitcoind.supply_wallet.getblockchaininfo()["blocks"] # block height psbt_resp = wo.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 1}], init_block_height) # nLockTime set to current block height psbt = psbt_resp.get("psbt") po = BasicPSBT().parse(base64.b64decode(psbt)) assert po.parsed_txn.nLockTime == init_block_height policy_sign(wo, psbt) # success as this is first tx that sets block height from 0 assert settings_get("sssp")["pol"]["block_h"] == init_block_height # mine some, BUT not enough to satisfy velocity policy # - check velocity is exactly right to block number vs. required gap bitcoind.supply_wallet.generatetoaddress(blocks - 1, bitcoind.supply_wallet.getnewaddress()) block_height = bitcoind.supply_wallet.getblockchaininfo()["blocks"] psbt_resp = wo.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 1}], block_height) psbt = psbt_resp.get("psbt") po = BasicPSBT().parse(base64.b64decode(psbt)) assert po.parsed_txn.nLockTime == block_height policy_sign(wo, psbt, violation="velocity") assert settings_get("sssp")["pol"]["block_h"] == init_block_height # still initial block height as above failed # mine the remaining one block to satisfy velocity policy bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) block_height = bitcoind.supply_wallet.getblockchaininfo()["blocks"] psbt_resp = wo.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 1}], block_height) psbt = psbt_resp.get("psbt") po = BasicPSBT().parse(base64.b64decode(psbt)) assert po.parsed_txn.nLockTime == block_height policy_sign(wo, psbt) # success assert settings_get("sssp")["pol"]["block_h"] == block_height # updated block height # check txn re-sign fails (if velocity in effect) policy_sign(wo, psbt, violation="rewound") # check decreasing nLockTime policy_sign( wo, wo.walletcreatefundedpsbt( [], [{bitcoind.supply_wallet.getnewaddress(): 1}], block_height - 1 )["psbt"], violation="rewound" ) # check nLockTime disabled when velocity enabled - fail policy_sign( wo, wo.walletcreatefundedpsbt( [], [{bitcoind.supply_wallet.getnewaddress(): 1}], 0 )["psbt"], violation="no nLockTime" ) # unix timestamp policy_sign( wo, wo.walletcreatefundedpsbt( [], [{bitcoind.supply_wallet.getnewaddress(): 1}], 500000000 )["psbt"], violation="nLockTime not height" ) @pytest.mark.bitcoind @pytest.mark.parametrize("active", [True, False]) def test_warnings(setup_sssp, bitcoind, settings_set, policy_sign, pick_menu_item, bitcoind_d_sim_watch, settings_get, press_select, active): wo = bitcoind_d_sim_watch wo.keypoolrefill(20) settings_set("chain", "XRT") whitelist = ["bcrt1qlk39jrclgnawa42tvhu2n7se987qm96qg8v76e", "2Mxp1Dy2MyR4w36J2VaZhrFugNNFgh6LC1j", "mjR14oKxYzRg9RAZdpu3hrw8zXfFgGzLKm"] setup_sssp("11-11", mag=10000000, vel='6 blocks (hour)', whitelist=whitelist) if active: pick_menu_item("ACTIVATE") press_select() else: # demonstration that policy is in effect from configuration # user does not need to activate (or test-drive) and policy in effect already pass bitcoind.supply_wallet.sendtoaddress(address=wo.getnewaddress(), amount=2) bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) # create funded PSBT, first tx # whitelist OK, velocity OK, & magnitude OK - but fee high init_block_height = bitcoind.supply_wallet.getblockchaininfo()["blocks"] # block height psbt_resp = wo.walletcreatefundedpsbt([], [{whitelist[0]: 0.06},{whitelist[1]: 0.01},{whitelist[2]: 0.03}], init_block_height, {"fee_rate":48000}) psbt = psbt_resp.get("psbt") po = BasicPSBT().parse(base64.b64decode(psbt)) assert po.parsed_txn.nLockTime == init_block_height policy_sign(wo, psbt, violation="has warnings") # invalidate nLockTime with use of nSequence max values utxos = wo.listunspent() ins = [] for i, utxo in enumerate(utxos): # block height based RTL inp = { "txid": utxo["txid"], "vout": utxo["vout"], "sequence": 0xffffffff, } ins.append(inp) psbt_resp = wo.walletcreatefundedpsbt(ins, [{whitelist[0]: 0.06},{whitelist[1]: 0.01},{whitelist[2]: 0.03}], 0, {"fee_rate":2, "replaceable": False}) # locktime needs to be zero, otherwise exception from core (contradicting parameters) po = BasicPSBT().parse(base64.b64decode(psbt_resp.get("psbt"))) assert po.parsed_txn.nLockTime == 0 po.parsed_txn.nLockTime = init_block_height # add locktime po.txn = po.parsed_txn.serialize_with_witness() # num_warn=2, warn_list=["Bad Locktime"] policy_sign(wo, po.as_b64_str(), violation="has warnings") # exotic sighash warning settings_set("sighshchk", 1) # needed to only get warning instead of failure psbt_resp = wo.walletcreatefundedpsbt([], [{whitelist[0]: 0.06},{whitelist[1]: 0.01},{whitelist[2]: 0.03}], init_block_height, {"fee_rate":2, "replaceable": True}) po = BasicPSBT().parse(base64.b64decode(psbt_resp.get("psbt"))) for idx, i in enumerate(po.inputs): i.sighash = 2 # NONE # num_warn=2, warn_list=["sighash NONE"] policy_sign(wo, po.as_b64_str(), violation="has warnings") def test_remove_sssp(setup_sssp, pick_menu_item, press_select, cap_story, cap_menu, settings_get): setup_sssp("11-11", mag=10000000, vel='6 blocks (hour)') # check test drive pick_menu_item("Test Drive") time.sleep(.1) _, story = cap_story() assert "COLDCARD operation will look like with Spending Policy" in story press_select() time.sleep(.1) m = cap_menu() assert "EXIT TEST DRIVE" in m assert "Settings" not in m pick_menu_item("EXIT TEST DRIVE") time.sleep(.1) m = cap_menu() assert "Edit Policy..." in m # back in policy settings pick_menu_item("Remove Policy") time.sleep(.1) _, story = cap_story() assert "Bypass PIN will be removed" in story assert "spending policy settings forgotten" in story press_select() time.sleep(.1) assert not settings_get("sssp") tps = settings_get("tp") if tps: assert "11-11" not in tps assert not settings_get("sssp") def test_use_main_pin_as_unlock(setup_sssp, cap_story): # not allowed # simulator PIN with pytest.raises(Exception): setup_sssp("12-12") _, story = cap_story() assert "already in use" in story assert "PIN codes must be unique" in story @pytest.mark.parametrize("hide", [True, False]) def test_use_trick_pin_as_unlock(hide, setup_sssp, cap_story, new_trick_pin, pick_menu_item, press_select, clear_all_tricks): clear_all_tricks() pin = "11-11" new_trick_pin(pin, 'Wipe Seed', 'Wipe the seed and maybe do more') pick_menu_item('Wipe & Reboot') press_select() press_select() if hide: pick_menu_item(f"↳{pin}") pick_menu_item("Hide Trick") press_select() # confirm with pytest.raises(Exception): setup_sssp(pin) _, story = cap_story() assert "already in use" in story assert "PIN codes must be unique" in story @pytest.mark.parametrize("active_policy", [False, True]) def test_deltamode_signature(active_policy, setup_sssp, bitcoind, settings_set, start_sign, end_sign, pick_menu_item, press_select, set_deltamode, bitcoind_d_sim_watch, settings_get): # verify that "deltamode" trick pins will work in SSSP mode # - and that resulting signature is bad # - device should **not** wipe itself dest = "bcrt1qlk39jrclgnawa42tvhu2n7se987qm96qg8v76e" wo = bitcoind_d_sim_watch wo.keypoolrefill(20) settings_set("chain", "XRT") if active_policy: setup_sssp(f"{random.randint(0,99)}-11", mag=100) pick_menu_item("ACTIVATE") press_select() bitcoind.supply_wallet.sendtoaddress(address=wo.getnewaddress(), amount=2) bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) # create funded PSBT, first tx # - within active policy. init_block_height = bitcoind.supply_wallet.getblockchaininfo()["blocks"] # block height psbt_resp = wo.walletcreatefundedpsbt([], [{dest: 0.06}], init_block_height, {"fee_rate":2, "replaceable": False}) psbt = psbt_resp.get("psbt") po = BasicPSBT().parse(base64.b64decode(psbt)) assert po.parsed_txn.nLockTime == init_block_height start_sign(base64.b64decode(psbt), finalize=True) signed = end_sign(accept=True, finalize=True) set_deltamode(True) start_sign(base64.b64decode(psbt), finalize=True) signed2 = end_sign(accept=True, finalize=True) # check wrong signature happened assert signed != signed2 probs = wo.testmempoolaccept([signed2.hex()])[0] try: # old bitcoind assert 'Signature must be zero' in probs['reject-reason'], probs except AssertionError: assert 'mandatory-script-verify-flag-failed' in probs['reject-reason'], probs assert not probs['allowed'] # check right signature no_probs = wo.testmempoolaccept([signed.hex()])[0] assert no_probs['allowed'] @pytest.mark.bitcoind def test_sssp_enforce_tmp_seed(setup_sssp, bitcoind, settings_set, settings_get, press_select, pick_menu_item, cap_menu, go_to_passphrase, enter_complex, need_keypress, word_menu_entry, fake_txn, start_sign, dev, cap_story, get_last_violation, end_sign): tmp_words = "style car win bomb plug raccoon predict warm wrap flush usual seminar" blocks = 6 # ~1 hour settings_set("chain", "XRT") setup_sssp("11-11", mag=2, vel='6 blocks (hour)', rel_keys=True) assert "block_h" not in settings_get("sssp")["pol"] pick_menu_item("ACTIVATE") press_select() time.sleep(.1) m = cap_menu() # check we are in hobbled mode & okeys is respected assert "Passphrase" in m assert "Settings" not in m # import word-based seed as tmp and check that sssp is enforced pick_menu_item("Advanced/Tools") pick_menu_item("Temporary Seed") need_keypress("4") pick_menu_item("Import Words") pick_menu_item("12 Words") word_menu_entry(tmp_words.split()) press_select() time.sleep(.1) m = cap_menu() assert "Passphrase" in m # word based + okeys assert "Settings" not in m xpub = dev.send_recv(CCProtocolPacker.get_xpub("m"), timeout=None) psbt = fake_txn(2,2, input_amount=200000000, master_xpub=xpub) start_sign(psbt) time.sleep(.1) _, story = cap_story() assert "Spending Policy violation" in story press_select() # recurse deeper, to passphrase wallet, on top of word-based tmp seed go_to_passphrase() enter_complex("AAA", apply=True) press_select() m = cap_menu() assert "Passphrase" not in m # xprv based assert "Settings" not in m # still in hobbled xpub1 = dev.send_recv(CCProtocolPacker.get_xpub("m"), timeout=None) psbt = fake_txn(2, 2, input_amount=200000000, master_xpub=xpub1) start_sign(psbt) time.sleep(.1) _, story = cap_story() assert "Spending Policy violation" in story press_select() time.sleep(.1) # try success signing psbt = fake_txn(2, 2, input_amount=1000000, master_xpub=xpub1, lock_time=50) start_sign(psbt) time.sleep(.1) title, story = cap_story() assert title == 'OK TO SEND?' assert "Spending Policy violation" not in story assert end_sign() # go back to previous temporary seed and verify block_h was updated pick_menu_item("Advanced/Tools") pick_menu_item("Temporary Seed") need_keypress("4") pick_menu_item("Import Words") pick_menu_item("12 Words") word_menu_entry(tmp_words.split()) press_select() # lock time is still 50 - as in previous case = rewound psbt = fake_txn(2, 2, input_amount=1000000, master_xpub=xpub, lock_time=50) start_sign(psbt) time.sleep(.1) title, story = cap_story() assert "Spending Policy violation" in story assert get_last_violation() == "rewound (50)" press_select() time.sleep(.1) # bump locktime psbt = fake_txn(2, 2, input_amount=1000000, master_xpub=xpub, lock_time=56) start_sign(psbt) time.sleep(.1) title, story = cap_story() assert title == 'OK TO SEND?' assert end_sign() time.sleep(.1) pick_menu_item("Restore Master") press_select() time.sleep(.1) m = cap_menu() assert "Passphrase" in m assert "Settings" not in m # still in hobbled psbt = fake_txn(2, 2, input_amount=200000000, lock_time=150) start_sign(psbt) time.sleep(.1) _, story = cap_story() assert "Spending Policy violation" in story press_select() # lock time is still 56 - as in previous case = rewound psbt = fake_txn(2, 2, input_amount=1000000, lock_time=56) start_sign(psbt) time.sleep(.1) title, story = cap_story() assert "Spending Policy violation" in story assert get_last_violation() == "rewound (56)" press_select() time.sleep(.1) # bump locktime psbt = fake_txn(2, 2, input_amount=1000000, lock_time=70) start_sign(psbt) time.sleep(.1) title, story = cap_story() assert title == 'OK TO SEND?' assert end_sign() time.sleep(.1) def test_sssp_notes_enable(only_q1, setup_sssp): # just test menu item works setup_sssp("11-11", mag=2, vel='6 blocks (hour)', notes_and_pws=True) def test_sssp_word_check(setup_sssp): # just test menu item works setup_sssp("11-11", mag=2, vel='6 blocks (hour)', word_check=True) @pytest.mark.bitcoind def test_ccc_with_sssp_block_h(setup_ccc, ccc_ms_setup, setup_sssp, bitcoind, policy_sign, settings_get, settings_set, bitcoind_create_watch_only_wallet, pick_menu_item, press_select, cap_story): settings_set("ccc", None) settings_set("sssp", None) settings_set("multisig", []) settings_set("chain", "XRT") setup_ccc(mag=10, vel='Unlimited') _, target_mi = ccc_ms_setup() bitcoind_wo = bitcoind_create_watch_only_wallet(target_mi) setup_sssp(pin="11-11", mag=10, vel='48 blocks (8h)') pick_menu_item("Test Drive") time.sleep(.1) _, story = cap_story() assert "COLDCARD operation will look like with Spending Policy" in story press_select() multi_addr = bitcoind_wo.getnewaddress() bitcoind.supply_wallet.sendtoaddress(address=multi_addr, amount=49) bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) cur_h = bitcoind.supply_wallet.getblockchaininfo()["blocks"] psbt1 = bitcoind_wo.walletcreatefundedpsbt( [], [{bitcoind.supply_wallet.getnewaddress(): 1}], cur_h )["psbt"] policy_sign(bitcoind_wo, psbt1) assert cur_h == settings_get("sssp")["pol"]["block_h"] assert cur_h == settings_get("ccc")["pol"]["block_h"] baseline_block_h = cur_h bitcoind.supply_wallet.generatetoaddress(5, bitcoind.supply_wallet.getnewaddress()) # second signing -> SSSP velocity BLOCKS but CCC overrides and signing allowed chosen_lock_time = baseline_block_h + 1 psbt2 = bitcoind_wo.walletcreatefundedpsbt( [], [{bitcoind.supply_wallet.getnewaddress(): 1}], chosen_lock_time )["psbt"] policy_sign(bitcoind_wo, psbt2) assert chosen_lock_time == settings_get("ccc")["pol"]["block_h"] # SSSP block_h is updated too assert chosen_lock_time == settings_get("sssp")["pol"]["block_h"] pick_menu_item("EXIT TEST DRIVE") settings_set("ccc", None) settings_set("sssp", None) # EOF