23 changed files with 3526 additions and 94 deletions
@ -0,0 +1,964 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
from pprint import pprint |
||||||
|
|
||||||
|
from hashlib import sha256 |
||||||
|
from unittest import IsolatedAsyncioTestCase |
||||||
|
|
||||||
|
import jmclient # install asyncioreactor |
||||||
|
from twisted.internet import reactor |
||||||
|
|
||||||
|
import pytest |
||||||
|
|
||||||
|
import jmbitcoin as btc |
||||||
|
from jmbase import get_log |
||||||
|
from jmclient import ( |
||||||
|
load_test_config, jm_single, get_network, cryptoengine, VolatileStorage, |
||||||
|
FrostWallet, WalletService) |
||||||
|
from jmclient.frost_clients import DKGClient, FROSTClient |
||||||
|
from jmfrost.chilldkg_ref.chilldkg import ( |
||||||
|
hostpubkey_gen, ParticipantMsg1, CoordinatorMsg1, ParticipantMsg2, |
||||||
|
CoordinatorMsg2) |
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.usefixtures("setup_regtest_frost_bitcoind") |
||||||
|
|
||||||
|
log = get_log() |
||||||
|
|
||||||
|
|
||||||
|
async def get_populated_wallet(entropy=None): |
||||||
|
storage = VolatileStorage() |
||||||
|
dkg_storage = VolatileStorage() |
||||||
|
recovery_storage = VolatileStorage() |
||||||
|
FrostWallet.initialize(storage, dkg_storage, recovery_storage, |
||||||
|
get_network(), entropy=entropy) |
||||||
|
wlt = FrostWallet(storage, dkg_storage, recovery_storage) |
||||||
|
await wlt.async_init(storage) |
||||||
|
return wlt |
||||||
|
|
||||||
|
|
||||||
|
async def populate_dkg_session(test_case): |
||||||
|
dkgc1 = DKGClient(test_case.wlt_svc1) |
||||||
|
dkgc2 = DKGClient(test_case.wlt_svc2) |
||||||
|
dkgc3 = DKGClient(test_case.wlt_svc3) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1_2 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
test_case.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash3_hex, |
||||||
|
session_id3_hex, |
||||||
|
sig3_hex, |
||||||
|
pmsg1_3 |
||||||
|
) = dkgc3.on_dkg_init( |
||||||
|
test_case.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3) |
||||||
|
|
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
test_case.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2) |
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
test_case.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3) |
||||||
|
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1) |
||||||
|
|
||||||
|
pmsg2_2 = dkgc2.party_step2(session_id, cmsg1) |
||||||
|
pmsg2_2 = dkgc2.deserialize_pmsg2(pmsg2_2) |
||||||
|
pmsg2_3 = dkgc3.party_step2(session_id, cmsg1) |
||||||
|
pmsg2_3 = dkgc3.deserialize_pmsg2(pmsg2_3) |
||||||
|
|
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
test_case.nick2, session_id, pmsg2_2) |
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
test_case.nick3, session_id, pmsg2_3) |
||||||
|
cmsg2 = dkgc3.deserialize_cmsg2(cmsg2) |
||||||
|
|
||||||
|
assert dkgc2.finalize(session_id, cmsg2, ext_recovery) |
||||||
|
assert dkgc3.finalize(session_id, cmsg2, ext_recovery) |
||||||
|
dkgc1.on_dkg_finalized(test_case.nick2, session_id) |
||||||
|
dkgc1.on_dkg_finalized(test_case.nick3, session_id) |
||||||
|
return session_id |
||||||
|
|
||||||
|
|
||||||
|
class DKGClientTestCaseBase(IsolatedAsyncioTestCase): |
||||||
|
|
||||||
|
def setUp(self): |
||||||
|
load_test_config(config_path='./test_frost') |
||||||
|
btc.select_chain_params("bitcoin/regtest") |
||||||
|
cryptoengine.BTC_P2TR.VBYTE = 100 |
||||||
|
jm_single().bc_interface.tick_forward_chain_interval = 2 |
||||||
|
|
||||||
|
async def asyncSetUp(self): |
||||||
|
entropy1 = bytes.fromhex('8e5e5677fb302874a607b63ad03ba434') |
||||||
|
entropy2 = bytes.fromhex('38dfa80fbb21b32b2b2740e00a47de9d') |
||||||
|
entropy3 = bytes.fromhex('3ad9c77fcd1d537b6ef396952d1221a0') |
||||||
|
# entropy4 wor wallet with hospubkey not in joinmarket.cfg |
||||||
|
entropy4 = bytes.fromhex('ce88b87f6c85d651e416b8173ab95e57') |
||||||
|
self.wlt1 = await get_populated_wallet(entropy1) |
||||||
|
self.hostpubkey1 = hostpubkey_gen(self.wlt1._hostseckey[:32]) |
||||||
|
self.wlt_svc1 = WalletService(self.wlt1) |
||||||
|
self.wlt2 = await get_populated_wallet(entropy2) |
||||||
|
self.hostpubkey2 = hostpubkey_gen(self.wlt2._hostseckey[:32]) |
||||||
|
self.wlt_svc2 = WalletService(self.wlt2) |
||||||
|
self.wlt3 = await get_populated_wallet(entropy3) |
||||||
|
self.hostpubkey3 = hostpubkey_gen(self.wlt3._hostseckey[:32]) |
||||||
|
self.wlt_svc3 = WalletService(self.wlt3) |
||||||
|
self.wlt4= await get_populated_wallet(entropy4) |
||||||
|
self.hostpubkey4 = hostpubkey_gen(self.wlt4._hostseckey[:32]) |
||||||
|
self.wlt_svc4 = WalletService(self.wlt4) |
||||||
|
self.nick1, self.nick2, self.nick3, self.nick4 = [ |
||||||
|
'nick1', 'nick2', 'nick3', 'nick4' |
||||||
|
] |
||||||
|
|
||||||
|
|
||||||
|
class DKGClientTestCase(DKGClientTestCaseBase): |
||||||
|
|
||||||
|
async def test_dkg_init(self): |
||||||
|
# test wallet with unknown hostpubkey |
||||||
|
dkgc1 = DKGClient(self.wlt_svc4) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
assert hostpubkeyhash_hex is None |
||||||
|
assert session_id is None |
||||||
|
assert sig_hex is None |
||||||
|
|
||||||
|
dkgc1 = DKGClient(self.wlt_svc1) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
assert hostpubkeyhash_hex and len(hostpubkeyhash_hex) == 64 |
||||||
|
assert session_id and len(session_id) == 32 |
||||||
|
assert sig_hex and len(sig_hex) == 128 |
||||||
|
|
||||||
|
async def test_on_dkg_init(self): |
||||||
|
dkgc1 = DKGClient(self.wlt_svc1) |
||||||
|
dkgc2 = DKGClient(self.wlt_svc2) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
|
||||||
|
# fail with wrong pubkeyhash |
||||||
|
hostpubkeyhash4_hex = sha256(self.hostpubkey4).digest() |
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash4_hex, session_id, sig_hex) |
||||||
|
for v in [nick1, hostpubkeyhash2_hex, session_id2_hex, |
||||||
|
sig2_hex, pmsg1]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
# fail with wrong sig |
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, '01020304'*16) |
||||||
|
for v in [nick1, hostpubkeyhash2_hex, session_id2_hex, |
||||||
|
sig2_hex, pmsg1]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
assert nick1 == self.nick1 |
||||||
|
assert hostpubkeyhash2_hex and len(hostpubkeyhash2_hex) == 64 |
||||||
|
assert session_id2_hex and len(session_id2_hex) == 64 |
||||||
|
assert bytes.fromhex(session_id2_hex) == session_id |
||||||
|
assert sig_hex and len(sig_hex) == 128 |
||||||
|
assert pmsg1 is not None |
||||||
|
|
||||||
|
# fail on second call with right params |
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
for v in [nick1, hostpubkeyhash2_hex, session_id2_hex, |
||||||
|
sig2_hex, pmsg1]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
async def test_party_step1(self): |
||||||
|
dkgc1 = DKGClient(self.wlt_svc1) |
||||||
|
dkgc2 = DKGClient(self.wlt_svc2) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
|
||||||
|
# fail with unknown session_id |
||||||
|
pmsg1 = dkgc2.party_step1(b'\x05'*32) |
||||||
|
assert pmsg1 is None |
||||||
|
|
||||||
|
# fail when session.state1 aleready set |
||||||
|
pmsg1 = dkgc2.party_step1(session_id) |
||||||
|
assert pmsg1 is None |
||||||
|
|
||||||
|
session = dkgc2.dkg_sessions.get(session_id) |
||||||
|
session.state1 = None |
||||||
|
pmsg1 = dkgc2.party_step1(session_id) |
||||||
|
assert pmsg1 is not None |
||||||
|
assert isinstance(pmsg1, bytes) |
||||||
|
|
||||||
|
session.state1 = None |
||||||
|
pmsg1 = dkgc2.party_step1(session_id, serialize=False) |
||||||
|
assert pmsg1 is not None |
||||||
|
assert isinstance(pmsg1, ParticipantMsg1) |
||||||
|
|
||||||
|
def test_on_dkg_pmsg1(self): |
||||||
|
dkgc1 = DKGClient(self.wlt_svc1) |
||||||
|
dkgc2 = DKGClient(self.wlt_svc2) |
||||||
|
dkgc3 = DKGClient(self.wlt_svc3) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1_2 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash3_hex, |
||||||
|
session_id3_hex, |
||||||
|
sig3_hex, |
||||||
|
pmsg1_3 |
||||||
|
) = dkgc3.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3) |
||||||
|
|
||||||
|
# party2 added pmsg1, no ready_list, no cmsg1 returned yet |
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2) |
||||||
|
assert ready_list is None |
||||||
|
assert cmsg1 is None |
||||||
|
|
||||||
|
# unknown coordinator session |
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick3, hostpubkeyhash3_hex, b'\xaa'*32, sig3_hex, pmsg1_3) |
||||||
|
assert ready_list is None |
||||||
|
assert cmsg1 is None |
||||||
|
|
||||||
|
# unknown pubkeyhash |
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick3, b'\xaa'*32, session_id, sig3_hex, pmsg1_3) |
||||||
|
assert ready_list is None |
||||||
|
assert cmsg1 is None |
||||||
|
|
||||||
|
# wrong sig |
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick3, hostpubkeyhash3_hex, session_id, 'aa'*64, pmsg1_3) |
||||||
|
assert ready_list is None |
||||||
|
assert cmsg1 is None |
||||||
|
|
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3) |
||||||
|
assert ready_list == set([self.nick2, self.nick3]) |
||||||
|
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1) |
||||||
|
assert isinstance(cmsg1, CoordinatorMsg1) |
||||||
|
|
||||||
|
def test_coordinator_step1(self): |
||||||
|
dkgc1 = DKGClient(self.wlt_svc1) |
||||||
|
dkgc2 = DKGClient(self.wlt_svc2) |
||||||
|
dkgc3 = DKGClient(self.wlt_svc3) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1_2 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash3_hex, |
||||||
|
session_id3_hex, |
||||||
|
sig3_hex, |
||||||
|
pmsg1_3 |
||||||
|
) = dkgc3.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3) |
||||||
|
|
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2) |
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3) |
||||||
|
|
||||||
|
# unknown session_id |
||||||
|
cmsg1 = dkgc1.coordinator_step1(b'\xaa'*32) |
||||||
|
assert cmsg1 is None |
||||||
|
|
||||||
|
# coordinator.state already set |
||||||
|
cmsg1 = dkgc1.coordinator_step1(session_id) |
||||||
|
assert cmsg1 is None |
||||||
|
|
||||||
|
coordinator = dkgc1.dkg_coordinators.get(session_id) |
||||||
|
coordinator.state = None |
||||||
|
cmsg1 = dkgc1.coordinator_step1(session_id) |
||||||
|
|
||||||
|
def test_party_step2(self): |
||||||
|
dkgc1 = DKGClient(self.wlt_svc1) |
||||||
|
dkgc2 = DKGClient(self.wlt_svc2) |
||||||
|
dkgc3 = DKGClient(self.wlt_svc3) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1_2 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash3_hex, |
||||||
|
session_id3_hex, |
||||||
|
sig3_hex, |
||||||
|
pmsg1_3 |
||||||
|
) = dkgc3.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3) |
||||||
|
|
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2) |
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3) |
||||||
|
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1) |
||||||
|
|
||||||
|
# unknown session_id |
||||||
|
pmsg2 = dkgc2.party_step2(b'\xaa'*32, cmsg1) |
||||||
|
assert pmsg2 is None |
||||||
|
|
||||||
|
pmsg2 = dkgc2.party_step2(session_id, cmsg1) |
||||||
|
assert cmsg1 is not None |
||||||
|
pmsg2 = dkgc1.deserialize_pmsg2(pmsg2) |
||||||
|
assert isinstance(pmsg2, ParticipantMsg2) |
||||||
|
|
||||||
|
# session.state2 already set |
||||||
|
pmsg2 = dkgc2.party_step2(session_id, cmsg1) |
||||||
|
assert pmsg2 is None |
||||||
|
|
||||||
|
def test_on_dkg_pmsg2(self): |
||||||
|
dkgc1 = DKGClient(self.wlt_svc1) |
||||||
|
dkgc2 = DKGClient(self.wlt_svc2) |
||||||
|
dkgc3 = DKGClient(self.wlt_svc3) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1_2 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash3_hex, |
||||||
|
session_id3_hex, |
||||||
|
sig3_hex, |
||||||
|
pmsg1_3 |
||||||
|
) = dkgc3.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3) |
||||||
|
|
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2) |
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3) |
||||||
|
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1) |
||||||
|
|
||||||
|
pmsg2_2 = dkgc2.party_step2(session_id, cmsg1) |
||||||
|
pmsg2_2 = dkgc2.deserialize_pmsg2(pmsg2_2) |
||||||
|
assert isinstance(pmsg2_2, ParticipantMsg2) |
||||||
|
pmsg2_3 = dkgc3.party_step2(session_id, cmsg1) |
||||||
|
pmsg2_3 = dkgc3.deserialize_pmsg2(pmsg2_3) |
||||||
|
assert isinstance(pmsg2_3, ParticipantMsg2) |
||||||
|
|
||||||
|
# party2 added pmsg2, no ready_list, no cmsg2 returned yet |
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick2, session_id, pmsg2_2) |
||||||
|
assert ready_list is None |
||||||
|
assert cmsg2 is None |
||||||
|
assert ext_recovery is None |
||||||
|
|
||||||
|
# unknown coordinator session |
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick3, b'\xaa'*32, pmsg2_3) |
||||||
|
assert ready_list is None |
||||||
|
assert cmsg2 is None |
||||||
|
assert ext_recovery is None |
||||||
|
|
||||||
|
# unknown party nick |
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick4, session_id, pmsg2_3) |
||||||
|
assert ready_list is None |
||||||
|
assert cmsg2 is None |
||||||
|
assert ext_recovery is None |
||||||
|
|
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick3, session_id, pmsg2_3) |
||||||
|
cmsg2 = dkgc1.deserialize_cmsg2(cmsg2) |
||||||
|
assert ready_list == set([self.nick2, self.nick3]) |
||||||
|
assert isinstance(cmsg2, CoordinatorMsg2) |
||||||
|
assert isinstance(ext_recovery, bytes) |
||||||
|
|
||||||
|
# party pubkey for nick3 not found |
||||||
|
coordinator = dkgc1.dkg_coordinators.get(session_id) |
||||||
|
session3 = coordinator.sessions.pop(self.hostpubkey3) |
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick3, session_id, pmsg2_3) |
||||||
|
assert ready_list is None |
||||||
|
assert cmsg2 is None |
||||||
|
assert ext_recovery is None |
||||||
|
coordinator.sessions[self.hostpubkey3] = session3 |
||||||
|
|
||||||
|
# pmsg2 already set in coordinator sessions |
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick3, session_id, pmsg2_3) |
||||||
|
assert ready_list is None |
||||||
|
assert cmsg2 is None |
||||||
|
assert ext_recovery is None |
||||||
|
|
||||||
|
def test_coordinator_step2(self): |
||||||
|
dkgc1 = DKGClient(self.wlt_svc1) |
||||||
|
dkgc2 = DKGClient(self.wlt_svc2) |
||||||
|
dkgc3 = DKGClient(self.wlt_svc3) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1_2 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash3_hex, |
||||||
|
session_id3_hex, |
||||||
|
sig3_hex, |
||||||
|
pmsg1_3 |
||||||
|
) = dkgc3.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3) |
||||||
|
|
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2) |
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3) |
||||||
|
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1) |
||||||
|
|
||||||
|
pmsg2_2 = dkgc2.party_step2(session_id, cmsg1) |
||||||
|
pmsg2_2 = dkgc2.deserialize_pmsg2(pmsg2_2) |
||||||
|
pmsg2_3 = dkgc3.party_step2(session_id, cmsg1) |
||||||
|
pmsg2_3 = dkgc3.deserialize_pmsg2(pmsg2_3) |
||||||
|
|
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick2, session_id, pmsg2_2) |
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick3, session_id, pmsg2_3) |
||||||
|
|
||||||
|
# unknown session_id |
||||||
|
cmsg2 = dkgc1.coordinator_step2(b'\xaa'*32) |
||||||
|
assert cmsg2 is None |
||||||
|
|
||||||
|
# coordinator.cmsg2 already set |
||||||
|
cmsg2 = dkgc1.coordinator_step2(session_id) |
||||||
|
assert cmsg2 is None |
||||||
|
|
||||||
|
coordinator = dkgc1.dkg_coordinators.get(session_id) |
||||||
|
coordinator.cmsg2 = None |
||||||
|
cmsg2 = dkgc1.coordinator_step2(session_id) |
||||||
|
cmsg2 = dkgc1.deserialize_cmsg2(cmsg2) |
||||||
|
assert isinstance(cmsg2, CoordinatorMsg2) |
||||||
|
|
||||||
|
def test_dkg_finalize(self): |
||||||
|
dkgc1 = DKGClient(self.wlt_svc1) |
||||||
|
dkgc2 = DKGClient(self.wlt_svc2) |
||||||
|
dkgc3 = DKGClient(self.wlt_svc3) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1_2 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash3_hex, |
||||||
|
session_id3_hex, |
||||||
|
sig3_hex, |
||||||
|
pmsg1_3 |
||||||
|
) = dkgc3.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3) |
||||||
|
|
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2) |
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3) |
||||||
|
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1) |
||||||
|
|
||||||
|
pmsg2_2 = dkgc2.party_step2(session_id, cmsg1) |
||||||
|
pmsg2_2 = dkgc2.deserialize_pmsg2(pmsg2_2) |
||||||
|
pmsg2_3 = dkgc3.party_step2(session_id, cmsg1) |
||||||
|
pmsg2_3 = dkgc3.deserialize_pmsg2(pmsg2_3) |
||||||
|
|
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick2, session_id, pmsg2_2) |
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick3, session_id, pmsg2_3) |
||||||
|
cmsg2 = dkgc3.deserialize_cmsg2(cmsg2) |
||||||
|
|
||||||
|
# unknown session_id |
||||||
|
assert not dkgc2.finalize(b'\xaa'*32, cmsg2, ext_recovery) |
||||||
|
|
||||||
|
assert dkgc2.finalize(session_id, cmsg2, ext_recovery) |
||||||
|
assert dkgc3.finalize(session_id, cmsg2, ext_recovery) |
||||||
|
|
||||||
|
# session.dkg_output already set |
||||||
|
assert not dkgc2.finalize(session_id, cmsg2, ext_recovery) |
||||||
|
assert not dkgc3.finalize(session_id, cmsg2, ext_recovery) |
||||||
|
|
||||||
|
def test_on_dkg_finalized(self): |
||||||
|
dkgc1 = DKGClient(self.wlt_svc1) |
||||||
|
dkgc2 = DKGClient(self.wlt_svc2) |
||||||
|
dkgc3 = DKGClient(self.wlt_svc3) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pmsg1_2 |
||||||
|
) = dkgc2.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash3_hex, |
||||||
|
session_id3_hex, |
||||||
|
sig3_hex, |
||||||
|
pmsg1_3 |
||||||
|
) = dkgc3.on_dkg_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3) |
||||||
|
|
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2) |
||||||
|
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1( |
||||||
|
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3) |
||||||
|
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1) |
||||||
|
|
||||||
|
pmsg2_2 = dkgc2.party_step2(session_id, cmsg1) |
||||||
|
pmsg2_2 = dkgc2.deserialize_pmsg2(pmsg2_2) |
||||||
|
pmsg2_3 = dkgc3.party_step2(session_id, cmsg1) |
||||||
|
pmsg2_3 = dkgc3.deserialize_pmsg2(pmsg2_3) |
||||||
|
|
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick2, session_id, pmsg2_2) |
||||||
|
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2( |
||||||
|
self.nick3, session_id, pmsg2_3) |
||||||
|
cmsg2 = dkgc3.deserialize_cmsg2(cmsg2) |
||||||
|
|
||||||
|
assert dkgc2.finalize(session_id, cmsg2, ext_recovery) |
||||||
|
assert dkgc3.finalize(session_id, cmsg2, ext_recovery) |
||||||
|
|
||||||
|
# unknown session_id |
||||||
|
dkgc1.on_dkg_finalized(self.nick2, b'\xaa'*32) |
||||||
|
|
||||||
|
assert not dkgc1.on_dkg_finalized(self.nick2, session_id) |
||||||
|
assert dkgc1.on_dkg_finalized(self.nick3, session_id) |
||||||
|
|
||||||
|
|
||||||
|
class FROSTClientTestCase(DKGClientTestCaseBase): |
||||||
|
|
||||||
|
async def asyncSetUp(self): |
||||||
|
await super().asyncSetUp() |
||||||
|
self.dkg_session_id = await populate_dkg_session(self) |
||||||
|
self.fc1 = FROSTClient(self.wlt_svc1) |
||||||
|
self.fc2 = FROSTClient(self.wlt_svc2) |
||||||
|
self.fc3 = FROSTClient(self.wlt_svc3) |
||||||
|
self.fc4 = FROSTClient(self.wlt_svc4) |
||||||
|
|
||||||
|
async def test_frost_init(self): |
||||||
|
msg_bytes = bytes.fromhex('aabb'*16) |
||||||
|
# test wallet with unknown hostpubkey |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = self.fc4.frost_init( |
||||||
|
self.dkg_session_id, msg_bytes) |
||||||
|
assert hostpubkeyhash_hex is None |
||||||
|
assert session_id is None |
||||||
|
assert sig_hex is None |
||||||
|
|
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init( |
||||||
|
self.dkg_session_id, msg_bytes) |
||||||
|
assert hostpubkeyhash_hex and len(hostpubkeyhash_hex) == 64 |
||||||
|
assert session_id and len(session_id) == 32 |
||||||
|
assert sig_hex and len(sig_hex) == 128 |
||||||
|
|
||||||
|
async def test_on_frost_init(self): |
||||||
|
msg_bytes = bytes.fromhex('aabb'*16) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init( |
||||||
|
self.dkg_session_id, msg_bytes) |
||||||
|
|
||||||
|
# fail with wrong pubkeyhash |
||||||
|
hostpubkeyhash4_hex = sha256(self.hostpubkey4).digest() |
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pub_nonce |
||||||
|
) = self.fc2.on_frost_init( |
||||||
|
self.nick1, hostpubkeyhash4_hex, session_id, sig_hex) |
||||||
|
for v in [nick1, hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, sig2_hex, pub_nonce]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
# fail with wrong sig |
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pub_nonce |
||||||
|
) = self.fc2.on_frost_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, '01020304'*16) |
||||||
|
for v in [nick1, hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, sig2_hex, pub_nonce]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pub_nonce |
||||||
|
) = self.fc2.on_frost_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
assert nick1 == self.nick1 |
||||||
|
assert hostpubkeyhash2_hex and len(hostpubkeyhash2_hex) == 64 |
||||||
|
assert session_id2_hex and len(session_id2_hex) == 64 |
||||||
|
assert bytes.fromhex(session_id2_hex) == session_id |
||||||
|
assert sig_hex and len(sig_hex) == 128 |
||||||
|
assert pub_nonce and len(pub_nonce) == 66 |
||||||
|
|
||||||
|
# fail on second call with right params |
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pub_nonce |
||||||
|
) = self.fc2.on_frost_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
for v in [nick1, hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, sig2_hex, pub_nonce]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
def test_frost_round1(self): |
||||||
|
msg_bytes = bytes.fromhex('aabb'*16) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init( |
||||||
|
self.dkg_session_id, msg_bytes) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pub_nonce |
||||||
|
) = self.fc2.on_frost_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
|
||||||
|
# fail with unknown session_id |
||||||
|
pub_nonce = self.fc2.party_step1(b'\x05'*32) |
||||||
|
assert pub_nonce is None |
||||||
|
|
||||||
|
# fail with session.sec_nonce already set |
||||||
|
pub_nonce = self.fc2.frost_round1(session_id) |
||||||
|
assert pub_nonce is None |
||||||
|
|
||||||
|
session = self.fc2.frost_sessions.get(session_id) |
||||||
|
session.sec_nonce = None |
||||||
|
pub_nonce = self.fc2.frost_round1(session_id) |
||||||
|
assert pub_nonce and len(pub_nonce) == 66 |
||||||
|
|
||||||
|
def test_on_frost_round1(self): |
||||||
|
msg_bytes = bytes.fromhex('aabb'*16) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init( |
||||||
|
self.dkg_session_id, msg_bytes) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pub_nonce2 |
||||||
|
) = self.fc2.on_frost_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash3_hex, |
||||||
|
session_id3_hex, |
||||||
|
sig3_hex, |
||||||
|
pub_nonce3 |
||||||
|
) = self.fc3.on_frost_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
|
||||||
|
# unknown session_id |
||||||
|
( |
||||||
|
ready_list, |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = self.fc1.on_frost_round1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, b'\xaa'*32, |
||||||
|
sig2_hex, pub_nonce2) |
||||||
|
for v in [ready_list, nonce_agg, dkg_session_id, ids, msg]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
# unknown pubkeyhash |
||||||
|
( |
||||||
|
ready_list, |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = self.fc1.on_frost_round1( |
||||||
|
self.nick2, 'bb'*32, session_id, sig2_hex, pub_nonce2) |
||||||
|
for v in [ready_list, nonce_agg, dkg_session_id, ids, msg]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
# wrong sig |
||||||
|
( |
||||||
|
ready_list, |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = self.fc1.on_frost_round1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, '1234'*32, pub_nonce2) |
||||||
|
for v in [ready_list, nonce_agg, dkg_session_id, ids, msg]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
( |
||||||
|
ready_list, |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = self.fc1.on_frost_round1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, |
||||||
|
sig2_hex, pub_nonce2) |
||||||
|
assert ready_list == set([self.nick2]) |
||||||
|
assert nonce_agg and len(nonce_agg)== 66 |
||||||
|
assert dkg_session_id and dkg_session_id == self.dkg_session_id |
||||||
|
assert ids == [1, 2] |
||||||
|
assert msg and len(msg) == 32 and msg == msg_bytes |
||||||
|
|
||||||
|
# miminum pub_nonce set already presented, ignoring additional |
||||||
|
( |
||||||
|
ready_list, |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = self.fc1.on_frost_round1( |
||||||
|
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pub_nonce3) |
||||||
|
for v in [ready_list, nonce_agg, dkg_session_id, ids, msg]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
def test_frost_agg1(self): |
||||||
|
msg_bytes = bytes.fromhex('aabb'*16) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init( |
||||||
|
self.dkg_session_id, msg_bytes) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pub_nonce2 |
||||||
|
) = self.fc2.on_frost_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
|
||||||
|
( |
||||||
|
ready_list, |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = self.fc1.on_frost_round1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, |
||||||
|
sig2_hex, pub_nonce2) |
||||||
|
|
||||||
|
# fail on unknown session_id |
||||||
|
( |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = self.fc1.frost_agg1(b'\xaa'*32) |
||||||
|
for v in [nonce_agg, dkg_session_id, ids, msg]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
# fail with coordinator.nonce_agg already set |
||||||
|
( |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = self.fc1.frost_agg1(session_id) |
||||||
|
for v in [nonce_agg, dkg_session_id, ids, msg]: |
||||||
|
assert v is None |
||||||
|
|
||||||
|
coordinator = self.fc1.frost_coordinators.get(session_id) |
||||||
|
coordinator.nonce_agg = None |
||||||
|
( |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = self.fc1.frost_agg1(session_id) |
||||||
|
assert nonce_agg and len(nonce_agg)== 66 |
||||||
|
assert dkg_session_id and dkg_session_id == self.dkg_session_id |
||||||
|
assert ids == [1, 2] |
||||||
|
assert msg and len(msg) == 32 and msg == msg_bytes |
||||||
|
|
||||||
|
def test_frost_round2(self): |
||||||
|
msg_bytes = bytes.fromhex('aabb'*16) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init( |
||||||
|
self.dkg_session_id, msg_bytes) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pub_nonce2 |
||||||
|
) = self.fc2.on_frost_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
|
||||||
|
( |
||||||
|
ready_list, |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = self.fc1.on_frost_round1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, |
||||||
|
sig2_hex, pub_nonce2) |
||||||
|
|
||||||
|
# fail on unknown session_id |
||||||
|
partial_sig = self.fc2.frost_round2( |
||||||
|
b'\xaa'*32, nonce_agg, self.dkg_session_id, ids, msg) |
||||||
|
|
||||||
|
# fail on unknown dkg_session_id |
||||||
|
partial_sig = self.fc2.frost_round2( |
||||||
|
session_id, nonce_agg, b'\xdd'*32, ids, msg) |
||||||
|
|
||||||
|
partial_sig = self.fc2.frost_round2( |
||||||
|
session_id, nonce_agg, self.dkg_session_id, ids, msg) |
||||||
|
assert partial_sig and len(partial_sig) == 32 |
||||||
|
|
||||||
|
# session.partial_sig already set |
||||||
|
partial_sig = self.fc2.frost_round2( |
||||||
|
session_id, nonce_agg, self.dkg_session_id, ids, msg) |
||||||
|
assert partial_sig is None |
||||||
|
|
||||||
|
def test_on_frost_round2(self): |
||||||
|
msg_bytes = bytes.fromhex('aabb'*16) |
||||||
|
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init( |
||||||
|
self.dkg_session_id, msg_bytes) |
||||||
|
|
||||||
|
( |
||||||
|
nick1, |
||||||
|
hostpubkeyhash2_hex, |
||||||
|
session_id2_hex, |
||||||
|
sig2_hex, |
||||||
|
pub_nonce2 |
||||||
|
) = self.fc2.on_frost_init( |
||||||
|
self.nick1, hostpubkeyhash_hex, session_id, sig_hex) |
||||||
|
|
||||||
|
( |
||||||
|
ready_list, |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = self.fc1.on_frost_round1( |
||||||
|
self.nick2, hostpubkeyhash2_hex, session_id, |
||||||
|
sig2_hex, pub_nonce2) |
||||||
|
|
||||||
|
partial_sig = self.fc2.frost_round2( |
||||||
|
session_id, nonce_agg, self.dkg_session_id, ids, msg) |
||||||
|
|
||||||
|
# unknown party nick |
||||||
|
sig = self.fc1.on_frost_round2(self.nick4, session_id, partial_sig) |
||||||
|
assert sig is None |
||||||
|
|
||||||
|
# party pubkey for nick3 not found |
||||||
|
coordinator = self.fc1.frost_coordinators.get(session_id) |
||||||
|
session2 = coordinator.sessions.pop(self.hostpubkey2) |
||||||
|
sig = self.fc1.on_frost_round2(self.nick2, session_id, partial_sig) |
||||||
|
assert sig is None |
||||||
|
coordinator.sessions[self.hostpubkey2] = session2 |
||||||
|
|
||||||
|
# fail on unknown session_id |
||||||
|
sig = self.fc1.on_frost_round2(self.nick2, b'\xaa'*32, partial_sig) |
||||||
|
assert sig is None |
||||||
|
|
||||||
|
sig = self.fc1.on_frost_round2(self.nick2, session_id, partial_sig) |
||||||
|
assert sig and len(sig) == 64 |
||||||
|
|
||||||
|
# partial_sig already set in coordinator |
||||||
|
sig = self.fc1.on_frost_round2(self.nick2, session_id, partial_sig) |
||||||
|
assert sig is None |
||||||
@ -0,0 +1,341 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
import asyncio |
||||||
|
import base64 |
||||||
|
import time |
||||||
|
from pprint import pprint |
||||||
|
|
||||||
|
from unittest import IsolatedAsyncioTestCase |
||||||
|
|
||||||
|
import jmclient # install asyncioreactor |
||||||
|
from twisted.internet import reactor |
||||||
|
|
||||||
|
import pytest |
||||||
|
|
||||||
|
import jmbitcoin as btc |
||||||
|
from jmbase import get_log |
||||||
|
from jmclient import ( |
||||||
|
load_test_config, jm_single, get_network, cryptoengine, VolatileStorage, |
||||||
|
FrostWallet, WalletService) |
||||||
|
from jmclient import FrostIPCServer, FrostIPCClient |
||||||
|
from jmclient.frost_clients import FROSTClient |
||||||
|
|
||||||
|
from test_frost_clients import populate_dkg_session |
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.usefixtures("setup_regtest_frost_bitcoind") |
||||||
|
|
||||||
|
log = get_log() |
||||||
|
|
||||||
|
|
||||||
|
async def get_populated_wallet(entropy=None): |
||||||
|
storage = VolatileStorage() |
||||||
|
dkg_storage = VolatileStorage() |
||||||
|
recovery_storage = VolatileStorage() |
||||||
|
FrostWallet.initialize(storage, dkg_storage, recovery_storage, |
||||||
|
get_network(), entropy=entropy) |
||||||
|
wlt = FrostWallet(storage, dkg_storage, recovery_storage) |
||||||
|
await wlt.async_init(storage) |
||||||
|
return wlt |
||||||
|
|
||||||
|
|
||||||
|
class DummyFrostJMClientProtocol: |
||||||
|
|
||||||
|
def __init__(self, factory, client, nick): |
||||||
|
self.nick = nick |
||||||
|
self.factory = factory |
||||||
|
self.client = client |
||||||
|
self.party_clients = {} |
||||||
|
|
||||||
|
async def dkg_gen(self): |
||||||
|
log.debug(f'Coordinator call dkg_gen') |
||||||
|
client = self.factory.client |
||||||
|
md_type_idx = None |
||||||
|
session_id = None |
||||||
|
session = None |
||||||
|
|
||||||
|
while True: |
||||||
|
if md_type_idx is None: |
||||||
|
md_type_idx = await client.dkg_gen() |
||||||
|
if md_type_idx is None: |
||||||
|
log.debug('finished dkg_gen execution') |
||||||
|
break |
||||||
|
|
||||||
|
if session_id is None: |
||||||
|
session_id, _, session = self.dkg_init(*md_type_idx) |
||||||
|
if session_id is None: |
||||||
|
log.warn('could not get session_id from dkg_init}') |
||||||
|
await asyncio.sleep(5) |
||||||
|
continue |
||||||
|
|
||||||
|
pub = await client.wait_on_dkg_output(session_id) |
||||||
|
if not pub: |
||||||
|
session_id = None |
||||||
|
session = None |
||||||
|
continue |
||||||
|
|
||||||
|
if session.dkg_output: |
||||||
|
md_type_idx = None |
||||||
|
session_id = None |
||||||
|
session = None |
||||||
|
client.dkg_gen_list.pop(0) |
||||||
|
continue |
||||||
|
|
||||||
|
def dkg_init(self, mixdepth, address_type, index): |
||||||
|
log.debug(f'Coordinator call dkg_init ' |
||||||
|
f'({mixdepth}, {address_type}, {index})') |
||||||
|
client = self.factory.client |
||||||
|
hostpubkeyhash, session_id, sig = client.dkg_init( |
||||||
|
mixdepth, address_type, index) |
||||||
|
coordinator = client.dkg_coordinators.get(session_id) |
||||||
|
session = client.dkg_sessions.get(session_id) |
||||||
|
if session_id and session and coordinator: |
||||||
|
session.dkg_init_sec = time.time() |
||||||
|
|
||||||
|
for _, pc in self.party_clients.items(): |
||||||
|
|
||||||
|
async def on_dkg_init(pc, nick, hostpubkeyhash, |
||||||
|
session_id, sig): |
||||||
|
await pc.on_dkg_init( |
||||||
|
nick, hostpubkeyhash, session_id, sig) |
||||||
|
|
||||||
|
asyncio.create_task(on_dkg_init( |
||||||
|
pc, self.nick, hostpubkeyhash, session_id, sig)) |
||||||
|
return session_id, coordinator, session |
||||||
|
return None, None, None |
||||||
|
|
||||||
|
async def on_dkg_init(self, nick, hostpubkeyhash, session_id, sig): |
||||||
|
client = self.factory.client |
||||||
|
nick, hostpubkeyhash, session_id, sig, pmsg1 = client.on_dkg_init( |
||||||
|
nick, hostpubkeyhash, session_id, sig) |
||||||
|
if pmsg1: |
||||||
|
pc = self.party_clients[nick] |
||||||
|
session_id = bytes.fromhex(session_id) |
||||||
|
await pc.on_dkg_pmsg1( |
||||||
|
self.nick, hostpubkeyhash, session_id, sig, pmsg1) |
||||||
|
|
||||||
|
async def on_dkg_pmsg1(self, nick, hostpubkeyhash, session_id, sig, pmsg1): |
||||||
|
client = self.factory.client |
||||||
|
pmsg1 = client.deserialize_pmsg1(pmsg1) |
||||||
|
ready_nicks, cmsg1 = client.on_dkg_pmsg1( |
||||||
|
nick, hostpubkeyhash, session_id, sig, pmsg1) |
||||||
|
if ready_nicks and cmsg1: |
||||||
|
for party_nick in ready_nicks: |
||||||
|
pc = self.party_clients[party_nick] |
||||||
|
await pc.on_dkg_cmsg1(self.nick, session_id, cmsg1) |
||||||
|
|
||||||
|
async def on_dkg_cmsg1(self, nick, session_id, cmsg1): |
||||||
|
client = self.factory.client |
||||||
|
session = client.dkg_sessions.get(session_id) |
||||||
|
if not session: |
||||||
|
log.error(f'on_dkg_cmsg1: session {session_id} not found') |
||||||
|
return {'accepted': True} |
||||||
|
if session and session.coord_nick == nick: |
||||||
|
cmsg1 = client.deserialize_cmsg1(cmsg1) |
||||||
|
pmsg2 = client.party_step2(session_id, cmsg1) |
||||||
|
if pmsg2: |
||||||
|
pc = self.party_clients[nick] |
||||||
|
await pc.on_dkg_pmsg2(self.nick, session_id, pmsg2) |
||||||
|
else: |
||||||
|
log.error(f'on_dkg_cmsg1: not coordinator nick {nick}') |
||||||
|
|
||||||
|
async def on_dkg_pmsg2(self, nick, session_id, pmsg2): |
||||||
|
client = self.factory.client |
||||||
|
pmsg2 = client.deserialize_pmsg2(pmsg2) |
||||||
|
ready_nicks, cmsg2, ext_recovery = client.on_dkg_pmsg2( |
||||||
|
nick, session_id, pmsg2) |
||||||
|
if ready_nicks and cmsg2 and ext_recovery: |
||||||
|
for party_nick in ready_nicks: |
||||||
|
pc = self.party_clients[party_nick] |
||||||
|
await pc.on_dkg_cmsg2( |
||||||
|
self.nick, session_id, cmsg2, ext_recovery) |
||||||
|
|
||||||
|
async def on_dkg_cmsg2(self, nick, session_id, cmsg2, ext_recovery): |
||||||
|
client = self.factory.client |
||||||
|
session = client.dkg_sessions.get(session_id) |
||||||
|
if not session: |
||||||
|
log.error(f'on_dkg_cmsg2: session {session_id} not found') |
||||||
|
return {'accepted': True} |
||||||
|
if session and session.coord_nick == nick: |
||||||
|
cmsg2 = client.deserialize_cmsg2(cmsg2) |
||||||
|
finalized = client.finalize(session_id, cmsg2, ext_recovery) |
||||||
|
if finalized: |
||||||
|
pc = self.party_clients[nick] |
||||||
|
await pc.on_dkg_finalized(self.nick, session_id) |
||||||
|
else: |
||||||
|
log.error(f'on_dkg_cmsg2: not coordinator nick {nick}') |
||||||
|
|
||||||
|
async def on_dkg_finalized(self, nick, session_id): |
||||||
|
client = self.factory.client |
||||||
|
log.debug(f'Coordinator get dkgfinalized') |
||||||
|
client.on_dkg_finalized(nick, session_id) |
||||||
|
|
||||||
|
def frost_init(self, dkg_session_id, msg_bytes): |
||||||
|
log.debug(f'Coordinator call frost_init') |
||||||
|
client = self.factory.client |
||||||
|
hostpubkeyhash, session_id, sig = client.frost_init( |
||||||
|
dkg_session_id, msg_bytes) |
||||||
|
coordinator = client.frost_coordinators.get(session_id) |
||||||
|
session = client.frost_sessions.get(session_id) |
||||||
|
if session_id and session and coordinator: |
||||||
|
coordinator.frost_init_sec = time.time() |
||||||
|
for _, pc in self.party_clients.items(): |
||||||
|
|
||||||
|
async def on_frost_init(pc, nick, hostpubkeyhash, |
||||||
|
session_id, sig): |
||||||
|
await pc.on_frost_init( |
||||||
|
nick, hostpubkeyhash, session_id, sig) |
||||||
|
|
||||||
|
asyncio.create_task(on_frost_init( |
||||||
|
pc, self.nick, hostpubkeyhash, session_id, sig)) |
||||||
|
return session_id, coordinator, session |
||||||
|
|
||||||
|
async def on_frost_init(self, nick, hostpubkeyhash, session_id, sig): |
||||||
|
client = self.factory.client |
||||||
|
( |
||||||
|
nick, |
||||||
|
hostpubkeyhash, |
||||||
|
session_id, |
||||||
|
sig, |
||||||
|
pub_nonce |
||||||
|
) = client.on_frost_init(nick, hostpubkeyhash, session_id, sig) |
||||||
|
if pub_nonce: |
||||||
|
pc = self.party_clients[nick] |
||||||
|
session_id = bytes.fromhex(session_id) |
||||||
|
await pc.on_frost_round1( |
||||||
|
self.nick, hostpubkeyhash, session_id, sig, pub_nonce) |
||||||
|
|
||||||
|
async def on_frost_round1(self, nick, hostpubkeyhash, |
||||||
|
session_id, sig, pub_nonce): |
||||||
|
client = self.factory.client |
||||||
|
( |
||||||
|
ready_nicks, |
||||||
|
nonce_agg, |
||||||
|
dkg_session_id, |
||||||
|
ids, |
||||||
|
msg |
||||||
|
) = client.on_frost_round1( |
||||||
|
nick, hostpubkeyhash, session_id, sig, pub_nonce) |
||||||
|
if ready_nicks and nonce_agg: |
||||||
|
for party_nick in ready_nicks: |
||||||
|
pc = self.party_clients[nick] |
||||||
|
self.frost_agg1(pc, self.nick, session_id, nonce_agg, |
||||||
|
dkg_session_id, ids, msg) |
||||||
|
|
||||||
|
def frost_agg1(self, pc, nick, session_id, |
||||||
|
nonce_agg, dkg_session_id, ids, msg): |
||||||
|
pc.on_frost_agg1( |
||||||
|
self.nick, session_id, nonce_agg, dkg_session_id, ids, msg) |
||||||
|
|
||||||
|
def on_frost_agg1(self, nick, session_id, |
||||||
|
nonce_agg, dkg_session_id, ids, msg): |
||||||
|
client = self.factory.client |
||||||
|
session = client.frost_sessions.get(session_id) |
||||||
|
if not session: |
||||||
|
log.error(f'on_frost_agg1: session {session_id} not found') |
||||||
|
return |
||||||
|
if session and session.coord_nick == nick: |
||||||
|
partial_sig = client.frost_round2( |
||||||
|
session_id, nonce_agg, dkg_session_id, ids, msg) |
||||||
|
if partial_sig: |
||||||
|
pc = self.party_clients[nick] |
||||||
|
pc.on_frost_round2(self.nick, session_id, partial_sig) |
||||||
|
else: |
||||||
|
log.error(f'on_frost_agg1: not coordinator nick {nick}') |
||||||
|
|
||||||
|
def on_frost_round2(self, nick, session_id, partial_sig): |
||||||
|
client = self.factory.client |
||||||
|
sig = client.on_frost_round2(nick, session_id, partial_sig) |
||||||
|
if sig: |
||||||
|
log.debug(f'Successfully get signature {sig.hex()[:8]}...') |
||||||
|
|
||||||
|
|
||||||
|
class DummyFrostJMClientProtocolFactory: |
||||||
|
|
||||||
|
protocol = DummyFrostJMClientProtocol |
||||||
|
|
||||||
|
def __init__(self, client, nick): |
||||||
|
self.client = client |
||||||
|
self.proto_client = self.protocol(self, self.client, nick) |
||||||
|
|
||||||
|
def add_party_client(self, nick, party_client): |
||||||
|
self.proto_client.party_clients[nick] = party_client |
||||||
|
|
||||||
|
def getClient(self): |
||||||
|
return self.proto_client |
||||||
|
|
||||||
|
|
||||||
|
class FrostIPCTestCaseBase(IsolatedAsyncioTestCase): |
||||||
|
|
||||||
|
def setUp(self): |
||||||
|
load_test_config(config_path='./test_frost') |
||||||
|
btc.select_chain_params("bitcoin/regtest") |
||||||
|
cryptoengine.BTC_P2TR.VBYTE = 100 |
||||||
|
jm_single().bc_interface.tick_forward_chain_interval = 2 |
||||||
|
|
||||||
|
async def asyncSetUp(self): |
||||||
|
self.nick1, self.nick2, self.nick3 = ['nick1', 'nick2', 'nick3'] |
||||||
|
entropy1 = bytes.fromhex('8e5e5677fb302874a607b63ad03ba434') |
||||||
|
entropy2 = bytes.fromhex('38dfa80fbb21b32b2b2740e00a47de9d') |
||||||
|
entropy3 = bytes.fromhex('3ad9c77fcd1d537b6ef396952d1221a0') |
||||||
|
self.wlt1 = await get_populated_wallet(entropy1) |
||||||
|
self.wlt_svc1 = WalletService(self.wlt1) |
||||||
|
self.fc1 = FROSTClient(self.wlt_svc1) |
||||||
|
cfactory1 = DummyFrostJMClientProtocolFactory(self.fc1, self.nick1) |
||||||
|
self.wlt1.set_client_factory(cfactory1) |
||||||
|
|
||||||
|
self.wlt2 = await get_populated_wallet(entropy2) |
||||||
|
self.wlt_svc2 = WalletService(self.wlt2) |
||||||
|
self.fc2 = FROSTClient(self.wlt_svc2) |
||||||
|
cfactory2 = DummyFrostJMClientProtocolFactory(self.fc2, self.nick2) |
||||||
|
self.wlt2.set_client_factory(cfactory2) |
||||||
|
|
||||||
|
self.wlt3 = await get_populated_wallet(entropy3) |
||||||
|
self.wlt_svc3 = WalletService(self.wlt3) |
||||||
|
self.fc3 = FROSTClient(self.wlt_svc3) |
||||||
|
cfactory3 = DummyFrostJMClientProtocolFactory(self.fc3, self.nick3) |
||||||
|
self.wlt3.set_client_factory(cfactory3) |
||||||
|
|
||||||
|
cfactory1.add_party_client(self.nick2, cfactory2.proto_client) |
||||||
|
cfactory1.add_party_client(self.nick3, cfactory3.proto_client) |
||||||
|
|
||||||
|
cfactory2.add_party_client(self.nick1, cfactory1.proto_client) |
||||||
|
cfactory2.add_party_client(self.nick3, cfactory3.proto_client) |
||||||
|
|
||||||
|
cfactory3.add_party_client(self.nick1, cfactory1.proto_client) |
||||||
|
cfactory3.add_party_client(self.nick2, cfactory2.proto_client) |
||||||
|
|
||||||
|
await populate_dkg_session(self) |
||||||
|
|
||||||
|
self.ipcs = FrostIPCServer(self.wlt1) |
||||||
|
await self.ipcs.async_init() |
||||||
|
self.ipcc = FrostIPCClient(self.wlt1) |
||||||
|
await self.ipcc.async_init() |
||||||
|
self.wlt1.set_ipc_client(self.ipcc) |
||||||
|
|
||||||
|
|
||||||
|
class FrostIPCClientTestCase(FrostIPCTestCaseBase): |
||||||
|
|
||||||
|
async def asyncSetUp(self): |
||||||
|
await super().asyncSetUp() |
||||||
|
self.serve_task = asyncio.create_task(self.ipcs.serve_forever()) |
||||||
|
|
||||||
|
async def asyncTearDown(self): |
||||||
|
self.serve_task.cancel("cancel from asyncTearDown") |
||||||
|
|
||||||
|
async def test_get_dkg_pubkey(self): |
||||||
|
pubkey = await self.ipcc.get_dkg_pubkey(0, 0, 0) |
||||||
|
dkg = self.wlt1.dkg |
||||||
|
pubkeys = list(dkg._dkg_pubkey.values()) |
||||||
|
assert pubkey and pubkey in pubkeys |
||||||
|
|
||||||
|
pubkey = await self.ipcc.get_dkg_pubkey(0, 0, 1) |
||||||
|
pubkeys = list(dkg._dkg_pubkey.values()) |
||||||
|
assert pubkey and pubkey in pubkeys |
||||||
|
|
||||||
|
async def test_frost_sign(self): |
||||||
|
sighash = bytes.fromhex('01020304'*8) |
||||||
|
sig, pubkey, tweaked_pubkey = await self.ipcc.frost_sign(0, 0, 0, sighash) |
||||||
|
assert sig and len(sig) == 64 |
||||||
|
assert pubkey and len(pubkey) == 33 |
||||||
|
assert tweaked_pubkey and len(tweaked_pubkey) == 33 |
||||||
@ -0,0 +1,410 @@ |
|||||||
|
'''Wallet functionality tests.''' |
||||||
|
import os |
||||||
|
import json |
||||||
|
from pprint import pprint |
||||||
|
|
||||||
|
from unittest import IsolatedAsyncioTestCase |
||||||
|
|
||||||
|
import bencoder |
||||||
|
|
||||||
|
import jmclient # install asyncioreactor |
||||||
|
from twisted.internet import reactor |
||||||
|
|
||||||
|
import pytest |
||||||
|
import jmbitcoin as btc |
||||||
|
from jmbase import get_log |
||||||
|
from jmclient import ( |
||||||
|
load_test_config, jm_single, VolatileStorage, get_network, cryptoengine, |
||||||
|
create_wallet, open_test_wallet_maybe, FrostWallet, DKGManager) |
||||||
|
|
||||||
|
from jmfrost.chilldkg_ref.chilldkg import DKGOutput, hostpubkey_gen |
||||||
|
from jmclient.frost_clients import ( |
||||||
|
serialize_ext_recovery, decrypt_ext_recovery) |
||||||
|
|
||||||
|
pytestmark = pytest.mark.usefixtures("setup_regtest_frost_bitcoind") |
||||||
|
|
||||||
|
test_create_wallet_filename = "frost_testwallet_for_create_wallet_test" |
||||||
|
|
||||||
|
log = get_log() |
||||||
|
|
||||||
|
|
||||||
|
async def get_populated_wallet(): |
||||||
|
storage = VolatileStorage() |
||||||
|
dkg_storage = VolatileStorage() |
||||||
|
recovery_storage = VolatileStorage() |
||||||
|
FrostWallet.initialize(storage, dkg_storage, recovery_storage, |
||||||
|
get_network()) |
||||||
|
wallet = FrostWallet(storage, dkg_storage, recovery_storage) |
||||||
|
await wallet.async_init(storage) |
||||||
|
return wallet |
||||||
|
|
||||||
|
|
||||||
|
def populate_dkg(wlt, add_party=True, add_coordinator=True, save_dkg=True): |
||||||
|
pubkey = hostpubkey_gen(wlt._hostseckey[:32]) |
||||||
|
md_type_idx = (0, 0, 0) # mixdepth, address_type, index |
||||||
|
ext_recovery_bytes = serialize_ext_recovery(*md_type_idx) |
||||||
|
ext_recovery = btc.ecies_encrypt(ext_recovery_bytes, pubkey) |
||||||
|
if add_party: |
||||||
|
wlt.dkg.add_party_data( |
||||||
|
session_id=bytes.fromhex('aa'*32), |
||||||
|
dkg_output=DKGOutput( |
||||||
|
bytes.fromhex('01'*32), # secshare |
||||||
|
bytes.fromhex('02'*32 + '01'), # threshold_pubkey |
||||||
|
[ # pubshares |
||||||
|
bytes.fromhex('03'*32 + '02'), |
||||||
|
bytes.fromhex('03'*32 + '03'), |
||||||
|
bytes.fromhex('03'*32 + '04'), |
||||||
|
] |
||||||
|
), |
||||||
|
hostpubkeys=[ |
||||||
|
bytes.fromhex('02'*32 + '05'), |
||||||
|
bytes.fromhex('02'*32 + '06'), |
||||||
|
bytes.fromhex('02'*32 + '07'), |
||||||
|
], |
||||||
|
t=2, |
||||||
|
recovery_data=bytes.fromhex('0102030405'*10), |
||||||
|
ext_recovery=ext_recovery, |
||||||
|
save_dkg=save_dkg |
||||||
|
) |
||||||
|
if add_coordinator: |
||||||
|
wlt.dkg.add_coordinator_data( |
||||||
|
session_id=bytes.fromhex('bb'*32), |
||||||
|
dkg_output=DKGOutput( |
||||||
|
bytes.fromhex('11'*32), # secshare |
||||||
|
bytes.fromhex('02'*32 + '11'), # threshold_pubkey |
||||||
|
[ # pubshares |
||||||
|
bytes.fromhex('03'*32 + '12'), |
||||||
|
bytes.fromhex('03'*32 + '13'), |
||||||
|
bytes.fromhex('03'*32 + '14'), |
||||||
|
] |
||||||
|
), |
||||||
|
hostpubkeys=[ |
||||||
|
bytes.fromhex('02'*32 + '15'), |
||||||
|
bytes.fromhex('02'*32 + '16'), |
||||||
|
bytes.fromhex('02'*32 + '17'), |
||||||
|
], |
||||||
|
t=2, |
||||||
|
recovery_data=bytes.fromhex('0102030405'*10), |
||||||
|
ext_recovery=ext_recovery, |
||||||
|
save_dkg=save_dkg |
||||||
|
) |
||||||
|
return ext_recovery |
||||||
|
|
||||||
|
|
||||||
|
def check_dkg(wlt, ext_recovery, check_party=True, check_coordinator=True): |
||||||
|
if check_party: |
||||||
|
dkg_dict = wlt._dkg_storage.data[DKGManager.STORAGE_KEY] |
||||||
|
assert dkg_dict[DKGManager.SECSHARE_SUBKEY] == { |
||||||
|
b'\xaa'*32: b'\x01'*32 |
||||||
|
} |
||||||
|
assert dkg_dict[DKGManager.PUBSHARES_SUBKEY] == { |
||||||
|
b'\xaa'*32: [ |
||||||
|
bytes.fromhex('03'*32 + '02'), |
||||||
|
bytes.fromhex('03'*32 + '03'), |
||||||
|
bytes.fromhex('03'*32 + '04'), |
||||||
|
] |
||||||
|
} |
||||||
|
assert dkg_dict[DKGManager.PUBKEY_SUBKEY] == { |
||||||
|
b'\xaa'*32: bytes.fromhex('02'*32 + '01'), |
||||||
|
} |
||||||
|
assert dkg_dict[DKGManager.HOSTPUBKEYS_SUBKEY] == { |
||||||
|
b'\xaa'*32: [ |
||||||
|
bytes.fromhex('02'*32 + '05'), |
||||||
|
bytes.fromhex('02'*32 + '06'), |
||||||
|
bytes.fromhex('02'*32 + '07'), |
||||||
|
] |
||||||
|
} |
||||||
|
assert dkg_dict[DKGManager.T_SUBKEY] == { |
||||||
|
b'\xaa'*32: 2, |
||||||
|
} |
||||||
|
assert dkg_dict[DKGManager.SESSIONS_SUBKEY] == dict() |
||||||
|
rec_dict = wlt._recovery_storage.data[DKGManager.RECOVERY_STORAGE_KEY] |
||||||
|
assert rec_dict == { |
||||||
|
b'\xaa'*32: [ |
||||||
|
ext_recovery, |
||||||
|
bytes.fromhex('0102030405'*10), |
||||||
|
], |
||||||
|
} |
||||||
|
if check_coordinator: |
||||||
|
ext_recovery_bytes = decrypt_ext_recovery(wlt._hostseckey, |
||||||
|
ext_recovery) |
||||||
|
dkg_dict = wlt._dkg_storage.data[DKGManager.STORAGE_KEY] |
||||||
|
assert dkg_dict[DKGManager.SECSHARE_SUBKEY] == { |
||||||
|
b'\xbb'*32: b'\x11'*32 |
||||||
|
} |
||||||
|
assert dkg_dict[DKGManager.PUBSHARES_SUBKEY] == { |
||||||
|
b'\xbb'*32: [ |
||||||
|
bytes.fromhex('03'*32 + '12'), |
||||||
|
bytes.fromhex('03'*32 + '13'), |
||||||
|
bytes.fromhex('03'*32 + '14'), |
||||||
|
] |
||||||
|
} |
||||||
|
assert dkg_dict[DKGManager.PUBKEY_SUBKEY] == { |
||||||
|
b'\xbb'*32: bytes.fromhex('02'*32 + '11'), |
||||||
|
} |
||||||
|
assert dkg_dict[DKGManager.HOSTPUBKEYS_SUBKEY] == { |
||||||
|
b'\xbb'*32: [ |
||||||
|
bytes.fromhex('02'*32 + '15'), |
||||||
|
bytes.fromhex('02'*32 + '16'), |
||||||
|
bytes.fromhex('02'*32 + '17'), |
||||||
|
] |
||||||
|
} |
||||||
|
assert dkg_dict[DKGManager.T_SUBKEY] == { |
||||||
|
b'\xbb'*32: 2, |
||||||
|
} |
||||||
|
assert dkg_dict[DKGManager.SESSIONS_SUBKEY] == { |
||||||
|
ext_recovery_bytes: b'\xbb'*32 |
||||||
|
} |
||||||
|
rec_dict = wlt._recovery_storage.data[DKGManager.RECOVERY_STORAGE_KEY] |
||||||
|
assert rec_dict == { |
||||||
|
b'\xbb'*32: [ |
||||||
|
ext_recovery, |
||||||
|
bytes.fromhex('0102030405'*10), |
||||||
|
], |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
class AsyncioTestCase(IsolatedAsyncioTestCase): |
||||||
|
|
||||||
|
params = { |
||||||
|
'test_is_standard_wallet_script': [FrostWallet] |
||||||
|
} |
||||||
|
|
||||||
|
def setUp(self): |
||||||
|
load_test_config(config_path='./test_frost') |
||||||
|
btc.select_chain_params("bitcoin/regtest") |
||||||
|
#see note in cryptoengine.py: |
||||||
|
cryptoengine.BTC_P2TR.VBYTE = 100 |
||||||
|
jm_single().bc_interface.tick_forward_chain_interval = 2 |
||||||
|
|
||||||
|
def tearDown(self): |
||||||
|
if os.path.exists(test_create_wallet_filename): |
||||||
|
os.remove(test_create_wallet_filename) |
||||||
|
dkg_filename = f'{test_create_wallet_filename}.dkg' |
||||||
|
recovery_filename = f'{test_create_wallet_filename}.dkg_recovery' |
||||||
|
if os.path.exists(dkg_filename): |
||||||
|
os.remove(dkg_filename) |
||||||
|
if os.path.exists(recovery_filename): |
||||||
|
os.remove(recovery_filename) |
||||||
|
|
||||||
|
async def test_create_wallet(self): |
||||||
|
password = b"hunter2" |
||||||
|
wallet_name = test_create_wallet_filename |
||||||
|
# test mainnet (we are not transacting) |
||||||
|
btc.select_chain_params("bitcoin") |
||||||
|
wallet = await create_wallet(wallet_name, password, 4, FrostWallet) |
||||||
|
mnemonic = wallet.get_mnemonic_words()[0] |
||||||
|
wallet.close() |
||||||
|
# ensure that the wallet file created is openable with the password, |
||||||
|
# and has the parameters that were claimed on creation: |
||||||
|
new_wallet = await open_test_wallet_maybe( |
||||||
|
wallet_name, "", 4, password=password, ask_for_password=False) |
||||||
|
assert new_wallet.get_mnemonic_words()[0] == mnemonic |
||||||
|
btc.select_chain_params("bitcoin/regtest") |
||||||
|
|
||||||
|
async def test_dkg_manager_initialized(self): |
||||||
|
wlt = await get_populated_wallet() |
||||||
|
dkg_dict = wlt._dkg_storage.data[DKGManager.STORAGE_KEY] |
||||||
|
assert set(dkg_dict.keys()) == set([ |
||||||
|
DKGManager.SECSHARE_SUBKEY, |
||||||
|
DKGManager.PUBSHARES_SUBKEY, |
||||||
|
DKGManager.PUBKEY_SUBKEY, |
||||||
|
DKGManager.HOSTPUBKEYS_SUBKEY, |
||||||
|
DKGManager.T_SUBKEY, |
||||||
|
DKGManager.SESSIONS_SUBKEY, |
||||||
|
]) |
||||||
|
|
||||||
|
assert dkg_dict[DKGManager.SECSHARE_SUBKEY] == dict() |
||||||
|
assert dkg_dict[DKGManager.PUBSHARES_SUBKEY] == dict() |
||||||
|
assert dkg_dict[DKGManager.PUBKEY_SUBKEY] == dict() |
||||||
|
assert dkg_dict[DKGManager.HOSTPUBKEYS_SUBKEY] == dict() |
||||||
|
assert dkg_dict[DKGManager.T_SUBKEY] == dict() |
||||||
|
assert dkg_dict[DKGManager.SESSIONS_SUBKEY] == dict() |
||||||
|
rec_dict = wlt._recovery_storage.data[DKGManager.RECOVERY_STORAGE_KEY] |
||||||
|
assert rec_dict == dict() |
||||||
|
|
||||||
|
async def test_dkg_add_party_data(self): |
||||||
|
wlt = await get_populated_wallet() |
||||||
|
ext_recovery = populate_dkg(wlt, True, False) |
||||||
|
check_dkg(wlt, ext_recovery, True, False) |
||||||
|
|
||||||
|
async def test_dkg_add_coordinator_data(self): |
||||||
|
wlt = await get_populated_wallet() |
||||||
|
ext_recovery = populate_dkg(wlt, False, True) |
||||||
|
check_dkg(wlt, ext_recovery, False, True) |
||||||
|
|
||||||
|
async def test_dkg_save(self): |
||||||
|
wlt = await get_populated_wallet() |
||||||
|
ext_recovery = populate_dkg(wlt, True, True, save_dkg=False) |
||||||
|
ext_recovery_bytes = decrypt_ext_recovery(wlt._hostseckey, |
||||||
|
ext_recovery) |
||||||
|
|
||||||
|
saved_dkg = bencoder.bdecode(wlt._dkg_storage.file_data[8:]) |
||||||
|
STORAGE_KEY = DKGManager.STORAGE_KEY |
||||||
|
HOSTPUBKEYS_SUBKEY = DKGManager.HOSTPUBKEYS_SUBKEY |
||||||
|
PUBKEY_SUBKEY = DKGManager.PUBKEY_SUBKEY |
||||||
|
PUBSHARES_SUBKEY = DKGManager.PUBSHARES_SUBKEY |
||||||
|
SECSHARE_SUBKEY = DKGManager.SECSHARE_SUBKEY |
||||||
|
T_SUBKEY = DKGManager.T_SUBKEY |
||||||
|
SESSIONS_SUBKEY = DKGManager.SESSIONS_SUBKEY |
||||||
|
|
||||||
|
assert saved_dkg[STORAGE_KEY][SECSHARE_SUBKEY] == dict() |
||||||
|
assert saved_dkg[STORAGE_KEY][PUBSHARES_SUBKEY] == dict() |
||||||
|
assert saved_dkg[STORAGE_KEY][PUBKEY_SUBKEY] == dict() |
||||||
|
assert saved_dkg[STORAGE_KEY][HOSTPUBKEYS_SUBKEY] == dict() |
||||||
|
assert saved_dkg[STORAGE_KEY][T_SUBKEY] == dict() |
||||||
|
assert saved_dkg[STORAGE_KEY][SESSIONS_SUBKEY] == dict() |
||||||
|
|
||||||
|
saved_rec = bencoder.bdecode(wlt._recovery_storage.file_data[8:]) |
||||||
|
assert saved_rec[b'dkg'] == dict() |
||||||
|
|
||||||
|
wlt.dkg.save() |
||||||
|
|
||||||
|
saved_dkg = bencoder.bdecode(wlt._dkg_storage.file_data[8:]) |
||||||
|
assert set(saved_dkg[STORAGE_KEY][SECSHARE_SUBKEY].keys()) == set([ |
||||||
|
b'\xaa'*32, |
||||||
|
b'\xbb'*32, |
||||||
|
]) |
||||||
|
assert set(saved_dkg[STORAGE_KEY][PUBSHARES_SUBKEY].keys()) == set([ |
||||||
|
b'\xaa'*32, |
||||||
|
b'\xbb'*32, |
||||||
|
]) |
||||||
|
assert set(saved_dkg[STORAGE_KEY][PUBKEY_SUBKEY].keys()) == set([ |
||||||
|
b'\xaa'*32, |
||||||
|
b'\xbb'*32, |
||||||
|
]) |
||||||
|
assert set(saved_dkg[STORAGE_KEY][HOSTPUBKEYS_SUBKEY].keys()) == set([ |
||||||
|
b'\xaa'*32, |
||||||
|
b'\xbb'*32, |
||||||
|
]) |
||||||
|
assert set(saved_dkg[STORAGE_KEY][T_SUBKEY].keys()) == set([ |
||||||
|
b'\xaa'*32, |
||||||
|
b'\xbb'*32, |
||||||
|
]) |
||||||
|
assert set(saved_dkg[STORAGE_KEY][SESSIONS_SUBKEY].keys()) == set([ |
||||||
|
ext_recovery_bytes |
||||||
|
]) |
||||||
|
|
||||||
|
saved_rec = bencoder.bdecode(wlt._recovery_storage.file_data[8:]) |
||||||
|
RECOVERY_STORAGE_KEY = DKGManager.RECOVERY_STORAGE_KEY |
||||||
|
assert set(saved_rec[RECOVERY_STORAGE_KEY].keys()) == set([ |
||||||
|
b'\xaa'*32, |
||||||
|
b'\xbb'*32, |
||||||
|
]) |
||||||
|
|
||||||
|
async def test_dkg_load_storage(self): |
||||||
|
password = b"hunter2" |
||||||
|
wlt = await create_wallet( |
||||||
|
test_create_wallet_filename, password, 4, FrostWallet) |
||||||
|
mnemonic = wlt.get_mnemonic_words()[0] |
||||||
|
ext_recovery = populate_dkg(wlt, False, True) |
||||||
|
check_dkg(wlt, ext_recovery, False, True) |
||||||
|
wlt.save() |
||||||
|
wlt.close() |
||||||
|
|
||||||
|
new_wlt = await open_test_wallet_maybe( |
||||||
|
test_create_wallet_filename, "", 4, password=password, |
||||||
|
ask_for_password=False, |
||||||
|
load_dkg=True, dkg_read_only=False, read_only=True) |
||||||
|
|
||||||
|
dkgman = DKGManager( |
||||||
|
new_wlt, new_wlt._dkg_storage, new_wlt._recovery_storage) |
||||||
|
new_wlt._dkg = dkgman |
||||||
|
check_dkg(new_wlt, ext_recovery, False, True) |
||||||
|
|
||||||
|
async def test_dkg_find_session(self): |
||||||
|
wlt = await get_populated_wallet() |
||||||
|
ext_recovery = populate_dkg(wlt, True, True) |
||||||
|
assert wlt.dkg.find_session(0, 0, 0) == b'\xbb'*32 |
||||||
|
assert wlt.dkg.find_session(0, 0, 1) is None |
||||||
|
|
||||||
|
async def test_dkg_find_dkg_pubkey(self): |
||||||
|
wlt = await get_populated_wallet() |
||||||
|
ext_recovery = populate_dkg(wlt, True, True) |
||||||
|
assert wlt.dkg.find_dkg_pubkey(0, 0, 0) == b'\x02'*32 + b'\x11' |
||||||
|
assert wlt.dkg.find_dkg_pubkey(0, 0, 1) is None |
||||||
|
|
||||||
|
async def test_dkg_recover(self): |
||||||
|
assert 0 # FIXME need to add test |
||||||
|
|
||||||
|
async def test_dkg_ls(self): |
||||||
|
wlt = await get_populated_wallet() |
||||||
|
ext_recovery = populate_dkg(wlt, True, True) |
||||||
|
ls_data = wlt.dkg.dkg_ls() |
||||||
|
ls_title = 'DKG data:\n' |
||||||
|
ls_title_len = len(ls_title) |
||||||
|
assert ls_data.startswith(ls_title) |
||||||
|
ls_data = ls_data[ls_title_len:] |
||||||
|
ls_json = json.loads(ls_data) |
||||||
|
assert set(ls_json.keys()) == set(['sessions', 'a'*64, 'b'*64]) |
||||||
|
assert ls_json['sessions']['0,0,0'] == 'b'*64 |
||||||
|
ls_json_a = ls_json['a'*64] |
||||||
|
ls_json_b = ls_json['b'*64] |
||||||
|
|
||||||
|
assert ls_json_a['secshare'] == '01'*32 |
||||||
|
assert set(ls_json_a['pubshares']) == set(['03'*32 + '02', |
||||||
|
'03'*32 + '03', |
||||||
|
'03'*32 + '04']) |
||||||
|
assert ls_json_a['pubkey'] == '02'*32 + '01' |
||||||
|
assert set(ls_json_a['hostpubkeys']) == set(['02'*32 + '05', |
||||||
|
'02'*32 + '06', |
||||||
|
'02'*32 + '07']) |
||||||
|
assert ls_json_a['t'] == 2 |
||||||
|
|
||||||
|
assert ls_json_b['secshare'] == '11'*32 |
||||||
|
assert set(ls_json_b['pubshares']) == set(['03'*32 + '12', |
||||||
|
'03'*32 + '13', |
||||||
|
'03'*32 + '14']) |
||||||
|
assert ls_json_b['pubkey'] == '02'*32 + '11' |
||||||
|
assert set(ls_json_b['hostpubkeys']) == set(['02'*32 + '15', |
||||||
|
'02'*32 + '16', |
||||||
|
'02'*32 + '17']) |
||||||
|
assert ls_json_b['t'] == 2 |
||||||
|
|
||||||
|
async def test_dkg_rm(self): |
||||||
|
wlt = await get_populated_wallet() |
||||||
|
ext_recovery = populate_dkg(wlt, True, True) |
||||||
|
rm_data = wlt.dkg.dkg_rm(['a'*64]) |
||||||
|
rm_lines = rm_data.split('\n') |
||||||
|
assert rm_lines[0] == f'dkg data for session {"a"*64} deleted' |
||||||
|
rm_data = wlt.dkg.dkg_rm(['a'*64]) |
||||||
|
rm_lines = rm_data.split('\n') |
||||||
|
assert rm_lines[0] == f'not found dkg data for session {"a"*64}' |
||||||
|
|
||||||
|
rm_data = wlt.dkg.dkg_rm(['b'*64]) |
||||||
|
rm_lines = rm_data.split('\n') |
||||||
|
assert rm_lines[0] == f'dkg data for session {"b"*64} deleted' |
||||||
|
assert rm_lines[1] == f'session data for session {"b"*64} deleted' |
||||||
|
rm_data = wlt.dkg.dkg_rm(['b'*64]) |
||||||
|
rm_lines = rm_data.split('\n') |
||||||
|
assert rm_lines[0] == f'not found dkg data for session {"b"*64}' |
||||||
|
|
||||||
|
async def test_recdkg_ls(self): |
||||||
|
wlt = await get_populated_wallet() |
||||||
|
ext_recovery = populate_dkg(wlt, True, True) |
||||||
|
ls_data = wlt.dkg.recdkg_ls() |
||||||
|
ls_lines = ls_data.split('\n') |
||||||
|
assert ls_lines[1] == 'Decrypted sesions:' |
||||||
|
assert ls_lines[-2] == 'Not decrypted sesions:' |
||||||
|
assert ls_lines[-1] == '[]' |
||||||
|
ls_json = json.loads('\n'.join(ls_lines[2:-3])) |
||||||
|
assert ls_json[0] == ['a'*64, '0'*12, '0102030405'*10] |
||||||
|
assert ls_json[1] == ['b'*64, '0'*12, '0102030405'*10] |
||||||
|
|
||||||
|
async def test_recdkg_rm(self): |
||||||
|
wlt = await get_populated_wallet() |
||||||
|
ext_recovery = populate_dkg(wlt, True, True) |
||||||
|
rm_data = wlt.dkg.recdkg_rm(['a'*64]) |
||||||
|
rm_lines = rm_data.split('\n') |
||||||
|
assert rm_lines[0] == f'dkg recovery data for session {"a"*64} deleted' |
||||||
|
rm_data = wlt.dkg.recdkg_rm(['a'*64]) |
||||||
|
rm_lines = rm_data.split('\n') |
||||||
|
assert rm_lines[0] == \ |
||||||
|
f'not found dkg recovery data for session {"a"*64}' |
||||||
|
rm_data = wlt.dkg.recdkg_rm(['b'*64]) |
||||||
|
rm_lines = rm_data.split('\n') |
||||||
|
assert rm_lines[0] == f'dkg recovery data for session {"b"*64} deleted' |
||||||
|
rm_data = wlt.dkg.recdkg_rm(['b'*64]) |
||||||
|
rm_lines = rm_data.split('\n') |
||||||
|
assert rm_lines[0] == \ |
||||||
|
f'not found dkg recovery data for session {"b"*64}' |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,152 @@ |
|||||||
|
# Implementation of the Trusted Dealer Key Generation approach for FROST mentioned |
||||||
|
# in https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/15/ (Appendix D). |
||||||
|
# |
||||||
|
# It's worth noting that this isn't the only compatible method (with BIP FROST Signing), |
||||||
|
# there are alternative key generation methods available, such as BIP-FROST-DKG: |
||||||
|
# https://github.com/BlockstreamResearch/bip-frost-dkg |
||||||
|
|
||||||
|
# todo: use the `Scalar` type like BIP-DKG? |
||||||
|
#todo: this shows mypy error, but the file runs |
||||||
|
|
||||||
|
from typing import Tuple, List, NewType |
||||||
|
import unittest |
||||||
|
# todo: replace random module with secrets |
||||||
|
import random |
||||||
|
# for [1] import functions from reference |
||||||
|
# [2] specify path for bip340 when running reference.py |
||||||
|
# import sys, os |
||||||
|
# script_dir = os.path.dirname(os.path.abspath(__file__)) |
||||||
|
# parent_dir = os.path.abspath(os.path.join(script_dir, '..')) |
||||||
|
# sys.path.append(parent_dir) |
||||||
|
from jmfrost.frost_ref.utils.bip340 import ( |
||||||
|
Point, n as curve_order, bytes_from_int, |
||||||
|
point_mul, G, has_even_y, x |
||||||
|
) |
||||||
|
|
||||||
|
# point on the secret polynomial, represents a signer's secret share |
||||||
|
PolyPoint = Tuple[int, int] |
||||||
|
# point on the secp256k1 curve, represents a signer's public share |
||||||
|
ECPoint = Point |
||||||
|
|
||||||
|
# |
||||||
|
# The following helper functions and types were copied from reference.py |
||||||
|
# |
||||||
|
PlainPk = NewType('PlainPk', bytes) |
||||||
|
|
||||||
|
def xbytes(P: Point) -> bytes: |
||||||
|
return bytes_from_int(x(P)) |
||||||
|
|
||||||
|
def cbytes(P: Point) -> bytes: |
||||||
|
a = b'\x02' if has_even_y(P) else b'\x03' |
||||||
|
return a + xbytes(P) |
||||||
|
|
||||||
|
def derive_interpolating_value_internal(L: List[int], x_i: int) -> int: |
||||||
|
num, deno = 1, 1 |
||||||
|
for x_j in L: |
||||||
|
if x_j == x_i: |
||||||
|
continue |
||||||
|
num *= x_j |
||||||
|
deno *= (x_j - x_i) |
||||||
|
return num * pow(deno, curve_order - 2, curve_order) % curve_order |
||||||
|
# |
||||||
|
# End of helper functions and types copied from reference.py. |
||||||
|
# |
||||||
|
|
||||||
|
# evaluates poly using Horner's method, assuming coeff[0] corresponds |
||||||
|
# to the coefficient of highest degree term |
||||||
|
def polynomial_evaluate(coeffs: List[int], x: int) -> int: |
||||||
|
res = 0 |
||||||
|
for coeff in coeffs: |
||||||
|
res = res * x + coeff |
||||||
|
return res % curve_order |
||||||
|
|
||||||
|
|
||||||
|
def secret_share_combine(shares: List[PolyPoint]) -> int: |
||||||
|
x_coords = [] |
||||||
|
for (x, y) in shares: |
||||||
|
x_coords.append(x) |
||||||
|
|
||||||
|
secret = 0 |
||||||
|
for (x, y) in shares: |
||||||
|
delta = y * derive_interpolating_value_internal(x_coords, x) |
||||||
|
secret += delta |
||||||
|
return secret % curve_order |
||||||
|
|
||||||
|
# coeffs shouldn't include the const term (i.e. secret) |
||||||
|
def secret_share_shard(secret: int, coeffs: List[int], max_participants: int) -> List[PolyPoint]: |
||||||
|
coeffs = coeffs + [secret] |
||||||
|
|
||||||
|
secshares: List[PolyPoint] = [] |
||||||
|
for x_i in range(1, max_participants + 1): |
||||||
|
y_i = polynomial_evaluate(coeffs, x_i) |
||||||
|
secshare_i = (x_i, y_i) |
||||||
|
secshares.append(secshare_i) |
||||||
|
return secshares |
||||||
|
|
||||||
|
def trusted_dealer_keygen(secret_key: int, max_participants: int, min_participants: int) -> Tuple[ECPoint, List[PolyPoint], List[ECPoint]]: |
||||||
|
assert (1 <= secret_key <= curve_order - 1) |
||||||
|
assert (2 <= min_participants <= max_participants) |
||||||
|
# we don't force BIP340 compatibility of group pubkey in keygen |
||||||
|
P = point_mul(G, secret_key) |
||||||
|
assert P is not None |
||||||
|
|
||||||
|
coeffs = [] |
||||||
|
for i in range(min_participants - 1): |
||||||
|
coeffs.append(random.randint(1, curve_order - 1)) |
||||||
|
secshares = secret_share_shard(secret_key, coeffs, max_participants) |
||||||
|
pubshares = [] |
||||||
|
for secshare in secshares: |
||||||
|
X = point_mul(G, secshare[1]) |
||||||
|
assert X is not None |
||||||
|
pubshares.append(X) |
||||||
|
return (P, secshares, pubshares) |
||||||
|
|
||||||
|
# Test vector from RFC draft. |
||||||
|
# section F.5 of https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/15/ |
||||||
|
class Tests(unittest.TestCase): |
||||||
|
def setUp(self) -> None: |
||||||
|
self.max_participants = 3 |
||||||
|
self.min_participants = 2 |
||||||
|
self.poly = [ |
||||||
|
0xfbf85eadae3058ea14f19148bb72b45e4399c0b16028acaf0395c9b03c823579, |
||||||
|
0x0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114, |
||||||
|
] |
||||||
|
self.shares: List[PolyPoint] = [ |
||||||
|
(1, 0x08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c), |
||||||
|
(2, 0x04f0feac2edcedc6ce1253b7fab8c86b856a797f44d83d82a385554e6e401984), |
||||||
|
(3, 0x00e95d59dd0d46b0e303e500b62b7ccb0e555d49f5b849f5e748c071da8c0dbc), |
||||||
|
] |
||||||
|
self.secret = 0x0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114 |
||||||
|
|
||||||
|
def test_polynomial_evaluate(self) -> None: |
||||||
|
coeffs = self.poly.copy() |
||||||
|
expected_secret = self.secret |
||||||
|
|
||||||
|
self.assertEqual(polynomial_evaluate(coeffs, 0), expected_secret) |
||||||
|
|
||||||
|
def test_secret_share_combine(self) -> None: |
||||||
|
shares: List[PolyPoint] = self.shares.copy() |
||||||
|
expected_secret = self.secret |
||||||
|
|
||||||
|
self.assertEqual(secret_share_combine([shares[0], shares[1]]), expected_secret) |
||||||
|
self.assertEqual(secret_share_combine([shares[1], shares[2]]), expected_secret) |
||||||
|
self.assertEqual(secret_share_combine([shares[0], shares[2]]), expected_secret) |
||||||
|
self.assertEqual(secret_share_combine(shares), expected_secret) |
||||||
|
|
||||||
|
def test_trusted_dealer_keygen(self) -> None: |
||||||
|
secret_key = random.randint(1, curve_order - 1) |
||||||
|
max_participants = 5 |
||||||
|
min_participants = 3 |
||||||
|
group_pk, secshares, pubshares = trusted_dealer_keygen(secret_key, max_participants, min_participants) |
||||||
|
|
||||||
|
# group_pk need not be xonly (i.e., have even y always) |
||||||
|
self.assertEqual(group_pk, point_mul(G, secret_key)) |
||||||
|
self.assertEqual(secret_share_combine(secshares), secret_key) |
||||||
|
self.assertEqual(len(secshares), max_participants) |
||||||
|
self.assertEqual(len(pubshares), max_participants) |
||||||
|
for i in range(len(pubshares)): |
||||||
|
with self.subTest(i=i): |
||||||
|
self.assertEqual(pubshares[i], point_mul(G, secshares[i][1])) |
||||||
|
|
||||||
|
if __name__=='__main__': |
||||||
|
unittest.main() |
||||||
@ -0,0 +1,154 @@ |
|||||||
|
#NOTE: This configuration file is for testing with regtest only |
||||||
|
#For mainnet usage, running a JoinMarket script will create the default file |
||||||
|
[DAEMON] |
||||||
|
no_daemon = 1 |
||||||
|
daemon_port = 27183 |
||||||
|
daemon_host = localhost |
||||||
|
use_ssl = false |
||||||
|
|
||||||
|
[BLOCKCHAIN] |
||||||
|
blockchain_source = regtest |
||||||
|
rpc_host = localhost |
||||||
|
rpc_port = 18443 |
||||||
|
rpc_user = bitcoinrpc |
||||||
|
rpc_password = 123456abcdef |
||||||
|
network = testnet |
||||||
|
rpc_wallet_file = jm-test-frost-wallet |
||||||
|
|
||||||
|
[MESSAGING:server1] |
||||||
|
type = irc |
||||||
|
host = localhost |
||||||
|
hostid = localhost1 |
||||||
|
channel = joinmarket-pit |
||||||
|
port = 16667 |
||||||
|
usessl = false |
||||||
|
socks5 = false |
||||||
|
socks5_host = localhost |
||||||
|
socks5_port = 9150 |
||||||
|
|
||||||
|
[MESSAGING:server2] |
||||||
|
type = irc |
||||||
|
host = localhost |
||||||
|
hostid = localhost2 |
||||||
|
channel = joinmarket-pit |
||||||
|
port = 16668 |
||||||
|
usessl = false |
||||||
|
socks5 = false |
||||||
|
socks5_host = localhost |
||||||
|
socks5_port = 9150 |
||||||
|
|
||||||
|
[MESSAGING:onion] |
||||||
|
# onion based message channels must have the exact type 'onion' |
||||||
|
# (while the section name above can be MESSAGING:whatever), and there must |
||||||
|
# be only ONE such message channel configured (note the directory servers |
||||||
|
# can be multiple, below): |
||||||
|
type = onion |
||||||
|
socks5_host = localhost |
||||||
|
socks5_port = 9050 |
||||||
|
# the tor control configuration: |
||||||
|
tor_control_host = localhost |
||||||
|
# or, to use a UNIX socket |
||||||
|
# control_host = unix:/var/run/tor/control |
||||||
|
tor_control_port = 9051 |
||||||
|
# the host/port actually serving the hidden service |
||||||
|
# (note the *virtual port*, that the client uses, |
||||||
|
# is hardcoded to 80): |
||||||
|
onion_serving_host = 127.0.0.1 |
||||||
|
onion_serving_port = 8080 |
||||||
|
# This is mandatory for directory nodes (who must also set their |
||||||
|
# own .onion:port as the only directory in directory_nodes, below), |
||||||
|
# but NOT TO BE USED by non-directory nodes (which is you, unless |
||||||
|
# you know otherwise!), as it will greatly degrade your privacy. |
||||||
|
# |
||||||
|
# Special handling on regtest, so just ignore and let the code handle it: |
||||||
|
hidden_service_dir = "" |
||||||
|
# This is a comma separated list (comma can be omitted if only one item). |
||||||
|
# Each item has format host:port |
||||||
|
# On regtest we are going to increment the port numbers served from, with |
||||||
|
# the value used here as the starting value: |
||||||
|
directory_nodes = localhost:8081 |
||||||
|
# this is not present in default real config |
||||||
|
# and is specifically used to flag tests: |
||||||
|
# means we use indices 1,2,3,4,5: |
||||||
|
regtest_count=1,5 |
||||||
|
|
||||||
|
[TIMEOUT] |
||||||
|
maker_timeout_sec = 10 |
||||||
|
|
||||||
|
[LOGGING] |
||||||
|
console_log_level = DEBUG |
||||||
|
|
||||||
|
[POLICY] |
||||||
|
|
||||||
|
segwit = true |
||||||
|
native = true |
||||||
|
frost = true |
||||||
|
|
||||||
|
# for dust sweeping, try merge_algorithm = gradual |
||||||
|
# for more rapid dust sweeping, try merge_algorithm = greedy |
||||||
|
# for most rapid dust sweeping, try merge_algorithm = greediest |
||||||
|
# but don't forget to bump your miner fees! |
||||||
|
merge_algorithm = default |
||||||
|
# the fee estimate is based on a projection of how many satoshis |
||||||
|
# per kB are needed to get in one of the next N blocks, N set here |
||||||
|
# as the value of 'tx_fees'. This estimate can be extremely high |
||||||
|
# if you set N=1, so we choose N=3 for a more reasonable figure, |
||||||
|
# as our default. Note that for clients not using a local blockchain |
||||||
|
# instance, we retrieve an estimate from the API at cointape.com, currently. |
||||||
|
tx_fees = 3 |
||||||
|
taker_utxo_retries = 3 |
||||||
|
taker_utxo_age = 1 |
||||||
|
taker_utxo_amtpercent = 20 |
||||||
|
accept_commitment_broadcasts = 1 |
||||||
|
#some settings useful for testing scenarios |
||||||
|
#laxity for repeated tests; tests on actual |
||||||
|
#commitments/maker limit/utxo sourcing logic should obviously reset |
||||||
|
taker_utxo_retries = 3 |
||||||
|
minimum_makers = 1 |
||||||
|
listunspent_args = [0] |
||||||
|
max_sats_freeze_reuse = -1 |
||||||
|
|
||||||
|
# ONLY for testing! |
||||||
|
max_cj_fee_abs = 200000 |
||||||
|
max_cj_fee_rel = 0.2 |
||||||
|
|
||||||
|
[PAYJOIN] |
||||||
|
# for the majority of situations, the defaults |
||||||
|
# need not be altered - they will ensure you don't pay |
||||||
|
# a significantly higher fee. |
||||||
|
# MODIFICATION OF THESE SETTINGS IS DISADVISED. |
||||||
|
|
||||||
|
# Payjoin protocol version; currently only '1' is supported. |
||||||
|
payjoin_version = 1 |
||||||
|
|
||||||
|
# servers can change their destination address by default (0). |
||||||
|
# if '1', they cannot. Note that servers can explicitly request |
||||||
|
# that this is activated, in which case we respect that choice. |
||||||
|
disable_output_substitution = 0 |
||||||
|
|
||||||
|
# "default" here indicates that we will allow the receiver to |
||||||
|
# increase the fee we pay by: |
||||||
|
# 1.2 * (our_fee_rate_per_vbyte * vsize_of_our_input_type) |
||||||
|
# (see https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#span_idfeeoutputspanFee_output) |
||||||
|
# (and 1.2 to give breathing room) |
||||||
|
# which indicates we are allowing roughly one extra input's fee. |
||||||
|
# If it is instead set to an integer, then that many satoshis are allowed. |
||||||
|
# Additionally, note that we will also set the parameter additionafeeoutputindex |
||||||
|
# to that of our change output, unless there is none in which case this is disabled. |
||||||
|
max_additional_fee_contribution = default |
||||||
|
|
||||||
|
# this is the minimum satoshis per vbyte we allow in the payjoin |
||||||
|
# transaction; note it is decimal, not integer. |
||||||
|
min_fee_rate = 1.1 |
||||||
|
|
||||||
|
# for payjoins to hidden service endpoints, the socks5 configuration: |
||||||
|
onion_socks5_host = localhost |
||||||
|
onion_socks5_port = 9050 |
||||||
|
# in some exceptional case the HS may be SSL configured, |
||||||
|
# this feature is not yet implemented in code, but here for the |
||||||
|
# future: |
||||||
|
hidden_service_ssl = false |
||||||
|
|
||||||
|
[FROST] |
||||||
|
t = 2 |
||||||
|
hostpubkeys = 024cc1ec6fedba593a6cb683b627953b2aa80f8df80f360f78805fc00898697c74,03a8348fe4afd1974d07a50783c5d5c1ef59200eeb8ab97c7d8606534749a7043d,0307952377783138b82b222fd73199c541338a96cf758ed5a27816d6e6a324e77d |
||||||
@ -0,0 +1,150 @@ |
|||||||
|
#NOTE: This configuration file is for testing with regtest only |
||||||
|
#For mainnet usage, running a JoinMarket script will create the default file |
||||||
|
[DAEMON] |
||||||
|
no_daemon = 1 |
||||||
|
daemon_port = 27183 |
||||||
|
daemon_host = localhost |
||||||
|
use_ssl = false |
||||||
|
|
||||||
|
[BLOCKCHAIN] |
||||||
|
blockchain_source = regtest |
||||||
|
rpc_host = localhost |
||||||
|
rpc_port = 18443 |
||||||
|
rpc_user = bitcoinrpc |
||||||
|
rpc_password = 123456abcdef |
||||||
|
network = testnet |
||||||
|
rpc_wallet_file = jm-test-taproot-wallet |
||||||
|
|
||||||
|
[MESSAGING:server1] |
||||||
|
type = irc |
||||||
|
host = localhost |
||||||
|
hostid = localhost1 |
||||||
|
channel = joinmarket-pit |
||||||
|
port = 16667 |
||||||
|
usessl = false |
||||||
|
socks5 = false |
||||||
|
socks5_host = localhost |
||||||
|
socks5_port = 9150 |
||||||
|
|
||||||
|
[MESSAGING:server2] |
||||||
|
type = irc |
||||||
|
host = localhost |
||||||
|
hostid = localhost2 |
||||||
|
channel = joinmarket-pit |
||||||
|
port = 16668 |
||||||
|
usessl = false |
||||||
|
socks5 = false |
||||||
|
socks5_host = localhost |
||||||
|
socks5_port = 9150 |
||||||
|
|
||||||
|
[MESSAGING:onion] |
||||||
|
# onion based message channels must have the exact type 'onion' |
||||||
|
# (while the section name above can be MESSAGING:whatever), and there must |
||||||
|
# be only ONE such message channel configured (note the directory servers |
||||||
|
# can be multiple, below): |
||||||
|
type = onion |
||||||
|
socks5_host = localhost |
||||||
|
socks5_port = 9050 |
||||||
|
# the tor control configuration: |
||||||
|
tor_control_host = localhost |
||||||
|
# or, to use a UNIX socket |
||||||
|
# control_host = unix:/var/run/tor/control |
||||||
|
tor_control_port = 9051 |
||||||
|
# the host/port actually serving the hidden service |
||||||
|
# (note the *virtual port*, that the client uses, |
||||||
|
# is hardcoded to 80): |
||||||
|
onion_serving_host = 127.0.0.1 |
||||||
|
onion_serving_port = 8080 |
||||||
|
# This is mandatory for directory nodes (who must also set their |
||||||
|
# own .onion:port as the only directory in directory_nodes, below), |
||||||
|
# but NOT TO BE USED by non-directory nodes (which is you, unless |
||||||
|
# you know otherwise!), as it will greatly degrade your privacy. |
||||||
|
# |
||||||
|
# Special handling on regtest, so just ignore and let the code handle it: |
||||||
|
hidden_service_dir = "" |
||||||
|
# This is a comma separated list (comma can be omitted if only one item). |
||||||
|
# Each item has format host:port |
||||||
|
# On regtest we are going to increment the port numbers served from, with |
||||||
|
# the value used here as the starting value: |
||||||
|
directory_nodes = localhost:8081 |
||||||
|
# this is not present in default real config |
||||||
|
# and is specifically used to flag tests: |
||||||
|
# means we use indices 1,2,3,4,5: |
||||||
|
regtest_count=1,5 |
||||||
|
|
||||||
|
[TIMEOUT] |
||||||
|
maker_timeout_sec = 10 |
||||||
|
|
||||||
|
[LOGGING] |
||||||
|
console_log_level = DEBUG |
||||||
|
|
||||||
|
[POLICY] |
||||||
|
|
||||||
|
segwit = true |
||||||
|
native = true |
||||||
|
taproot = true |
||||||
|
|
||||||
|
# for dust sweeping, try merge_algorithm = gradual |
||||||
|
# for more rapid dust sweeping, try merge_algorithm = greedy |
||||||
|
# for most rapid dust sweeping, try merge_algorithm = greediest |
||||||
|
# but don't forget to bump your miner fees! |
||||||
|
merge_algorithm = default |
||||||
|
# the fee estimate is based on a projection of how many satoshis |
||||||
|
# per kB are needed to get in one of the next N blocks, N set here |
||||||
|
# as the value of 'tx_fees'. This estimate can be extremely high |
||||||
|
# if you set N=1, so we choose N=3 for a more reasonable figure, |
||||||
|
# as our default. Note that for clients not using a local blockchain |
||||||
|
# instance, we retrieve an estimate from the API at cointape.com, currently. |
||||||
|
tx_fees = 3 |
||||||
|
taker_utxo_retries = 3 |
||||||
|
taker_utxo_age = 1 |
||||||
|
taker_utxo_amtpercent = 20 |
||||||
|
accept_commitment_broadcasts = 1 |
||||||
|
#some settings useful for testing scenarios |
||||||
|
#laxity for repeated tests; tests on actual |
||||||
|
#commitments/maker limit/utxo sourcing logic should obviously reset |
||||||
|
taker_utxo_retries = 3 |
||||||
|
minimum_makers = 1 |
||||||
|
listunspent_args = [0] |
||||||
|
max_sats_freeze_reuse = -1 |
||||||
|
|
||||||
|
# ONLY for testing! |
||||||
|
max_cj_fee_abs = 200000 |
||||||
|
max_cj_fee_rel = 0.2 |
||||||
|
|
||||||
|
[PAYJOIN] |
||||||
|
# for the majority of situations, the defaults |
||||||
|
# need not be altered - they will ensure you don't pay |
||||||
|
# a significantly higher fee. |
||||||
|
# MODIFICATION OF THESE SETTINGS IS DISADVISED. |
||||||
|
|
||||||
|
# Payjoin protocol version; currently only '1' is supported. |
||||||
|
payjoin_version = 1 |
||||||
|
|
||||||
|
# servers can change their destination address by default (0). |
||||||
|
# if '1', they cannot. Note that servers can explicitly request |
||||||
|
# that this is activated, in which case we respect that choice. |
||||||
|
disable_output_substitution = 0 |
||||||
|
|
||||||
|
# "default" here indicates that we will allow the receiver to |
||||||
|
# increase the fee we pay by: |
||||||
|
# 1.2 * (our_fee_rate_per_vbyte * vsize_of_our_input_type) |
||||||
|
# (see https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#span_idfeeoutputspanFee_output) |
||||||
|
# (and 1.2 to give breathing room) |
||||||
|
# which indicates we are allowing roughly one extra input's fee. |
||||||
|
# If it is instead set to an integer, then that many satoshis are allowed. |
||||||
|
# Additionally, note that we will also set the parameter additionafeeoutputindex |
||||||
|
# to that of our change output, unless there is none in which case this is disabled. |
||||||
|
max_additional_fee_contribution = default |
||||||
|
|
||||||
|
# this is the minimum satoshis per vbyte we allow in the payjoin |
||||||
|
# transaction; note it is decimal, not integer. |
||||||
|
min_fee_rate = 1.1 |
||||||
|
|
||||||
|
# for payjoins to hidden service endpoints, the socks5 configuration: |
||||||
|
onion_socks5_host = localhost |
||||||
|
onion_socks5_port = 9050 |
||||||
|
# in some exceptional case the HS may be SSL configured, |
||||||
|
# this feature is not yet implemented in code, but here for the |
||||||
|
# future: |
||||||
|
hidden_service_ssl = false |
||||||
Loading…
Reference in new issue