Skip to content

Commit

Permalink
Merge 060ca8f into 3bbaf6a
Browse files Browse the repository at this point in the history
  • Loading branch information
fyookball committed Oct 17, 2020
2 parents 3bbaf6a + 060ca8f commit da0531d
Show file tree
Hide file tree
Showing 7 changed files with 583 additions and 13 deletions.
16 changes: 16 additions & 0 deletions lib/commands.py
Expand Up @@ -36,6 +36,7 @@
from functools import wraps

from . import bitcoin
from . import rpa_paycode
from . import util
from .address import Address, AddressError
from .bitcoin import hash_160, COIN, TYPE_ADDRESS
Expand Down Expand Up @@ -546,6 +547,20 @@ def _mktx(self, outputs, fee=None, change_addr=None, domain=None, nocheck=False,
self.wallet.save_transactions()
return tx

@command('')
def rpa_generate_paycode(self):
return rpa_paycode.rpa_generate_paycode(self.wallet)

@command('wp')
def rpa_generate_transaction_from_paycode(self, amount, paycode=None, fee= None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, password=None, locktime=None,
op_return=None, op_return_raw=None):
# WARNING: Amount is in full Bitcoin Cash units
return rpa_paycode.rpa_generate_transaction_from_paycode(self.wallet, self.config, amount, paycode)

@command('wp')
def rpa_extract_private_key_from_transaction(self, raw_tx, password=None):
return rpa_paycode.rpa_extract_private_key_from_transaction(self.wallet, raw_tx, password)

@command('wp')
def payto(self, destination, amount, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, password=None, locktime=None,
op_return=None, op_return_raw=None, addtransaction=False):
Expand Down Expand Up @@ -876,6 +891,7 @@ def help(self):
'paid': (None, "Show only paid requests."),
'passphrase': (None, "Seed extension"),
'password': ("-W", "Password"),
'paycode': (None, 'RPA Resuable Payment Address Paycode'),
'payment_url': (None, 'Optional URL where you would like users to POST the BIP70 Payment message'),
'pending': (None, "Show only pending requests."),
'privkey': (None, "Private key. Set to '?' to get a prompt."),
Expand Down
4 changes: 2 additions & 2 deletions lib/keystore.py
Expand Up @@ -104,7 +104,7 @@ def decrypt_message(self, sequence, message, password):
decrypted = ec.decrypt_message(message)
return decrypted

def sign_transaction(self, tx, password, *, use_cache=False):
def sign_transaction(self, tx, password, *, use_cache=False, ndata=None):
if self.is_watching_only():
return
# Raise if password is not correct.
Expand All @@ -115,7 +115,7 @@ def sign_transaction(self, tx, password, *, use_cache=False):
keypairs[k] = self.get_private_key(v, password)
# Sign
if keypairs:
tx.sign(keypairs, use_cache=use_cache)
tx.sign(keypairs, use_cache=use_cache, ndata=ndata)


class Imported_KeyStore(Software_KeyStore):
Expand Down
343 changes: 343 additions & 0 deletions lib/rpa_paycode.py
@@ -0,0 +1,343 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# -*- mode: python3 -*-
# This file (c) 2020 Jonald Fyookball
# Part of the Electron Cash SPV Wallet
# License: MIT
'''
This implements the functionality for RPA (Reusable Payment Address) aka Paycodes
'''


from . import address
from . import bitcoin
from . import rpaaddr
from . import transaction

from .address import Address, AddressError, Base58, PublicKey
from .bitcoin import COIN, TYPE_ADDRESS, sha256
from .transaction import Transaction

from decimal import Decimal as PyDecimal

def satoshis(amount):
# satoshi conversion must not be performed by the parser
return int(COIN*PyDecimal(amount)) if amount not in ['!', None] else amount

def resolver(wallet, x, nocheck):
if x is None:
return None
out = wallet.contacts.resolve(x)
if out.get('type') == 'openalias' and nocheck is False and out.get('validated') is False:
raise BaseException('cannot verify alias', x)
return out['address']

def mktx(wallet, config, outputs, fee=None, change_addr=None, domain=None, nocheck=False,
unsigned=False, password=None, locktime=None, op_return=None, op_return_raw=None):
if op_return and op_return_raw:
raise ValueError('Both op_return and op_return_raw cannot be specified together!')

domain = None if domain is None else map(self._resolver, domain)
final_outputs = []
if op_return:
final_outputs.append(OPReturn.output_for_stringdata(op_return))
elif op_return_raw:
try:
op_return_raw = op_return_raw.strip()
tmp = bytes.fromhex(op_return_raw).hex()
assert tmp == op_return_raw.lower()
op_return_raw = tmp
except Exception as e:
raise ValueError("op_return_raw must be an even number of hex digits") from e
final_outputs.append(OPReturn.output_for_rawhex(op_return_raw))

for address, amount in outputs:
address = resolver(wallet, address, nocheck)
amount = satoshis(amount)
final_outputs.append((TYPE_ADDRESS, address, amount))

coins = wallet.get_spendable_coins(domain, config)
tx = wallet.make_unsigned_transaction(coins, final_outputs, config, fee, change_addr)
if locktime != None:
tx.locktime = locktime
if not unsigned:
run_hook('sign_tx', wallet, tx)
wallet.sign_transaction(tx, password)
return tx

def calculate_paycode_shared_secret(private_key, public_key, outpoint):

"""private key is expected to be an integer.
public_key is expected to be bytes.
outpoint is expected to be a string.
returns the paycode shared secret as bytes"""

from fastecdsa import keys, curve
from fastecdsa.point import Point

# Public key is expected to be compressed. Change into a point object.
pubkey_point = bitcoin.ser_to_point(public_key)
fastecdsa_point = Point(pubkey_point.x(), pubkey_point.y(), curve.secp256k1)

# Multiply the public and private points together
ecdh_product = fastecdsa_point * private_key
ecdh_x = ecdh_product.x
ecdh_x_bytes = ecdh_x.to_bytes(33, byteorder="big")

# Get the hash of the product
sha_ecdh_x_bytes = sha256(ecdh_x_bytes)
sha_ecdh_x_as_int = int.from_bytes(sha_ecdh_x_bytes, byteorder="big")

# Hash the outpoint string
hash_of_outpoint = sha256(outpoint)
hash_of_outpoint_as_int = int.from_bytes(hash_of_outpoint, byteorder="big")

# Sum the ECDH hash and the outpoint Hash
grand_sum = sha_ecdh_x_as_int + hash_of_outpoint_as_int

# Hash the final result
grand_sum_hex = hex(grand_sum)
shared_secret = sha256(grand_sum_hex)

return shared_secret

def generate_address_from_pubkey_and_secret(parent_pubkey, secret):

"""parent_pubkey and secret are expected to be bytes
This function generates a receiving address based on CKD."""

new_pubkey = bitcoin.CKD_pub(parent_pubkey, secret, 0)[0]

use_uncompressed = False

# Currently, just uses compressed keys, but if this ever changes to require uncompressed points:
if use_uncompressed:
pubkey_point = bitcoin.ser_to_point(new_pubkey)
point_x=pubkey_point.x()
point_y=pubkey_point.y()
uncompressed="04"+hex(pubkey_point.x())[2:]+hex(pubkey_point.y())[2:]
new_pubkey = bytes.fromhex(uncompressed)

addr = Address.from_pubkey(new_pubkey)
return addr


def generate_privkey_from_secret(parent_privkey, secret):

"""parent_privkey and secret are expected to be bytes
This function generates a receiving address based on CKD."""

new_privkey = bitcoin.CKD_priv(parent_privkey, secret, 0)[0].hex()
return new_privkey


def rpa_generate_paycode(wallet, prefix_size="08"):

#prefix size should be either 0x04 , 0x08, 0x0C, 0x10

# Fields of the paycode
version = "01"
scanpubkey = wallet.derive_pubkeys(0, 0)
spendpubkey = wallet.derive_pubkeys(0, 1)
expiry = "00000000"

# Concatenate
payloadstring = version + prefix_size + scanpubkey + spendpubkey + expiry

# Convert to bytes
payloadbytes = bytes.fromhex(payloadstring)

# Generate paycode "address" via rpaaddr function
prefix="paycode"
retval = rpaaddr.encode_full(prefix, rpaaddr.PUBKEY_TYPE, payloadbytes)

return retval


def rpa_generate_transaction_from_paycode(wallet, config, amount, rpa_paycode=None, fee= None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, password=None, locktime=None,
op_return=None, op_return_raw=None):

if not wallet.is_schnorr_enabled():
print ("You must enable schnorr signing on this wallet for RPA. Exiting.")
return 0

# Initialize variable for the final return value.
final_raw_tx = 0

# Decode the paycode
rprefix, addr_hash= rpaaddr.decode(rpa_paycode)
paycode_hex = addr_hash.hex()

# Parse paycode
paycode_field_version = paycode_hex[0:2]
paycode_field_prefix_size = paycode_hex [2:4]
paycode_field_scan_pubkey = paycode_hex [4:70]
paycode_field_spend_pubkey = paycode_hex [70:136]
paycode_field_expiry = paycode_hex [136:144]
paycode_field_checksum = paycode_hex [ 144: 154]

# Initialize a few variables for the transaction
tx_fee = satoshis(fee)
domain = from_addr.split(',') if from_addr else None

# Initiliaze a few variables for grinding
tx_matches_paycode_prefix = False
grind_nonce = 0
grinding_version = "1"

if paycode_field_prefix_size == "04":
prefix_chars=1
elif paycode_field_prefix_size == "08":
prefix_chars=2
elif paycode_field_prefix_size == "0C":
prefix_chars=3
elif paycode_field_prefix_size == "10":
prefix_chars=4
else:
raise BaseException("Invalid prefix size. Must be 4,8,12, or 16 bits.")

print ("Attempting to grind a matching prefix. This may take a few minutes. Please be patient.")



# While loop for grinding. Keep grinding until txid prefix matches paycode scanpubkey prefix.
while not tx_matches_paycode_prefix:

# Construct the transaction, initially with a dummy destination
rpa_dummy_address = wallet.dummy_address().to_string(Address.FMT_CASHADDR)
unsigned = True
tx = mktx(wallet, config, [(rpa_dummy_address, amount)], tx_fee, change_addr, domain, nocheck, unsigned, password, locktime, op_return, op_return_raw)

# Calculate ndata for grinding. Ndata is passed through the stack as an input into RFC 6979
grind_nonce_string = str(grind_nonce)
grinding_message = rpa_paycode + grind_nonce_string + grinding_version
ndata = sha256(grinding_message)

# Use the first input (input zero) for our shared secret
input_zero = tx._inputs[0]

# Fetch our own private key for the coin
bitcoin_addr = input_zero["address"]
private_key_wif_format = wallet.export_private_key(bitcoin_addr, password)
private_key_int_format = int.from_bytes(Base58.decode_check(private_key_wif_format)[1:33], byteorder="big")

# Grab the outpoint (the colon is intentionally ommitted from the string)
outpoint_string = str(input_zero["prevout_hash"])+str(input_zero["prevout_n"])

# Format the pubkey in preparation to get the shared secret
scanpubkey_bytes = bytes.fromhex(paycode_field_scan_pubkey)

# Calculate shared secret
shared_secret = calculate_paycode_shared_secret(private_key_int_format, scanpubkey_bytes, outpoint_string)

# Get the real destination for the transaction
rpa_destination_address=generate_address_from_pubkey_and_secret(bytes.fromhex(paycode_field_spend_pubkey), shared_secret).to_string(Address.FMT_CASHADDR)

# Swap the dummy destination for the real destination
tx.rpa_paycode_swap_dummy_for_destination(rpa_dummy_address, rpa_destination_address)

# Sort the inputs and outputs deterministically
tx.BIP_LI01_sort()

# Now we need to sign the transaction after the outputs are known
wallet.sign_transaction(tx, password, ndata=ndata)

# Generate the raw transaction
raw_tx_string = tx.as_dict()["hex"]

# Get the TxId for this raw Tx.
double_hash_tx = bytearray(sha256(sha256(bytes.fromhex(raw_tx_string))))
double_hash_tx.reverse()
txid=double_hash_tx.hex()

# Check if we got a successful match. If so, exit.
if txid[0:prefix_chars].upper() == paycode_field_scan_pubkey[2:prefix_chars+2].upper():
print ("Grinding successful after ", grind_nonce, " iterations.")
print ("Transaction Id: ", txid)
print ("prefix is ", txid[0:prefix_chars].upper())
final_raw_tx = raw_tx_string
tx_matches_paycode_prefix = True # <<-- exit

# Increment the nonce
grind_nonce+=1

return final_raw_tx

def rpa_extract_private_key_from_transaction(wallet, raw_tx, password=None):

# Initialize return value. Will return 0 if no private key can be found.
retval = 0

# Deserialize the raw transaction
unpacked_tx = Transaction.deserialize(Transaction(raw_tx))

# Get a list of output addresses (we will need this for later to check if our key matches)
output_addresses = []
outputs = unpacked_tx["outputs"]
for i in outputs:
output_addresses.append(i['address'].to_string(Address.FMT_CASHADDR))

# Variables for looping
number_of_inputs = len (unpacked_tx["inputs"])
input_index = 0
process_inputs = True

# Process each input until we find one that creates the shared secret to get a private key for an output
while process_inputs:

# Grab the outpoint
single_input = unpacked_tx["inputs"][input_index]
prevout_hash = single_input["prevout_hash"]
prevout_n = str(single_input["prevout_n"]) # n is int. convert to str.
outpoint_string = prevout_hash + prevout_n

# Get the pubkey of the sender from the scriptSig.
scriptSig = bytes.fromhex(single_input["scriptSig"])
d={}
parsed_scriptSig = transaction.parse_scriptSig(d, scriptSig)
sender_pubkey = bytes.fromhex(d["pubkeys"][0])

# We need the private key that corresponds to the scanpubkey.
# In this implementation, this is the one that goes with receiving address 0
scanpubkey = wallet.derive_pubkeys(0, 0)

# Fetch our own private (scan) key out of the wallet.
scan_bitcoin_addr = Address.from_pubkey(scanpubkey)
scan_private_key_wif_format = wallet.export_private_key(scan_bitcoin_addr, password)
scan_private_key_int_format = int.from_bytes(Base58.decode_check(scan_private_key_wif_format)[1:33], byteorder="big")

# Calculate shared secret
shared_secret = calculate_paycode_shared_secret(scan_private_key_int_format, sender_pubkey, outpoint_string)

# Get the spendpubkey for our paycode.
# In this implementation, simply: receiving address 1.
spendpubkey = wallet.derive_pubkeys(0, 1)

# Get the destination address for the transaction
destination=generate_address_from_pubkey_and_secret(bytes.fromhex(spendpubkey), shared_secret).to_string(Address.FMT_CASHADDR)

# Fetch our own private (spend) key out of the wallet.
spendpubkey = wallet.derive_pubkeys(0, 1)
spend_bitcoin_addr = Address.from_pubkey(spendpubkey)
spend_private_key_wif_format = wallet.export_private_key(spend_bitcoin_addr, password)
spend_private_key_int_format = int.from_bytes(Base58.decode_check(spend_private_key_wif_format)[1:33], byteorder="big")

# Generate the private key for the money being received via paycode
privkey = generate_privkey_from_secret(bytes.fromhex(hex(spend_private_key_int_format)[2:]), shared_secret)

# Check the address matches
if destination in output_addresses:
process_inputs = False
retval = privkey

# Increment the input
input_index+=1

# If this was the last input, stop.
if input_index >= number_of_inputs:
process_inputs = False

return retval



0 comments on commit da0531d

Please sign in to comment.