#! /usr/bin/env python '''Tests of Proof of discrete log equivalence commitments.''' import os import struct import json import pytest import copy from unittest import IsolatedAsyncioTestCase import jmbitcoin as bitcoin from jmbase import get_log, bintohex from jmclient import load_test_config, jm_single, generate_podle,\ generate_podle_error_string, get_commitment_file, PoDLE,\ get_podle_commitments, add_external_commitments, update_commitments from jmclient.podle import verify_all_NUMS, verify_podle, PoDLEError from commontest import make_wallets pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind") log = get_log() def generate_single_podle_sig(priv, i): """Make a podle entry for key priv at index i, using a dummy utxo value. This calls the underlying 'raw' code based on the class PoDLE, not the library 'generate_podle' which intelligently searches and updates commitments. """ dummy_utxo = bitcoin.b2x(bitcoin.Hash(priv)) + ":3" podle = PoDLE(dummy_utxo, priv) r = podle.generate_podle(i) return (r['P'], r['P2'], r['sig'], r['e'], r['commit']) class AsyncioTestCase(IsolatedAsyncioTestCase): async def asyncSetUp(self): load_test_config() if not os.path.exists("cmtdata"): os.mkdir("cmtdata") self.prev_commits = False #back up any existing commitments pcf = get_commitment_file() log.debug("Podle file: " + pcf) if os.path.exists(pcf): os.rename(pcf, pcf + ".bak") self.prev_commits = True async def asyncTearDown(self): pcf = get_commitment_file() if self.prev_commits: os.rename(pcf + ".bak", pcf) else: if os.path.exists(pcf): os.remove(pcf) async def test_commitments_empty(self): """Ensure that empty commitments file results in {} """ assert get_podle_commitments() == ([], {}) async def test_commitment_retries(self): """Assumes no external commitments available. Generate pretend priv/utxo pairs and check that they can be used taker_utxo_retries times. """ allowed = jm_single().config.getint("POLICY", "taker_utxo_retries") #make some pretend commitments dummy_priv_utxo_pairs = [(bitcoin.Hash(os.urandom(10)), bitcoin.b2x(bitcoin.Hash(os.urandom(10)))+":0") for _ in range(10)] #test a single commitment request of all 10 for x in dummy_priv_utxo_pairs: p = generate_podle([x], allowed) assert p #At this point slot 0 has been taken by all 10. for i in range(allowed-1): p = generate_podle(dummy_priv_utxo_pairs[:1], allowed) assert p p = generate_podle(dummy_priv_utxo_pairs[:1], allowed) assert p is None async def test_rand_commitments(self): for i in range(20): priv = os.urandom(32)+b"\x01" Pser, P2ser, s, e, commitment = generate_single_podle_sig(priv, 1 + i%5) assert verify_podle(Pser, P2ser, s, e, commitment) #tweak commitments to verify failure tweaked = [x[::-1] for x in [Pser, P2ser, s, e, commitment]] for i in range(5): #Check failure on garbling of each parameter y = [Pser, P2ser, s, e, commitment] y[i] = tweaked[i] fail = False try: fail = verify_podle(*y) except: pass finally: assert not fail async def test_nums_verify(self): """Check that the NUMS precomputed values are valid according to the code; assertion check implicit. """ verify_all_NUMS(True) async def test_external_commitments(self): """Add this generated commitment to the external list {txid:N:{'P':pubkey, 'reveal':{1:{'P2':P2,'s':s,'e':e}, 2:{..},..}}} Note we do this *after* the sendpayment test so that the external commitments will not erroneously used (they are fake). """ #ensure the file exists even if empty update_commitments() ecs = {} tries = jm_single().config.getint("POLICY","taker_utxo_retries") for i in range(10): priv = os.urandom(32) dummy_utxo = (bitcoin.Hash(priv), 2) ecs[dummy_utxo] = {} ecs[dummy_utxo]['reveal']={} for j in range(tries): P, P2, s, e, commit = generate_single_podle_sig(priv, j) if 'P' not in ecs[dummy_utxo]: ecs[dummy_utxo]['P']=P ecs[dummy_utxo]['reveal'][j] = {'P2':P2, 's':s, 'e':e} add_external_commitments(ecs) used, external = get_podle_commitments() for u in external: assert external[u]['P'] == ecs[u]['P'] for i in range(tries): for x in ['P2', 's', 'e']: assert external[u]['reveal'][i][x] == ecs[u]['reveal'][i][x] #add a dummy used commitment, then try again update_commitments(commitment=b"\xab"*32) ecs = {} known_commits = [] known_utxos = [] tries = 3 for i in range(1, 6): u = (struct.pack(b'B', i)*32, i+3) known_utxos.append(u) priv = struct.pack(b'B', i)*32+b"\x01" ecs[u] = {} ecs[u]['reveal']={} for j in range(tries): P, P2, s, e, commit = generate_single_podle_sig(priv, j) known_commits.append(commit) if 'P' not in ecs[u]: ecs[u]['P'] = P ecs[u]['reveal'][j] = {'P2':P2, 's':s, 'e':e} add_external_commitments(ecs) #simulate most of those external being already used for c in known_commits[:-1]: update_commitments(commitment=c) #this should find the remaining one utxo and return from it assert generate_podle([], max_tries=tries, allow_external=known_utxos) #test commitment removal tru = (struct.pack(b"B", 3)*32, 3+3) to_remove = {tru: ecs[tru]} update_commitments(external_to_remove=to_remove) #test that an incorrectly formatted file raises with open(get_commitment_file(), "rb") as f: validjson = json.loads(f.read().decode('utf-8')) corruptjson = copy.deepcopy(validjson) del corruptjson['used'] with open(get_commitment_file(), "wb") as f: f.write(json.dumps(corruptjson, indent=4).encode('utf-8')) with pytest.raises(PoDLEError) as e_info: get_podle_commitments() #clean up with open(get_commitment_file(), "wb") as f: f.write(json.dumps(validjson, indent=4).encode('utf-8')) async def test_podle_constructor(self): """Tests rules about construction of PoDLE object are conformed to. """ priv = b"\xaa"*32 #pub and priv together not allowed with pytest.raises(PoDLEError) as e_info: p = PoDLE(priv=priv, P="dummypub") #no pub or priv is allowed, i forget if this is useful for something p = PoDLE() #create from priv p = PoDLE(priv=priv+b"\x01", u=(struct.pack(b"B", 7)*32, 4)) pdict = p.generate_podle(2) assert all([k in pdict for k in ['used', 'utxo', 'P', 'P2', 'commit', 'sig', 'e']]) #using the valid data, serialize/deserialize test deser = p.deserialize_revelation(p.serialize_revelation()) assert all([deser[x] == pdict[x] for x in ['utxo', 'P', 'P2', 'sig', 'e']]) #deserialization must fail for wrong number of items with pytest.raises(PoDLEError) as e_info: p.deserialize_revelation(':'.join([str(x) for x in range(4)]), separator=':') #reveal() must work without pre-generated commitment p.commitment = None pdict2 = p.reveal() assert pdict2 == pdict #corrupt P2, cannot commit: p.P2 = "blah" with pytest.raises(PoDLEError) as e_info: p.get_commitment() #generation fails without a utxo p = PoDLE(priv=priv) with pytest.raises(PoDLEError) as e_info: p.generate_podle(0) #Test construction from pubkey pub = bitcoin.privkey_to_pubkey(priv+b"\x01") p = PoDLE(P=pub) with pytest.raises(PoDLEError) as e_info: p.get_commitment() with pytest.raises(PoDLEError) as e_info: p.verify("dummycommitment", range(3)) async def test_podle_error_string(self): example_utxos = [(b"\x00"*32, i) for i in range(6)] priv_utxo_pairs = [('fakepriv1', example_utxos[0]), ('fakepriv2', example_utxos[1])] to = example_utxos[2:4] ts = example_utxos[4:6] wallets = await make_wallets(1, [[1, 0, 0, 0, 0]]) wallet_service = wallets[0]['wallet'] cjamt = 100 tua = "3" tuamtper = "20" errmgsheader, errmsg = await generate_podle_error_string( priv_utxo_pairs, to, ts, wallet_service, cjamt, tua, tuamtper) assert errmgsheader == ("Failed to source a commitment; this debugging information" " may help:\n\n") y = [bintohex(x[0]) for x in example_utxos] assert all([errmsg.find(x) != -1 for x in y]) #ensure OK with nothing errmgsheader, errmsg = await generate_podle_error_string( [], [], [], wallet_service, cjamt, tua, tuamtper)