What changed, and why it matters
This commit is a routine test-suite addition for the COLDCARD's Single Signer Spending Policy (SSSP) feature. It adds automated tests that verify the spending policy correctly allows or blocks transactions based on configured limits (amount, velocity, whitelist). The only production code change is removing an unreachable 'return default' line after a function that always raises an exception, which has no security effect.
No action required; this is a benign test-only commit with a trivial dead-code cleanup.
Security signals we found
No security-relevant production code change
Dead code removal only (unreachable return after raise)
Test-only addition for spending policy enforcement
Evidence from the diff
The commit introduces testing/test_sssp.py with pytest fixtures and test cases covering SSSP magnitude, whitelist, velocity, warning, removal, and PIN-uniqueness scenarios. It also imports get_last_violation from test_ccc in conftest.py. The single change to shared/ccc.py deletes a dead ‘return default’ statement immediately following a ‘raise KeyError(key)’ in sssp_spending_policy(), so it is unreachable and behaviorally neutral.
Changed components
shared/ccc.py (dead code removal)testing/conftest.py (test import)testing/test_sssp.py (new test file)Inspect captured patch +549 / −1
diff --git a/shared/ccc.py b/shared/ccc.py
index 90eb801..be7f2be 100644
--- a/shared/ccc.py
+++ b/shared/ccc.py
@@ -1029,7 +1029,6 @@ def sssp_spending_policy(key, default=False, change=None):
raise KeyError(key)
- return default
async def sssp_feature_menu(*a):
# Show the top menu for SSSP feature, or enable access first time.
diff --git a/testing/conftest.py b/testing/conftest.py
index 1fd1d7d..ccfd5c6 100644
--- a/testing/conftest.py
+++ b/testing/conftest.py
@@ -2632,6 +2632,7 @@ def build_test_seed_vault():
from test_backup import backup_system
from test_bbqr import readback_bbqr, render_bbqr, readback_bbqr_ll, try_sign_bbqr, split_scan_bbqr
from test_bip39pw import set_bip39_pw
+from test_ccc import get_last_violation
from test_drv_entro import derive_bip85_secret, activate_bip85_ephemeral
from test_ephemeral import generate_ephemeral_words, import_ephemeral_xprv, goto_eph_seed_menu
from test_ephemeral import ephemeral_seed_disabled_ui, restore_main_seed, confirm_tmp_seed
diff --git a/testing/test_sssp.py b/testing/test_sssp.py
new file mode 100644
index 0000000..e8a137d
--- /dev/null
+++ b/testing/test_sssp.py
@@ -0,0 +1,548 @@
+# (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, os
+from psbt import BasicPSBT
+
+
+@pytest.fixture
+def goto_sssp_menu(goto_home, pick_menu_item, is_mark4):
+ 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):
+
+ 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":
+ 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
+ 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
+ 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:
+ 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
+ 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 "or Seed Vault (if any)" in story
+ if rel_keys:
+ assert "Enable?" in story
+ press_select() # confirm action
+ assert settings_get("sssp")["okeys"]
+ else:
+ assert "Disable?" in story
+ pol = settings_get("sssp")
+ if "okeys" in pol:
+ assert not pol["okeys"]
+
+ return doit
+
+
+@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.fixture
+def remove_settings_slots(settings_slots):
+ for s in settings_slots():
+ try:
+ os.remove(s)
+ except: pass
+
+
+@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, sim_exec, settings_remove,
+ reset_seed_words, settings_path, remove_settings_slots):
+
+ 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")
+
+ # I could use test drive here
+ settings_remove("sssp")
+ sim_exec('from pincodes import pa;pa.hobbled_mode = False; from actions import goto_top_menu; goto_top_menu()')
+
+
+@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, settings_remove, sim_exec):
+
+ 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)
+
+ 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")
+
+ # I could use test drive here
+ settings_remove("sssp")
+ sim_exec('from pincodes import pa;pa.hobbled_mode = False; from actions import goto_top_menu; goto_top_menu()')
+
+
+@pytest.mark.bitcoind
+@pytest.mark.parametrize("velocity_mi", ['6 blocks (hour)', '48 blocks (8h)'])
+def test_velocity(velocity_mi, setup_sssp, bitcoind, settings_set, settings_remove,
+ policy_sign, settings_get, bitcoind_d_sim_watch, sim_exec):
+
+ 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)
+
+ 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"
+ )
+
+ # I could use test drive here
+ settings_remove("sssp")
+ sim_exec('from pincodes import pa;pa.hobbled_mode = False; from actions import goto_top_menu; goto_top_menu()')
+
+
+@pytest.mark.bitcoind
+def test_warnings(setup_sssp, bitcoind, settings_set, policy_sign, sim_exec,
+ bitcoind_d_sim_watch, settings_get, settings_remove):
+
+ 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)
+
+ 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")
+
+ # I could use test drive here
+ settings_remove("sssp")
+ sim_exec('from pincodes import pa;pa.hobbled_mode = False; from actions import goto_top_menu; goto_top_menu()')
+
+
+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()
+ assert not settings_get("sssp")
+ tps = settings_get("tp")
+ if tps:
+ assert "11-11" not in tps
+
+
+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
+
+# EOF
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.