Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/xpubs #32

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/coordinator/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions src/coordinator/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
31 changes: 13 additions & 18 deletions src/coordinator/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 == []):
Expand All @@ -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())
Expand All @@ -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")

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -269,5 +266,3 @@ def save_signature(payload, db):
# print("TXID", txid)

return tx_serialized_hex


24 changes: 12 additions & 12 deletions src/signer/signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
handle_create_xpub(wallet, relay_manager, private_key)
handle_create_xpub(wallet, relay_manager, nostr_private_key)

I think this should be kept nostr_private_key? I renamed it as part of this cleanup a little over a week ago #18

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# TODO right now you have to manage your own address indecies
# TODO right now you have to manage your own address indexes

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
wallet, index, relay_manager, private_key)
wallet, index, relay_manager, nostr_private_key)


wallet.set_cmap(address_payload['cmap'])
wallet.set_pubkey_agg(address_payload['pubkey_agg'])
Expand All @@ -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!!')
Expand Down
48 changes: 21 additions & 27 deletions src/signer/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought - is it okay to multiply values like this? I would think for Schnorr it's fine since it has those nice linear properties that ECDSA doesn't, but it could be worth double checking if this is where the negation bug is coming from.

# 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)