ccc velocity
This commit is contained in:
parent
6df24f646d
commit
869e317db8
@ -77,7 +77,7 @@ class CCCFeature:
|
||||
def default_policy(cls):
|
||||
# a very basic an permissive policy, but non-zero too.
|
||||
# - 1BTC per day
|
||||
return dict(mag=1, vel=144, web2fa='', addrs=[])
|
||||
return dict(mag=1, vel=144, vel_block_h=0, web2fa='', addrs=[])
|
||||
|
||||
@classmethod
|
||||
def get_policy(cls):
|
||||
@ -130,6 +130,15 @@ class CCCFeature:
|
||||
raise CCCPolicyViolationError("magnitude")
|
||||
|
||||
# vel
|
||||
velocity = pol.get("vel", None)
|
||||
if velocity: # if zero - unlimited
|
||||
if psbt.lock_time >= 500000000:
|
||||
# this is unix timestamp - not allowed - fail
|
||||
raise CCCPolicyViolationError("velocity not block height")
|
||||
|
||||
# off by one possibility - we need to decide whether we want it to be <= or just <
|
||||
if psbt.lock_time < (pol.get("vel_block_h", 0) + velocity):
|
||||
raise CCCPolicyViolationError("velocity")
|
||||
|
||||
# whitelist
|
||||
wl = pol.get("addrs", None)
|
||||
@ -197,10 +206,14 @@ class CCCFeature:
|
||||
@classmethod
|
||||
def sign_psbt(cls, psbt):
|
||||
# do the math
|
||||
# TODO: capture the block height if vel is defined; no going back after this pt.
|
||||
psbt.sign_it(cls.get_encoded_secret(), cls.get_xfp())
|
||||
cls.last_fail_reason = None
|
||||
|
||||
velocity = cls.get_policy().get("vel", None)
|
||||
if velocity:
|
||||
# preserve velocity settings & update last block height
|
||||
cls.update_policy_key(vel_block_h=psbt.lock_time)
|
||||
|
||||
|
||||
def render_mag_value(mag):
|
||||
# handle integer bitcoins, and satoshis in same value
|
||||
@ -575,14 +588,17 @@ class CCCPolicyMenu(MenuSystem):
|
||||
# reminder: dont forget the poor Mk4 users
|
||||
# xxxxxxxxxxxxxxxx
|
||||
ch = [ 'Unlimited',
|
||||
'6 blocks (1 hr)',
|
||||
'6 blocks (1h)',
|
||||
'24 blocks (4h)',
|
||||
'48 blocks (8h)',
|
||||
'72 blocks (12h)',
|
||||
'144 blocks (day)',
|
||||
'144 blocks (1d)',
|
||||
'288 blocks (2d)',
|
||||
'432 blocks (3d)',
|
||||
'1008 blocks (wk)',
|
||||
'720 blocks (5d)',
|
||||
'1008 blocks (1w)',
|
||||
'2016 blocks (2w)',
|
||||
'4032 blocks (4w)',
|
||||
]
|
||||
va = [0] + [int(x.split()[0]) for x in ch[1:]]
|
||||
|
||||
|
||||
@ -376,7 +376,7 @@ class BasicPSBT:
|
||||
# auto-detect and decode Base64 and Hex.
|
||||
if raw[0:10].lower() == b'70736274ff':
|
||||
raw = a2b_hex(raw.strip())
|
||||
if raw[0:6] == b'cHNidP':
|
||||
if raw[0:6] in (b'cHNidP', 'cHNidP'):
|
||||
raw = b64decode(raw)
|
||||
assert raw[0:5] == b'psbt\xff', "bad magic {}".format(raw[0:5])
|
||||
with io.BytesIO(raw[5:]) as fd:
|
||||
|
||||
@ -222,14 +222,24 @@ def setup_ccc(goto_home, pick_menu_item, cap_story, press_select, pass_word_quiz
|
||||
assert f"{mag} {'BTC' if int(mag) < 1000 else 'SATS'}" in story
|
||||
press_select()
|
||||
|
||||
assert settings_get("ccc")["pol"]["mag"] == mag
|
||||
|
||||
if vel:
|
||||
if not mag:
|
||||
if not settings_get("ccc")["pol"]["mag"]:
|
||||
title, story = cap_story()
|
||||
assert 'Velocity limit requires' in story
|
||||
assert 'starting value' in story
|
||||
press_select()
|
||||
|
||||
pick_menu_item(vel_mi)
|
||||
pick_menu_item(vel) # actually a full menu item
|
||||
if vel == "Unlimited":
|
||||
target = 0
|
||||
else:
|
||||
target = int(vel.split()[0])
|
||||
|
||||
time.sleep(.2)
|
||||
assert settings_get("ccc")["pol"]["vel"] == target
|
||||
|
||||
if whitelist:
|
||||
pick_menu_item(whitelist_mi)
|
||||
@ -277,9 +287,6 @@ def setup_ccc(goto_home, pick_menu_item, cap_story, press_select, pass_word_quiz
|
||||
pick_menu_item(mi_2fa)
|
||||
|
||||
|
||||
|
||||
# TODO check settings object data
|
||||
|
||||
press_cancel() # leave Spending Policy
|
||||
|
||||
return c_words
|
||||
@ -425,14 +432,46 @@ def bitcoind_create_watch_only_wallet(pick_menu_item, need_keypress, microsd_pat
|
||||
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()
|
||||
assert 'OK TO SEND?' == title
|
||||
if violation:
|
||||
assert "(1 warning below)" in story
|
||||
assert "CCC: Violates spending policy. Won't sign." in story
|
||||
assert get_last_violation() == violation
|
||||
else:
|
||||
assert "warning" not in story
|
||||
|
||||
signed = end_sign(accept=True)
|
||||
po = BasicPSBT().parse(signed)
|
||||
|
||||
if violation is None:
|
||||
assert len(po.inputs[0].part_sigs) == 2 # CC key signed
|
||||
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
|
||||
else:
|
||||
assert len(po.inputs[0].part_sigs) == 1 # CC key did NOT sign
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.mark.bitcoind
|
||||
@pytest.mark.parametrize("mag_ok", [True, False])
|
||||
@pytest.mark.parametrize("mag", [1000000, None, 2])
|
||||
def test_ccc_magnitude(mag_ok, mag, setup_ccc, enter_enabled_ccc, ccc_ms_setup, start_sign,
|
||||
cap_menu, cap_story, bitcoind, end_sign, settings_set,
|
||||
bitcoind_create_watch_only_wallet, get_last_violation):
|
||||
def test_ccc_magnitude(mag_ok, mag, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
|
||||
cap_menu, bitcoind, settings_set, policy_sign,
|
||||
bitcoind_create_watch_only_wallet):
|
||||
|
||||
settings_set("ccc", None)
|
||||
settings_set("chain", "XRT")
|
||||
|
||||
if mag_ok:
|
||||
# always try limit/border value
|
||||
@ -446,7 +485,7 @@ def test_ccc_magnitude(mag_ok, mag, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
|
||||
else:
|
||||
to_send = ((mag / 100000000)+1) if mag > 1000 else (mag+0.001)
|
||||
|
||||
words = setup_ccc(mag=mag)
|
||||
words = setup_ccc(mag=mag, vel="Unlimited")
|
||||
enter_enabled_ccc(words, first_time=True)
|
||||
ccc_ms_setup()
|
||||
|
||||
@ -469,37 +508,13 @@ def test_ccc_magnitude(mag_ok, mag, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
|
||||
)
|
||||
psbt = psbt_resp.get("psbt")
|
||||
|
||||
start_sign(base64.b64decode(psbt))
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert 'OK TO SEND?' == title
|
||||
if not mag_ok:
|
||||
assert "(1 warning below)" in story
|
||||
assert "CCC: Violates spending policy. Won't sign." in story
|
||||
assert get_last_violation() == 'magnitude'
|
||||
else:
|
||||
assert "warning" not in story
|
||||
|
||||
signed = end_sign(accept=True)
|
||||
po = BasicPSBT().parse(signed)
|
||||
|
||||
if mag_ok:
|
||||
assert len(po.inputs[0].part_sigs) == 2 # CC key signed
|
||||
res = bitcoind_wo.finalizepsbt(base64.b64encode(signed).decode())
|
||||
assert res["complete"]
|
||||
tx_hex = res["hex"]
|
||||
res = bitcoind_wo.testmempoolaccept([tx_hex])
|
||||
assert res[0]["allowed"]
|
||||
res = bitcoind_wo.sendrawtransaction(tx_hex)
|
||||
assert len(res) == 64 # tx id
|
||||
else:
|
||||
assert len(po.inputs[0].part_sigs) == 1 # CC key did NOT sign
|
||||
policy_sign(bitcoind_wo, psbt, violation=None if mag_ok else "magnitude")
|
||||
|
||||
|
||||
@pytest.mark.bitcoind
|
||||
@pytest.mark.parametrize("whitelist_ok", [True, False])
|
||||
def test_ccc_whitelist(whitelist_ok, setup_ccc, enter_enabled_ccc, ccc_ms_setup, start_sign,
|
||||
cap_menu, cap_story, bitcoind, end_sign, settings_set,
|
||||
def test_ccc_whitelist(whitelist_ok, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
|
||||
cap_menu, bitcoind, settings_set, policy_sign,
|
||||
bitcoind_create_watch_only_wallet):
|
||||
|
||||
settings_set("ccc", None)
|
||||
@ -517,7 +532,7 @@ def test_ccc_whitelist(whitelist_ok, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
|
||||
else:
|
||||
send_to = bitcoind.supply_wallet.getnewaddress()
|
||||
|
||||
words = setup_ccc(whitelist=whitelist)
|
||||
words = setup_ccc(whitelist=whitelist, vel="Unlimited")
|
||||
enter_enabled_ccc(words, first_time=True)
|
||||
ccc_ms_setup()
|
||||
|
||||
@ -539,30 +554,72 @@ def test_ccc_whitelist(whitelist_ok, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
|
||||
[], [{send_to: 1}], 0, {"fee_rate": 2}
|
||||
)
|
||||
psbt = psbt_resp.get("psbt")
|
||||
policy_sign(bitcoind_wo, psbt, violation=None if whitelist_ok else "whitelist")
|
||||
|
||||
start_sign(base64.b64decode(psbt))
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert 'OK TO SEND?' == title
|
||||
if not whitelist_ok:
|
||||
assert "(1 warning below)" in story
|
||||
assert "CCC: Violates spending policy - whitelist. Won't sign." in story
|
||||
|
||||
@pytest.mark.bitcoind
|
||||
@pytest.mark.parametrize("velocity_mi", ['6 blocks (1h)', '48 blocks (8h)'])
|
||||
def test_ccc_velocity(velocity_mi, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
|
||||
cap_menu, bitcoind, settings_set, policy_sign,
|
||||
bitcoind_create_watch_only_wallet, settings_get):
|
||||
|
||||
settings_set("ccc", None)
|
||||
settings_set("chain", "XRT")
|
||||
|
||||
blocks = int(velocity_mi.split()[0])
|
||||
|
||||
words = setup_ccc(vel=velocity_mi)
|
||||
enter_enabled_ccc(words, first_time=True)
|
||||
ccc_ms_setup()
|
||||
|
||||
assert settings_get("ccc")["pol"]["vel_block_h"] == 0
|
||||
|
||||
m = cap_menu()
|
||||
for mi in m:
|
||||
if "2/3: Coldcard Cosign" in mi:
|
||||
target_mi = mi
|
||||
break
|
||||
else:
|
||||
assert "warning" not in story
|
||||
assert False
|
||||
|
||||
signed = end_sign(accept=True)
|
||||
po = BasicPSBT().parse(signed)
|
||||
bitcoind_wo = bitcoind_create_watch_only_wallet(target_mi)
|
||||
|
||||
if whitelist_ok:
|
||||
assert len(po.inputs[0].part_sigs) == 2 # CC key signed
|
||||
res = bitcoind_wo.finalizepsbt(base64.b64encode(signed).decode())
|
||||
assert res["complete"]
|
||||
tx_hex = res["hex"]
|
||||
res = bitcoind_wo.testmempoolaccept([tx_hex])
|
||||
assert res[0]["allowed"]
|
||||
res = bitcoind_wo.sendrawtransaction(tx_hex)
|
||||
assert len(res) == 64 # tx id
|
||||
else:
|
||||
assert len(po.inputs[0].part_sigs) == 1 # CC key did NOT sign
|
||||
multi_addr = bitcoind_wo.getnewaddress()
|
||||
bitcoind.supply_wallet.sendtoaddress(address=multi_addr, amount=5.0)
|
||||
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 = bitcoind_wo.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 1}],
|
||||
init_block_height) # nLockTime set to current block height
|
||||
psbt = psbt_resp.get("psbt")
|
||||
po = BasicPSBT().parse(psbt)
|
||||
assert po.parsed_txn.nLockTime == init_block_height
|
||||
policy_sign(bitcoind_wo, psbt) # success as this is first tx that sets block height from 0
|
||||
|
||||
assert settings_get("ccc")["pol"]["vel_block_h"] == init_block_height
|
||||
|
||||
# mine some, BUT not enough to satisfy velocity policy
|
||||
bitcoind.supply_wallet.generatetoaddress(blocks - 1, bitcoind.supply_wallet.getnewaddress())
|
||||
block_height = bitcoind.supply_wallet.getblockchaininfo()["blocks"]
|
||||
psbt_resp = bitcoind_wo.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 1}],
|
||||
block_height)
|
||||
psbt = psbt_resp.get("psbt")
|
||||
po = BasicPSBT().parse(psbt)
|
||||
assert po.parsed_txn.nLockTime == block_height
|
||||
policy_sign(bitcoind_wo, psbt, violation="velocity")
|
||||
|
||||
assert settings_get("ccc")["pol"]["vel_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 = bitcoind_wo.walletcreatefundedpsbt([], [{bitcoind.supply_wallet.getnewaddress(): 1}],
|
||||
block_height)
|
||||
psbt = psbt_resp.get("psbt")
|
||||
po = BasicPSBT().parse(psbt)
|
||||
assert po.parsed_txn.nLockTime == block_height
|
||||
policy_sign(bitcoind_wo, psbt) # success
|
||||
|
||||
assert settings_get("ccc")["pol"]["vel_block_h"] == block_height # updated block height
|
||||
|
||||
# EOF
|
||||
|
||||
Loading…
Reference in New Issue
Block a user