You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1031 lines
41 KiB
1031 lines
41 KiB
# -*- coding: utf-8 -*- |
|
|
|
import asyncio |
|
import os |
|
import time |
|
from hashlib import sha256 |
|
|
|
from bitcointx.core.key import XOnlyPubKey |
|
|
|
import jmbitcoin as btc |
|
from jmbase import hextobin, get_log |
|
from jmbitcoin import CCoinKey |
|
from jmclient.configure import jm_single |
|
from jmfrost.chilldkg_ref.chilldkg import ( |
|
params_id, |
|
hostpubkey_gen, |
|
participant_step1, |
|
participant_step2, |
|
participant_finalize, |
|
participant_investigate, |
|
coordinator_step1, |
|
coordinator_finalize, |
|
coordinator_investigate, |
|
SessionParams, |
|
DKGOutput, |
|
RecoveryData, |
|
FaultyParticipantOrCoordinatorError, |
|
UnknownFaultyParticipantOrCoordinatorError, |
|
ParticipantMsg1, |
|
ParticipantMsg2, |
|
CoordinatorMsg1, |
|
CoordinatorMsg2, |
|
) |
|
from jmfrost.chilldkg_ref import encpedpop |
|
from jmfrost.chilldkg_ref import simplpedpop |
|
from jmfrost.chilldkg_ref import vss |
|
from jmfrost.secp256k1lab import secp256k1 |
|
from jmfrost.frost_ref import reference as frost |
|
from jmfrost.frost_ref.utils.bip340 import schnorr_verify |
|
|
|
|
|
jlog = get_log() |
|
|
|
|
|
def calc_tweak(pubshares, ids_bytes, h=b''): |
|
pubkey = frost.derive_group_pubkey(pubshares, ids_bytes) |
|
return frost.tagged_hash("TapTweak", pubkey[1:] + h) |
|
|
|
|
|
def chilldkg_hexlify(data): |
|
if isinstance(data, bytes): |
|
return data.hex() |
|
if isinstance(data, dict): |
|
return {k: chilldkg_hexlify(v) for k, v in data.items()} |
|
if hasattr(data, "_asdict"): # NamedTuple |
|
return chilldkg_hexlify(data._asdict()) |
|
if isinstance(data, list): |
|
return [chilldkg_hexlify(v) for v in data] |
|
return data |
|
|
|
|
|
def decrypt_ext_recovery(privkey, enc_ext_recovery_base64): |
|
return btc.ecies_decrypt(privkey, enc_ext_recovery_base64) |
|
|
|
|
|
def serialize_ext_recovery(mixdepth, address_type, index): |
|
try: |
|
res = b'' |
|
res += mixdepth.to_bytes(1, 'big') |
|
res += address_type.to_bytes(1, 'big') |
|
res += index.to_bytes(4, 'big') |
|
return res |
|
except Exception as e: |
|
jlog.error(f'serialize_ext_recovery: serialization ' |
|
f'failed {repr(e)}') |
|
|
|
|
|
def deserialize_ext_recovery(ext_recovery_bytes): |
|
try: |
|
b = ext_recovery_bytes |
|
i = 0 |
|
mixdepth = int.from_bytes(b[i:i+1], 'big') |
|
i += 1 |
|
address_type = int.from_bytes(b[i:i+1], 'big') |
|
i += 1 |
|
index = int.from_bytes(b[i:i+4], 'big') |
|
i += 4 |
|
assert b[i:] == b'' |
|
return mixdepth, address_type, index |
|
except Exception as e: |
|
jlog.error(f'deserialize_ext_recovery: deserialization ' |
|
f'failed {repr(e)}') |
|
|
|
|
|
class DKGCoordinator: |
|
|
|
def __init__(self, *, mixdepth, address_type, index, |
|
session_id, hostpubkey): |
|
self.mixdepth = mixdepth |
|
self.address_type = address_type |
|
self.index = index |
|
self.session_id = session_id |
|
self.hostpubkey = hostpubkey |
|
self.parties = dict() |
|
self.sessions = dict() |
|
self.state = None |
|
self.cmsg2 = None |
|
self.ext_recovery = None |
|
|
|
|
|
class DKGSession: |
|
|
|
def __init__(self, *, session_id, hostpubkey, |
|
coord_nick, coord_hostpubkey): |
|
self.session_id = session_id |
|
self.hostpubkey = hostpubkey |
|
self.coord_nick = coord_nick |
|
self.coord_hostpubkey = coord_hostpubkey |
|
self.dkg_init_sec = 0 |
|
self.state1 = None |
|
self.state2 = None |
|
self.dkg_output = None |
|
self.recovery_data = None |
|
|
|
|
|
COORDINATOR = 'coordinator' |
|
|
|
|
|
class DKGClient: |
|
|
|
DKG_WAIT_SEC = 60 |
|
|
|
def __init__(self, wallet_service): |
|
self.aborted = False |
|
self.testflag = False |
|
self.offerlist = [] |
|
self.jm_up_loop = None |
|
self.jm_up = False |
|
self.dkg_gen_list = [] |
|
self.current_dkg_gen = None |
|
|
|
self.wallet_service = wallet_service |
|
hostpubkeys = jm_single().config.get('FROST', 'hostpubkeys') |
|
self.hostpubkeys = [hextobin(p) for p in hostpubkeys.split(',')] |
|
self.t = jm_single().config.getint('FROST', 't') |
|
self.session_params = SessionParams(self.hostpubkeys, self.t) |
|
self.dkg_coordinators = dict() |
|
self.dkg_sessions = dict() |
|
|
|
def on_jm_up(self): |
|
self.jm_up = True |
|
|
|
def find_pubkey_by_pubkeyhash(self, pubkeyhash): |
|
for pubkey in self.hostpubkeys: |
|
if pubkeyhash == sha256(pubkey).hexdigest(): |
|
return pubkey |
|
|
|
async def dkg_gen(self): |
|
if self.dkg_gen_list: |
|
self.current_dkg_gen = self.dkg_gen_list[0] |
|
else: |
|
self.current_dkg_gen = None |
|
return self.current_dkg_gen |
|
|
|
def dkg_init(self, mixdepth, address_type, index): |
|
try: |
|
wallet = self.wallet_service.wallet |
|
hostseckey = wallet._hostseckey[:32] |
|
hostpubkey = hostpubkey_gen(hostseckey) |
|
hostpubkeyhash = sha256(hostpubkey).digest() |
|
session_id = sha256(os.urandom(32)).digest() |
|
coordinator = DKGCoordinator(mixdepth=mixdepth, |
|
address_type=address_type, |
|
index=index, |
|
session_id=session_id, |
|
hostpubkey=hostpubkey) |
|
md_type_idx = (coordinator.mixdepth, |
|
coordinator.address_type, |
|
coordinator.index) |
|
ext_recovery_bytes = serialize_ext_recovery(*md_type_idx) |
|
coordinator.ext_recovery = self.encrypt_ext_recovery( |
|
coordinator, ext_recovery_bytes) |
|
self.dkg_coordinators[session_id] = coordinator |
|
session = DKGSession(session_id=session_id, |
|
hostpubkey=hostpubkey, |
|
coord_nick=COORDINATOR, |
|
coord_hostpubkey=hostpubkey) |
|
self.dkg_sessions[session_id] = session |
|
coordinator.parties[hostpubkey] = COORDINATOR |
|
coordinator.sessions[hostpubkey] = {} |
|
pmsg1 = self.party_step1(session_id, serialize=False) |
|
if not pmsg1: |
|
raise Exception(f'Can not create pmsg1 for ' |
|
f'session {session_id.hex()}') |
|
coordinator.sessions[hostpubkey]['nick'] = COORDINATOR |
|
coordinator.sessions[hostpubkey]['pmsg1'] = pmsg1 |
|
coin_key = CCoinKey.from_secret_bytes(hostseckey) |
|
sig = coin_key.sign_schnorr_no_tweak(session_id) |
|
return hostpubkeyhash.hex(), session_id, sig.hex() |
|
except Exception as e: |
|
jlog.error(f'dkg_init: {repr(e)}') |
|
return None, None, None |
|
|
|
def on_dkg_init(self, nick, pubkeyhash, session_id, sig): |
|
try: |
|
if session_id in self.dkg_sessions: |
|
raise Exception(f'session {session_id.hex()} already exists') |
|
pubkey = self.find_pubkey_by_pubkeyhash(pubkeyhash) |
|
if not pubkey: |
|
raise Exception(f'pubkey for {pubkeyhash.hex()} not found') |
|
xpubkey = XOnlyPubKey(pubkey[1:]) |
|
if not xpubkey.verify_schnorr(session_id, hextobin(sig)): |
|
raise Exception('signature verification failed') |
|
wallet = self.wallet_service.wallet |
|
hostseckey = wallet._hostseckey[:32] |
|
hostpubkey = hostpubkey_gen(hostseckey) |
|
hostpubkeyhash = sha256(hostpubkey).digest() |
|
session = DKGSession(session_id=session_id, |
|
hostpubkey=hostpubkey, |
|
coord_nick=nick, |
|
coord_hostpubkey=pubkey) |
|
self.dkg_sessions[session_id] = session |
|
coin_key = CCoinKey.from_secret_bytes(hostseckey) |
|
sig = coin_key.sign_schnorr_no_tweak(session_id) |
|
pmsg1 = self.party_step1(session_id) |
|
return (nick, hostpubkeyhash.hex(), session_id.hex(), |
|
sig.hex(), pmsg1) |
|
except Exception as e: |
|
jlog.error(f'on_dkg_init: {repr(e)}') |
|
return None, None, None, None, None |
|
|
|
def party_step1(self, session_id, *, serialize=True): |
|
try: |
|
session = self.dkg_sessions.get(session_id) |
|
if not session: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
if session.state1: |
|
raise Exception(f'session.state1 already set ' |
|
f'for {session_id.hex()}') |
|
wallet = self.wallet_service.wallet |
|
hostseckey = wallet._hostseckey[:32] |
|
random = os.urandom(32) |
|
session.state1, pmsg1 = participant_step1( |
|
hostseckey, self.session_params, random) |
|
if serialize: |
|
pmsg1 = self.serialize_pmsg1(pmsg1) |
|
jlog.debug('party_step1 run') |
|
return pmsg1 |
|
except Exception as e: |
|
jlog.error(f'party_step1: {repr(e)}') |
|
|
|
def on_dkg_pmsg1(self, nick, pubkeyhash, session_id, sig, pmsg1): |
|
try: |
|
coordinator = self.dkg_coordinators.get(session_id) |
|
if not coordinator: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
pubkey = self.find_pubkey_by_pubkeyhash(pubkeyhash) |
|
if not pubkey: |
|
raise Exception(f'pubkey for {pubkeyhash.hex()} not found') |
|
xpubkey = XOnlyPubKey(pubkey[1:]) |
|
if not xpubkey.verify_schnorr(session_id, hextobin(sig)): |
|
raise Exception(f'signature verification failed') |
|
if pubkey in coordinator.parties: |
|
jlog.debug(f'pubkey {pubkey.hex()} already in' |
|
f' coordinator parties') |
|
return None, None |
|
coordinator.parties[pubkey] = nick |
|
|
|
if not pubkey in coordinator.sessions: |
|
coordinator.sessions[pubkey] = {} |
|
coordinator.sessions[pubkey]['nick'] = nick |
|
coordinator.sessions[pubkey]['pmsg1'] = pmsg1 |
|
|
|
ready_list = set() |
|
if len(coordinator.sessions) == len(self.hostpubkeys): |
|
for session in coordinator.sessions.values(): |
|
if session['nick'] == COORDINATOR: |
|
continue |
|
ready_list.add(session['nick']) |
|
if ready_list and len(ready_list) == len(self.hostpubkeys) - 1: |
|
cmsg1 = self.coordinator_step1(session_id) |
|
pmsg2 = self.party_step2(session_id, cmsg1, serialize=False) |
|
self.on_dkg_pmsg2(COORDINATOR, session_id, pmsg2) |
|
return ready_list, self.serialize_cmsg1(cmsg1) |
|
else: |
|
return None, None |
|
except Exception as e: |
|
jlog.error(f'on_dkg_pmsg1: {repr(e)}') |
|
return None, None |
|
|
|
def coordinator_step1(self, session_id): |
|
try: |
|
coordinator = self.dkg_coordinators.get(session_id) |
|
if not coordinator: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
if coordinator.state: |
|
raise Exception(f'coordinator.state already set ' |
|
f'for {session_id.hex()}') |
|
pmsgs1 = [] |
|
for pubkey in self.hostpubkeys: |
|
session = coordinator.sessions[pubkey] |
|
pmsgs1.append(session['pmsg1']) |
|
|
|
coordinator.state, cmsg1 = coordinator_step1( |
|
pmsgs1, self.session_params) |
|
jlog.debug('coordinator_step1 run') |
|
return cmsg1 |
|
except Exception as e: |
|
jlog.error(f'coordinator_step1: {repr(e)}') |
|
|
|
def party_step2(self, session_id, cmsg1, *, serialize=True): |
|
try: |
|
session = self.dkg_sessions.get(session_id) |
|
if not session: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
if session.state2: |
|
raise Exception(f'session.state2 already set ' |
|
f'for {session_id.hex()}') |
|
wallet = self.wallet_service.wallet |
|
hostseckey = wallet._hostseckey[:32] |
|
session.state2, pmsg2 = participant_step2( |
|
hostseckey, session.state1, cmsg1) |
|
if serialize: |
|
pmsg2 = self.serialize_pmsg2(pmsg2) |
|
jlog.debug('party_step2 run') |
|
return pmsg2 |
|
except Exception as e: |
|
jlog.error(f'party_step2: {repr(e)}') |
|
|
|
def on_dkg_pmsg2(self, nick, session_id, pmsg2): |
|
try: |
|
coordinator = self.dkg_coordinators.get(session_id) |
|
if not coordinator: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
party = None |
|
for pubkey in self.hostpubkeys: |
|
if nick == coordinator.parties.get(pubkey): |
|
party = nick |
|
break |
|
if not party: |
|
raise Exception(f'unknown party {nick}') |
|
if not pubkey in coordinator.sessions: |
|
raise Exception(f'party pubkey for {nick} not found') |
|
if 'pmsg2' in coordinator.sessions[pubkey]: |
|
raise Exception(f'pmsg2 already set in coordinator sessions ' |
|
f'for pubkey {pubkey.hex()}') |
|
coordinator.sessions[pubkey]['pmsg2'] = pmsg2 |
|
|
|
ready_list = set() |
|
if len(coordinator.sessions) == len(self.hostpubkeys): |
|
for session in coordinator.sessions.values(): |
|
if session['nick'] == COORDINATOR: |
|
continue |
|
if not 'pmsg2' in session: |
|
continue |
|
ready_list.add(session['nick']) |
|
if ready_list and len(ready_list) == len(self.hostpubkeys) - 1: |
|
cmsg2 = self.coordinator_step2(session_id) |
|
ext_recovery = coordinator.ext_recovery |
|
return ready_list, self.serialize_cmsg2(cmsg2), ext_recovery |
|
else: |
|
return None, None, None |
|
except Exception as e: |
|
jlog.error(f'on_dkg_pmsg2: {repr(e)}') |
|
return None, None, None |
|
|
|
def coordinator_step2(self, session_id): |
|
try: |
|
coordinator = self.dkg_coordinators.get(session_id) |
|
if not coordinator: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
if coordinator.cmsg2: |
|
raise Exception(f'coordinator.cmsg2 already set ' |
|
f'for {session_id.hex()}') |
|
pmsgs2 = [] |
|
for pubkey in self.hostpubkeys: |
|
session = coordinator.sessions[pubkey] |
|
pmsgs2.append(session['pmsg2']) |
|
cmsg2, dkg_output, recovery_data = coordinator_finalize( |
|
coordinator.state, pmsgs2) |
|
coordinator.cmsg2 = cmsg2 |
|
jlog.debug('coordinator_step2 run') |
|
return cmsg2 |
|
except Exception as e: |
|
jlog.error(f'coordinator_step2 : {repr(e)}') |
|
|
|
def finalize(self, session_id, cmsg2, ext_recovery): |
|
try: |
|
session = self.dkg_sessions.get(session_id) |
|
if not session: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
if session.dkg_output: |
|
raise Exception(f'session.dkg_output already set ' |
|
f'for {session_id.hex()}') |
|
session.dkg_output, session.recovery_data = participant_finalize( |
|
session.state2, cmsg2) |
|
jlog.debug('finalize run') |
|
dkg_man = self.wallet_service.dkg |
|
session_id = session.session_id |
|
coordinator = self.dkg_coordinators.get(session_id) |
|
coord_hostpubkey = session.coord_hostpubkey |
|
if coordinator: |
|
dkg_man.add_coordinator_data( |
|
session_id=session_id, |
|
dkg_output=session.dkg_output, |
|
hostpubkeys=self.hostpubkeys, |
|
t=self.t, |
|
recovery_data=session.recovery_data, |
|
ext_recovery=ext_recovery) |
|
else: |
|
dkg_man.add_party_data( |
|
session_id=session_id, |
|
dkg_output=session.dkg_output, |
|
hostpubkeys=self.hostpubkeys, |
|
t=self.t, |
|
recovery_data=session.recovery_data, |
|
ext_recovery=ext_recovery) |
|
self.dkg_sessions.pop(session_id) |
|
return True |
|
except Exception as e: |
|
jlog.error(f'finalize: {repr(e)}') |
|
return False |
|
|
|
def on_dkg_finalized(self, nick, session_id): |
|
try: |
|
coordinator = self.dkg_coordinators.get(session_id) |
|
if not coordinator: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
party = None |
|
for pubkey in self.hostpubkeys: |
|
if nick == coordinator.parties.get(pubkey): |
|
party = nick |
|
break |
|
if not party: |
|
raise Exception(f'unknown party {nick}') |
|
if not pubkey in coordinator.sessions: |
|
raise Exception(f'party pubkey for {nick} not found') |
|
if 'finalized' in coordinator.sessions[pubkey]: |
|
raise Exception(f'finalized already set in coordinator ' |
|
f'sessions for pubkey {pubkey.hex()}') |
|
coordinator.sessions[pubkey]['finalized'] = True |
|
|
|
ready_list = set() |
|
if len(coordinator.sessions) == len(self.hostpubkeys): |
|
for session in coordinator.sessions.values(): |
|
if session['nick'] == COORDINATOR: |
|
continue |
|
if not 'finalized' in session: |
|
continue |
|
ready_list.add(session['nick']) |
|
if ready_list and len(ready_list) == len(self.hostpubkeys) - 1: |
|
ext_recovery = coordinator.ext_recovery |
|
self.finalize(session_id, coordinator.cmsg2, ext_recovery) |
|
return True |
|
return False |
|
except Exception as e: |
|
jlog.error(f'on_dkg_finalized: {repr(e)}') |
|
return False |
|
|
|
async def wait_on_dkg_output(self, session_id): |
|
try: |
|
session = self.dkg_sessions.get(session_id) |
|
if not session: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
while True: |
|
await asyncio.sleep(1) |
|
if session.dkg_output: |
|
break |
|
waiting_sec = time.time() - session.dkg_init_sec |
|
if waiting_sec > self.DKG_WAIT_SEC: |
|
raise Exception(f'timed out DKG session ' |
|
f'{session_id.hex()}') |
|
return session.dkg_output.threshold_pubkey |
|
except Exception as e: |
|
jlog.warn(f'wait_on_dkg_output: {repr(e)}') |
|
finally: |
|
sess_id = self.dkg_sessions.pop(session_id, None) |
|
if not sess_id: |
|
jlog.debug(f'wait_on_dkg_output: session {session_id.hex()}' |
|
f' not found in the dkg_sessions') |
|
sess_id = self.dkg_coordinators.pop(session_id, None) |
|
if not sess_id: |
|
jlog.debug(f'wait_on_dkg_output: session {session_id.hex()}' |
|
f' not found in the dkg_coordinators') |
|
|
|
def encrypt_ext_recovery(self, coordinator, ext_recovery_bytes): |
|
try: |
|
pubkey = coordinator.hostpubkey |
|
return btc.ecies_encrypt(ext_recovery_bytes, pubkey) |
|
except Exception as e: |
|
jlog.error(f'enc_ext_recovery: {repr(e)}') |
|
|
|
def serialize_pmsg1(self, pmsg1): |
|
try: |
|
enc_pmsg = pmsg1.enc_pmsg |
|
simpl_pmsg = enc_pmsg.simpl_pmsg |
|
com = simpl_pmsg.com |
|
pop = simpl_pmsg.pop |
|
ges = com.ges |
|
pubnonce = enc_pmsg.pubnonce |
|
enc_shares = enc_pmsg.enc_shares |
|
|
|
res = b'' |
|
res += len(ges).to_bytes(2, 'big') |
|
for ge in ges: |
|
res += ge.to_bytes_compressed() |
|
res += bytes(pop) |
|
res += pubnonce |
|
res += len(enc_shares).to_bytes(2, 'big') |
|
for es in enc_shares: |
|
res += es.to_bytes() |
|
return res |
|
except Exception as e: |
|
jlog.error(f'serialize_pmsg1: serialization failed {repr(e)}') |
|
|
|
def deserialize_pmsg1(self, pmsg1_bytes): |
|
try: |
|
b = pmsg1_bytes |
|
i = 0 |
|
|
|
ges_len = int.from_bytes(b[i:i+2], 'big') |
|
i += 2 |
|
|
|
ges = [] |
|
for j in range(ges_len): |
|
ge = secp256k1.GE.from_bytes_compressed(b[i:i+33]) |
|
ges.append(ge) |
|
i += 33 |
|
|
|
pop = simplpedpop.Pop(b[i:i+64]) |
|
i += 64 |
|
|
|
pubnonce = b[i:i+33] |
|
i += 33 |
|
|
|
enc_shares_len = int.from_bytes(b[i:i+2], 'big') |
|
i += 2 |
|
|
|
enc_shares = [] |
|
for j in range(enc_shares_len): |
|
es = secp256k1.Scalar.from_bytes_checked(b[i:i+32]) |
|
enc_shares.append(es) |
|
i += 32 |
|
|
|
assert b[i:] == b'' |
|
|
|
com = vss.VSSCommitment(ges) |
|
simpl_pmsg = simplpedpop.ParticipantMsg(com, pop) |
|
enc_pmsg = encpedpop.ParticipantMsg(simpl_pmsg, pubnonce, |
|
enc_shares) |
|
return ParticipantMsg1(enc_pmsg) |
|
except Exception as e: |
|
jlog.error(f'deserialize_pmsg1: deserialization failed {repr(e)}') |
|
|
|
def serialize_pmsg2(self, pmsg2): |
|
try: |
|
return b'' + pmsg2.sig |
|
except Exception as e: |
|
jlog.error(f'serialize_pmsg2: serialization failed {repr(e)}') |
|
|
|
def deserialize_pmsg2(self, pmsg2_bytes): |
|
try: |
|
return ParticipantMsg2(pmsg2_bytes) |
|
except Exception as e: |
|
jlog.error(f'deserialize_pmsg2: deserialization failed {repr(e)}') |
|
|
|
def serialize_cmsg1(self, cmsg1): |
|
try: |
|
enc_cmsg = cmsg1.enc_cmsg |
|
simpl_cmsg = enc_cmsg.simpl_cmsg |
|
coms_to_secrets = simpl_cmsg.coms_to_secrets |
|
sum_coms_to_nonconst_terms = simpl_cmsg.sum_coms_to_nonconst_terms |
|
pops = simpl_cmsg.pops |
|
pubnonces = enc_cmsg.pubnonces |
|
enc_secshares = cmsg1.enc_secshares |
|
|
|
res = b'' |
|
res += len(coms_to_secrets).to_bytes(2, 'big') |
|
for cts in coms_to_secrets: |
|
res += cts.to_bytes_compressed() |
|
res += len(sum_coms_to_nonconst_terms).to_bytes(2, 'big') |
|
for sctnct in sum_coms_to_nonconst_terms: |
|
res += sctnct.to_bytes_compressed() |
|
res += len(pops).to_bytes(2, 'big') |
|
for pop in pops: |
|
res += bytes(pop) |
|
res += len(pubnonces).to_bytes(2, 'big') |
|
for pubnonce in pubnonces: |
|
res += pubnonce |
|
res += len(enc_secshares).to_bytes(2, 'big') |
|
for es in enc_secshares: |
|
res += es.to_bytes() |
|
return res |
|
except Exception as e: |
|
jlog.error(f'serialize_cmsg1: serialization failed {repr(e)}') |
|
|
|
def deserialize_cmsg1(self, cmsg1_bytes): |
|
try: |
|
b = cmsg1_bytes |
|
i = 0 |
|
|
|
coms_to_secrets_len = int.from_bytes(b[i:i+2], 'big') |
|
i += 2 |
|
coms_to_secrets = [] |
|
for j in range(coms_to_secrets_len): |
|
cts = secp256k1.GE.from_bytes_compressed(b[i:i+33]) |
|
coms_to_secrets.append(cts) |
|
i += 33 |
|
|
|
sum_coms_to_nonconst_terms_len = int.from_bytes(b[i:i+2], 'big') |
|
i += 2 |
|
sum_coms_to_nonconst_terms = [] |
|
for j in range(sum_coms_to_nonconst_terms_len): |
|
sctnct = secp256k1.GE.from_bytes_compressed(b[i:i+33]) |
|
sum_coms_to_nonconst_terms.append(sctnct) |
|
i += 33 |
|
|
|
pops_len = int.from_bytes(b[i:i+2], 'big') |
|
i += 2 |
|
pops = [] |
|
for j in range(pops_len): |
|
pop = simplpedpop.Pop(b[i:i+64]) |
|
pops.append(pop) |
|
i += 64 |
|
|
|
pubnonces_len = int.from_bytes(b[i:i+2], 'big') |
|
i += 2 |
|
pubnonces = [] |
|
for j in range(pubnonces_len): |
|
pubnonce = b[i:i+33] |
|
pubnonces.append(pubnonce) |
|
i += 33 |
|
|
|
enc_secshares_len = int.from_bytes(b[i:i+2], 'big') |
|
i += 2 |
|
enc_secshares = [] |
|
for j in range(enc_secshares_len): |
|
es = secp256k1.Scalar.from_bytes_checked(b[i:i+32]) |
|
enc_secshares.append(es) |
|
i += 32 |
|
|
|
assert b[i:] == b'' |
|
|
|
simpl_cmsg = simplpedpop.CoordinatorMsg( |
|
coms_to_secrets, sum_coms_to_nonconst_terms, pops) |
|
enc_cmsg = encpedpop.CoordinatorMsg(simpl_cmsg, pubnonces) |
|
return CoordinatorMsg1(enc_cmsg, enc_secshares) |
|
except Exception as e: |
|
jlog.error(f'deserialize_cmsg1: deserialization failed {repr(e)}') |
|
|
|
def serialize_cmsg2(self, cmsg2): |
|
try: |
|
return b'' + cmsg2.cert |
|
except Exception as e: |
|
jlog.error(f'serialize_cmsg2: serialization failed {repr(e)}') |
|
|
|
def deserialize_cmsg2(self, cmsg2_bytes): |
|
try: |
|
return CoordinatorMsg2(cmsg2_bytes) |
|
except Exception as e: |
|
jlog.error(f'deserialize_cmsg2: deserialization failed {repr(e)}') |
|
|
|
|
|
class FROSTCoordinator: |
|
|
|
def __init__(self, *, session_id, hostpubkey, dkg_session_id, msg): |
|
self.session_id = session_id |
|
self.frost_init_sec = 0 |
|
self.hostpubkey = hostpubkey |
|
self.dkg_session_id = dkg_session_id |
|
self.msg = msg |
|
self.parties = dict() |
|
self.sessions = dict() |
|
self.nonce_agg = None |
|
self.ids = [] |
|
self.sig = None |
|
self.tweaked_pubkey = None |
|
|
|
def __str__(self): |
|
return self.__repr__() |
|
|
|
def __repr__(self): |
|
return (f'FROSTCoordinator(session_id={self.session_id}, ' |
|
f'frost_init_sec={self.frost_init_sec}, ' |
|
f'hostpubkey={self.hostpubkey}, ' |
|
f'dkg_session_id={self.dkg_session_id}, ' |
|
f'msg={self.msg}, ' |
|
f'parties={self.parties}, ' |
|
f'sessions={self.sessions}, ' |
|
f'nonce_agg={self.nonce_agg}, ' |
|
f'ids={self.ids}, ' |
|
f'sig={self.sig})') |
|
|
|
|
|
class FROSTSession: |
|
|
|
def __init__(self, *, session_id, hostpubkey, |
|
coord_nick, coord_hostpubkey): |
|
self.session_id = session_id |
|
self.hostpubkey = hostpubkey |
|
self.coord_nick = coord_nick |
|
self.coord_hostpubkey = coord_hostpubkey |
|
self.sec_nonce = None |
|
self.pub_nonce = None |
|
self.partial_sig = None |
|
|
|
def __str__(self): |
|
return self.__repr__() |
|
|
|
def __repr__(self): |
|
return (f'FROSTSession(session_id={self.session_id}, ' |
|
f'hostpubkey={self.hostpubkey}, ' |
|
f'coord_nick={self.coord_nick}, ' |
|
f'coord_hostpubkey={self.coord_hostpubkey}, ' |
|
f'sec_nonce={self.sec_nonce}, ' |
|
f'pub_nonce={self.pub_nonce}, ' |
|
f'partial_sig={self.partial_sig})') |
|
|
|
|
|
class FROSTClient(DKGClient): |
|
|
|
FROST_WAIT_SEC = 60 |
|
|
|
def __init__(self, wallet_service): |
|
super().__init__(wallet_service) |
|
self.frost_coordinators = dict() |
|
self.frost_sessions = dict() |
|
|
|
def frost_init(self, dkg_session_id, msg_bytes): |
|
try: |
|
wallet = self.wallet_service.wallet |
|
hostseckey = wallet._hostseckey[:32] |
|
hostpubkey = hostpubkey_gen(hostseckey) |
|
self.my_id = None |
|
for i, p in enumerate(self.hostpubkeys): |
|
if p == hostpubkey: |
|
self.my_id = (i+1).to_bytes(32, 'big') |
|
break |
|
assert self.my_id is not None, (f'unknown hostpubkey ' |
|
f'{hostpubkey.hex()}') |
|
hostpubkeyhash = sha256(hostpubkey).digest() |
|
session_id = sha256(os.urandom(32)).digest() |
|
coordinator = FROSTCoordinator(session_id=session_id, |
|
hostpubkey=hostpubkey, |
|
dkg_session_id=dkg_session_id, |
|
msg=msg_bytes) |
|
self.frost_coordinators[session_id] = coordinator |
|
session = FROSTSession(session_id=session_id, |
|
hostpubkey=hostpubkey, |
|
coord_nick=COORDINATOR, |
|
coord_hostpubkey=hostpubkey) |
|
self.frost_sessions[session_id] = session |
|
coordinator.parties[hostpubkey] = COORDINATOR |
|
coordinator.sessions[hostpubkey] = {} |
|
coordinator.sessions[hostpubkey]['nick'] = COORDINATOR |
|
pub_nonce = self.frost_round1(session_id) |
|
if not pub_nonce: |
|
raise Exception(f'Can not create pub_nonce for ' |
|
f'session {session_id.hex()}') |
|
coordinator.sessions[hostpubkey]['pub_nonce'] = pub_nonce |
|
coin_key = CCoinKey.from_secret_bytes(hostseckey) |
|
sig = coin_key.sign_schnorr_no_tweak(session_id) |
|
return hostpubkeyhash.hex(), session_id, sig.hex() |
|
except Exception as e: |
|
jlog.error(f'frost_init: {repr(e)}') |
|
return None, None, None |
|
|
|
def on_frost_init(self, nick, pubkeyhash, session_id, sig): |
|
try: |
|
if session_id in self.frost_sessions: |
|
raise Exception(f'session {session_id.hex()} already exists') |
|
pubkey = self.find_pubkey_by_pubkeyhash(pubkeyhash) |
|
if not pubkey: |
|
raise Exception(f'pubkey for {pubkeyhash.hex()} not found') |
|
xpubkey = XOnlyPubKey(pubkey[1:]) |
|
if not xpubkey.verify_schnorr(session_id, hextobin(sig)): |
|
raise Exception('signature verification failed') |
|
wallet = self.wallet_service.wallet |
|
hostseckey = wallet._hostseckey[:32] |
|
hostpubkey = hostpubkey_gen(hostseckey) |
|
self.my_id = None |
|
for i, p in enumerate(self.hostpubkeys): |
|
if p == hostpubkey: |
|
self.my_id = (i+1).to_bytes(32, 'big') |
|
break |
|
assert self.my_id is not None |
|
hostpubkeyhash = sha256(hostpubkey).digest() |
|
session = FROSTSession(session_id=session_id, |
|
hostpubkey=hostpubkey, |
|
coord_nick=nick, |
|
coord_hostpubkey=pubkey) |
|
self.frost_sessions[session_id] = session |
|
coin_key = CCoinKey.from_secret_bytes(hostseckey) |
|
sig = coin_key.sign_schnorr_no_tweak(session_id) |
|
pub_nonce = self.frost_round1(session_id) |
|
return (nick, hostpubkeyhash.hex(), session_id.hex(), |
|
sig.hex(), pub_nonce) |
|
except Exception as e: |
|
jlog.error(f'on_frost_init: {repr(e)}') |
|
return None, None, None, None, None |
|
|
|
def frost_round1(self, session_id): |
|
try: |
|
session = self.frost_sessions.get(session_id) |
|
if not session: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
if session.sec_nonce: |
|
raise Exception(f'session.sec_nonce already set ' |
|
f'for {session_id.hex()}') |
|
session.sec_nonce, session.pub_nonce = frost.nonce_gen( |
|
secshare=None, pubshare=None, group_pk=None, msg=None, |
|
extra_in=None) |
|
jlog.debug('frost_round1 run') |
|
return session.pub_nonce |
|
except Exception as e: |
|
jlog.error(f'frost_round1: {repr(e)}') |
|
|
|
def on_frost_round1(self, nick, pubkeyhash, session_id, sig, pub_nonce): |
|
try: |
|
coordinator = self.frost_coordinators.get(session_id) |
|
if not coordinator: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
if len(coordinator.sessions) == self.t: |
|
jlog.debug('on_frost_round1: miminum pub_nonce set already ' |
|
'presented, ignoring additional pub_nonce') |
|
return None, None, None, None, None |
|
pubkey = self.find_pubkey_by_pubkeyhash(pubkeyhash) |
|
if not pubkey: |
|
raise Exception(f'pubkey for {pubkeyhash} not found') |
|
xpubkey = XOnlyPubKey(pubkey[1:]) |
|
if not xpubkey.verify_schnorr(session_id, hextobin(sig)): |
|
raise Exception(f'signature verification failed') |
|
if pubkey in coordinator.parties: |
|
jlog.debug(f'pubkey {pubkey.hex()} already in' |
|
f' coordinator parties') |
|
return None, None, None, None, None |
|
coordinator.parties[pubkey] = nick |
|
|
|
if not pubkey in coordinator.sessions: |
|
coordinator.sessions[pubkey] = {} |
|
coordinator.sessions[pubkey]['nick'] = nick |
|
coordinator.sessions[pubkey]['pub_nonce'] = pub_nonce |
|
|
|
ready_list = set() |
|
if len(coordinator.sessions) == self.t: |
|
for session in coordinator.sessions.values(): |
|
if session['nick'] == COORDINATOR: |
|
continue |
|
ready_list.add(session['nick']) |
|
if ready_list and len(ready_list) == self.t - 1: |
|
coordinator.nonce_agg, dkg_session_id, ids, msg = \ |
|
self.frost_agg1(session_id) |
|
partial_sig = self.frost_round2( |
|
session_id, coordinator.nonce_agg, |
|
dkg_session_id, ids, msg) |
|
self.on_frost_round2( |
|
COORDINATOR, session_id, partial_sig) |
|
return (ready_list, coordinator.nonce_agg, |
|
dkg_session_id, ids, msg) |
|
else: |
|
return None, None, None, None, None |
|
except Exception as e: |
|
jlog.error(f'on_frost_round1: {repr(e)}') |
|
return None, None, None, None, None |
|
|
|
def frost_agg1(self, session_id): |
|
try: |
|
coordinator = self.frost_coordinators.get(session_id) |
|
if not coordinator: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
if coordinator.nonce_agg: |
|
raise Exception(f'coordinator.nonce_agg already set ' |
|
f'for {session_id.hex()}') |
|
pub_nonces = [] |
|
ids = [] |
|
for i, pubkey in enumerate(self.hostpubkeys): |
|
session = coordinator.sessions.get(pubkey) |
|
if not session: |
|
continue |
|
pub_nonce = session.get('pub_nonce') |
|
if not pub_nonce: |
|
continue |
|
pub_nonces.append(pub_nonce) |
|
ids.append(i+1) |
|
coordinator.ids = ids.copy() |
|
ids_bytes = [] |
|
for i in ids: |
|
ids_bytes.append(i.to_bytes(32, 'big')) |
|
assert len(ids) == self.t |
|
coordinator.nonce_agg = frost.nonce_agg(pub_nonces, ids_bytes) |
|
jlog.debug('frost_agg1 run') |
|
return (coordinator.nonce_agg, coordinator.dkg_session_id, ids, |
|
coordinator.msg) |
|
except Exception as e: |
|
jlog.error(f'frost_agg1: {repr(e)}') |
|
return None, None, None, None |
|
|
|
def frost_round2(self, session_id, nonce_agg, dkg_session_id, ids, msg): |
|
try: |
|
session = self.frost_sessions.get(session_id) |
|
if not session: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
if session.partial_sig: |
|
raise Exception(f'session.partial_sig already set ' |
|
f'for {session_id.hex()}') |
|
dkg = self.wallet_service.wallet.dkg |
|
secshare = dkg._dkg_secshare.get(dkg_session_id) |
|
if not secshare: |
|
raise Exception(f'secshare not found for ' |
|
f'{dkg_session_id.hex()}') |
|
_pubshares = dkg._dkg_pubshares.get(dkg_session_id) |
|
pubshares = [] |
|
for i, pubshare in enumerate(_pubshares): |
|
if (i+1) not in ids: |
|
continue |
|
pubshares.append(pubshare) |
|
ids_bytes = [] |
|
for i in ids: |
|
ids_bytes.append(i.to_bytes(32, 'big')) |
|
|
|
tweak = calc_tweak(pubshares, ids_bytes) |
|
tweaks = [tweak] |
|
is_xonly = [True] |
|
session_ctx = frost.SessionContext( |
|
nonce_agg, ids_bytes, pubshares, tweaks, is_xonly, msg) |
|
session.partial_sig = partial_sig = frost.sign( |
|
session.sec_nonce, secshare, self.my_id, session_ctx) |
|
jlog.debug('frost_round2 run') |
|
return partial_sig |
|
except Exception as e: |
|
jlog.error(f'frost_round2: {repr(e)}') |
|
|
|
def on_frost_round2(self, nick, session_id, partial_sig): |
|
try: |
|
coordinator = self.frost_coordinators.get(session_id) |
|
if not coordinator: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
party = None |
|
for pubkey in self.hostpubkeys: |
|
if nick == coordinator.parties.get(pubkey): |
|
party = nick |
|
break |
|
if not party: |
|
raise Exception(f'unknown party {nick}') |
|
if not pubkey in coordinator.sessions: |
|
raise Exception(f'party pubkey for {nick} not found') |
|
if 'partial_sig' in coordinator.sessions[pubkey]: |
|
raise Exception(f'partial_sig already set in coordinator ' |
|
f'sessions for pubkey {pubkey.hex()}') |
|
coordinator.sessions[pubkey]['partial_sig'] = partial_sig |
|
|
|
ready_list = set() |
|
if len(coordinator.sessions) == self.t: |
|
for session in coordinator.sessions.values(): |
|
if session['nick'] == COORDINATOR: |
|
continue |
|
if not 'partial_sig' in session: |
|
continue |
|
ready_list.add(session['nick']) |
|
if ready_list and len(ready_list) == self.t - 1: |
|
dkg_session_id = coordinator.dkg_session_id |
|
dkg = self.wallet_service.wallet.dkg |
|
_pubshares = dkg._dkg_pubshares.get(dkg_session_id) |
|
if not _pubshares: |
|
raise Exception(f'pubshares not found for ' |
|
f'{dkg_session_id.hex()}') |
|
ids = coordinator.ids |
|
ids_bytes = [] |
|
pubshares = [] |
|
for i, pubshare in enumerate(_pubshares): |
|
if (i+1) not in ids: |
|
continue |
|
pubshares.append(pubshare) |
|
ids_bytes.append((i+1).to_bytes(32, 'big')) |
|
tweak = calc_tweak(pubshares, ids_bytes) |
|
tweaks = [tweak] |
|
is_xonly = [True] |
|
session_ctx = frost.SessionContext( |
|
coordinator.nonce_agg, ids_bytes, pubshares, tweaks, |
|
is_xonly, coordinator.msg) |
|
partial_sigs = [] |
|
for pubkey in self.hostpubkeys: |
|
session = coordinator.sessions.get(pubkey) |
|
if not session: |
|
continue |
|
if 'partial_sig' in session: |
|
partial_sigs.append(session['partial_sig']) |
|
sig = frost.partial_sig_agg( |
|
partial_sigs, ids_bytes, session_ctx) |
|
tweak_ctx = frost.group_pubkey_and_tweak( |
|
pubshares, ids_bytes, tweaks, is_xonly) |
|
Q , _, _ = tweak_ctx |
|
tweaked_pubkey = frost.xbytes(Q) |
|
if not schnorr_verify(coordinator.msg, tweaked_pubkey, sig): |
|
raise Exception(f'on_frost_round2: schnorr_verify failed ' |
|
f'for {dkg_session_id.hex()}') |
|
coordinator.sig = sig |
|
coordinator.tweaked_pubkey = frost.cbytes(Q) |
|
return sig |
|
else: |
|
return None |
|
except Exception as e: |
|
jlog.error(f'on_frost_round2: {repr(e)}') |
|
return None |
|
|
|
async def wait_on_sig(self, session_id): |
|
try: |
|
coordinator = self.frost_coordinators.get(session_id) |
|
if not coordinator: |
|
raise Exception(f'session {session_id.hex()} not found') |
|
while True: |
|
await asyncio.sleep(1) |
|
if coordinator.sig: |
|
break |
|
waiting_sec = time.time() - coordinator.frost_init_sec |
|
if waiting_sec > self.FROST_WAIT_SEC: |
|
raise Exception(f'timed out FROST session ' |
|
f'{session_id.hex()}') |
|
return coordinator.sig, coordinator.tweaked_pubkey |
|
except Exception as e: |
|
jlog.error(f'wait_on_sig: {repr(e)}') |
|
return None, repr(e) |
|
finally: |
|
sess_id = self.frost_sessions.pop(session_id, None) |
|
if not sess_id: |
|
jlog.debug(f'wait_on_sig: session {session_id.hex()} not found' |
|
f' in the frost_sessions') |
|
sess_id = self.frost_coordinators.pop(session_id, None) |
|
if not sess_id: |
|
jlog.debug(f'wait_on_sig: session {session_id.hex()} not found' |
|
f' in the frost_coordinators')
|
|
|