diff --git a/src/coordinator/coordinator.py b/src/coordinator/coordinator.py index 9c4a5c5..c674143 100644 --- a/src/coordinator/coordinator.py +++ b/src/coordinator/coordinator.py @@ -119,7 +119,8 @@ def run(): 'r_agg': result[0], 'sig_hash': result[1], 'negated': result[2], - 'spend_request_id': json_payload['payload']['spend_request_id'] + 'spend_request_id': json_payload['payload']['spend_request_id'], + 'address_index': result[3], } elif command == "sign": if (result != None): diff --git a/src/coordinator/db.py b/src/coordinator/db.py index f05e837..8fdcfb0 100644 --- a/src/coordinator/db.py +++ b/src/coordinator/db.py @@ -83,14 +83,14 @@ def add_xpub(self, wallet_id, xpub): return True def add_spend_request(self, txid, output_index, prev_script_pubkey, prev_value_sats, spend_request_id, new_address, - value, wallet_id): + value, wallet_id, address_index): with open(self.json_file, "r") as f: data = json.load(f) filtered = [spend for spend in data['spends'] if spend['spend_request_id'] == spend_request_id] if len(filtered) > 0: return False data['spends'].append({'spend_request_id': spend_request_id, 'txid': txid, 'output_index': output_index, 'prev_script_pubkey': prev_script_pubkey, - 'prev_value_sats': prev_value_sats, 'new_address': new_address, 'value': value, 'wallet_id': wallet_id}) + 'prev_value_sats': prev_value_sats, 'new_address': new_address, 'value': value, 'wallet_id': wallet_id, 'address_index': address_index}) with open(self.json_file, "w") as f: json.dump(data, f) diff --git a/src/coordinator/wallet.py b/src/coordinator/wallet.py index a64419f..41946a1 100644 --- a/src/coordinator/wallet.py +++ b/src/coordinator/wallet.py @@ -45,7 +45,6 @@ def create_spending_transaction(txid, outputIndex, destination_addr, amount_sat, return (spending_tx, script_pubkey) - def create_wallet(payload: dict, db): if (not 'quorum' in payload): raise Exception("[wallet] Cannot create a wallet without the 'quorum' property") @@ -91,7 +90,6 @@ def get_address(payload: dict, db): wallet_id = get_wallet_id(payload) ec_public_keys = [] - # wallet = db.get_wallet(wallet_id) wallet_xpubs = db.get_xpubs(wallet_id) if (wallet_xpubs == []): @@ -100,17 +98,14 @@ def get_address(payload: dict, db): for xpub in wallet_xpubs: # The method to generate and aggregate MuSig key expects ECPubKey objects ec_public_key = ECPubKey() + bip32_node = BIP32.from_xpub(xpub['xpub']) + public_key = bip32_node.get_pubkey_from_path(f"m/{index}") + ec_public_key.set(public_key) - # TODO xpubs aren't working quite right. Using regular public keys for now. - # bip32_node = BIP32.from_xpub(xpub['xpub']) - # public_key = bip32_node.get_pubkey_from_path(f"m/{index}") - #e c_public_key.set(public_key) - - ec_public_key.set(bytes.fromhex(xpub['xpub'])) ec_public_keys.append(ec_public_key) c_map, pubkey_agg = generate_musig_key(ec_public_keys) - logging.info('[wallet] Aggregate public key: %s', pubkey_agg.get_bytes().hex()) + logging.info('[wallet] Aggregate public key: %s at index: %i', pubkey_agg.get_bytes().hex(), index) # Create a segwit v1 address (P2TR) from the aggregate key p2tr_address = program_to_witness(0x01, pubkey_agg.get_bytes()) @@ -130,8 +125,6 @@ def start_spend(payload: dict, db): # create an ID for this request spend_request_id = str(uuid.uuid4()) logging.info('[wallet] Starting spend request with id %s', spend_request_id) - - if (not 'txid' in payload): raise Exception("[wallet] Cannot spend without the 'txid' property, which corresponds to the transaction ID of the output that is being spent") @@ -144,9 +137,14 @@ def start_spend(payload: dict, db): if (not 'value' in payload): raise Exception("[wallet] Cannot spend without the 'value' property, which corresponds to the value (in satoshis) of the output that is being spent") + if (not 'address_index' in payload): + raise Exception("[wallet] Cannot spend without the 'address_index' property, which corresponds to which utxo from which address is being spent") + + txid = payload['txid'] output_index = payload['output_index'] destination_address = payload['new_address'] + address_index = payload['address_index'] wallet_id = get_wallet_id(payload) # 10% of fees will go to miners. Can have better fee support in the future @@ -166,7 +164,8 @@ def start_spend(payload: dict, db): spend_request_id, destination_address, output_amount, - wallet_id)): + wallet_id, + address_index)): logging.info('[wallet] Saved spend request %s to the database', spend_request_id) return spend_request_id @@ -194,8 +193,7 @@ def save_nonce(payload: dict, db): # When the last signer provides a nonce, we can return the aggregate nonce (R_AGG) nonces = db.get_all_nonces(spend_request_id) - - if (len(nonces) != wallet['quorum']): + if (len(nonces) < wallet['quorum']): return None # Generate nonce points @@ -211,13 +209,12 @@ def save_nonce(payload: dict, db): # Create a sighash for ALL (0x00) sighash_musig = TaprootSignatureHash(spending_tx, [{'n': spend_request['output_index'], 'nValue': spend_request['prev_value_sats'], 'scriptPubKey': bytes.fromhex(spend_request['prev_script_pubkey'])}], SIGHASH_ALL_TAPROOT) - print(sighash_musig) # Update cache spending_txs[R_agg] = spending_tx # Encode everything as hex before returning - return (R_agg.get_bytes().hex(), sighash_musig.hex(), negated) + return (R_agg.get_bytes().hex(), sighash_musig.hex(), negated, spend_request['address_index']) def save_signature(payload, db): if (not 'signature' in payload): @@ -269,5 +266,3 @@ def save_signature(payload, db): # print("TXID", txid) return tx_serialized_hex - - diff --git a/src/signer/signer.py b/src/signer/signer.py index d599a00..7930bfb 100644 --- a/src/signer/signer.py +++ b/src/signer/signer.py @@ -93,7 +93,7 @@ def handle_create_wallet(quorum, relay_manager, private_key): def handle_create_xpub(wallet, relay_manager, private_key): xpub = wallet.get_root_xpub() add_xpub_payload = generate_nostr_message( - command='xpub', payload={'wallet_id': wallet.get_wallet_id(), 'xpub': wallet.get_pubkey()}) + command='xpub', payload={'wallet_id': wallet.get_wallet_id(), 'xpub': xpub}) construct_and_publish_event(add_xpub_payload, private_key, relay_manager) print("Operation Finished") @@ -117,11 +117,11 @@ def handle_get_address(wallet, index, relay_manager, private_key): return new_address -def handle_spend(outpoint, new_address, value, wallet, relay_manager, private_key): +def handle_spend(outpoint, new_address, value, wallet, relay_manager, private_key, address_index): time_stamp = int(time.time()) req_id = str(uuid.uuid4()) start_spend_payload = generate_nostr_message(command='spend', req_id=req_id, payload={'wallet_id': wallet.get_wallet_id( - ), 'txid': outpoint[0], 'output_index': outpoint[1], 'new_address': new_address, 'value': value}) + ), 'txid': outpoint[0], 'output_index': outpoint[1], 'new_address': new_address, 'value': value, 'address_index': address_index}) construct_and_publish_event( start_spend_payload, private_key, relay_manager) @@ -158,7 +158,6 @@ def handle_sign_tx(spend_request_id, wallet, relay_manager, private_key): payloads = read_cordinator_messages( relay_manager, private_key, time_stamp_filter=time_stamp) - print(payloads) filtered_payloads = [payload for payload in payloads if payload['command'] == "nonce" and 'spend_request_id' in payload['payload'] and payload['payload']['spend_request_id'] == spend_request_id] logging.info(filtered_payloads) @@ -175,12 +174,12 @@ def handle_sign_tx(spend_request_id, wallet, relay_manager, private_key): r_agg = nonce_response['payload']['r_agg'] sig_hash = nonce_response['payload']['sig_hash'] should_negate_nonce = nonce_response['payload']['negated'] - + address_index = int(nonce_response['payload']['address_index']) wallet.set_r_agg(r_agg) wallet.set_sig_hash(sig_hash) wallet.set_should_negate_nonce(should_negate_nonce) - partial_signature = wallet.sign_with_current_context(nonce) + partial_signature = wallet.sign_with_current_context(nonce, address_index) logging.info(f"Providing partial signatuire: {partial_signature}") #Provide cordinator with partial sig @@ -253,13 +252,13 @@ def run_signer(wallet_id=None, key_pair_seed=None, nonce_seed=None): elif user_input.lower() == SignerCommands.SEND_PUBLIC_KEY.value: logging.info("Generating and posting the public key...") - handle_create_xpub(wallet, relay_manager, nostr_private_key) - + handle_create_xpub(wallet, relay_manager, private_key) elif user_input.lower() == SignerCommands.GENERATE_ADDRESS.value: - # TODO bug: you cannot sign or spend with out getting an address first - logging.info("Generating a new address...") + # TODO right now you have to manage your own address indecies + index = int(input("Enter address index: ")) + logging.info(f"Generating a new address at index {index} ...") address_payload = handle_get_address( - wallet, 0, relay_manager, nostr_private_key) + wallet, index, relay_manager, private_key) wallet.set_cmap(address_payload['cmap']) wallet.set_pubkey_agg(address_payload['pubkey_agg']) @@ -271,9 +270,10 @@ def run_signer(wallet_id=None, key_pair_seed=None, nonce_seed=None): index = int(input("Enter output index: ")) new_address = input("Destination address (where would you like to send funds to?): ") sats = int(input("Amount in satoshis (how much are we spending?): ")) + address_index = int(input("Which address index are you spending from: ")) spend_request_id = handle_spend( - [txid, index], new_address, sats, wallet, relay_manager, nostr_private_key) + [txid, index], new_address, sats, wallet, relay_manager, nostr_private_key, address_index) wallet.set_current_spend_request_id(spend_request_id) logging.info( f'Your spend request id {spend_request_id}, next provide nonces and signatures!!') diff --git a/src/signer/wallet.py b/src/signer/wallet.py index 90dad99..708ee6d 100644 --- a/src/signer/wallet.py +++ b/src/signer/wallet.py @@ -76,36 +76,30 @@ def set_sig_hash(self, sig_hash): def set_current_spend_request_id(self, current_spend_request_id): self.current_spend_request_id = current_spend_request_id - def get_private_key_tweaked(self): - if self.cmap != None: - # TODO this is all bip32 stuff - # TODO index is hardcoded at 1 - # index = 1 - # pk = self.get_pubkey_at_index(index).hex() - # TODO hardcoded pk, get from class variable - # prv = self.get_root_hd_node().get_privkey_from_path(f"m/{index}") - # print("prv", prv) - # private_key = ECKey().set(prv) - - pk = self.public_key.get_bytes().hex() - private_key = self.private_key - tweaked_key = private_key * self.cmap[pk] - - if self.pubkey_agg.get_y() % 2 != 0: - tweaked_key.negate() - self.pubkey_agg.negate() - - return tweaked_key - return None - - def sign_with_current_context(self, nonce: str): + def get_private_key_tweaked(self, address_index: int): + if self.cmap == None: + return None + pk = self.get_pubkey_at_index(address_index).hex() + prv = self.get_root_hd_node().get_privkey_from_path(f"m/{address_index}") + private_key = ECKey().set(prv) + + tweaked_key = private_key * self.cmap[pk] + # TODO bug here where the server calcualtes a different y value and the signer + if self.pubkey_agg.get_y() % 2 != 0: + tweaked_key.negate() + self.pubkey_agg.negate() + + return tweaked_key + + def sign_with_current_context(self, nonce: str, address_index: int): if self.sig_hash == None or self.cmap == None or self.r_agg == None or self.pubkey_agg == None: # TODO should throw return None - k1 = ECKey().set(self.nonce_seed) + k = ECKey().set(self.nonce_seed) # negate here if self.should_negate_nonce: - k1.negate() - tweaked_private_key = self.get_private_key_tweaked() - return sign_musig(tweaked_private_key, k1, self.r_agg, self.pubkey_agg, self.sig_hash) + k.negate() + tweaked_private_key = self.get_private_key_tweaked(address_index) + return sign_musig(tweaked_private_key, k, self.r_agg, self.pubkey_agg, self.sig_hash) +