1
0
mirror of https://github.com/bitcoin/bips.git synced 2024-11-19 01:40:05 +01:00

Add reference.py with test vectors

* reference.py contains the silent payment specific code
* secp256k1.py for doing the EC operations
* bech32m.py contains code for encoding/decoding bech32(m) addresses
* bitcoin_utils.py contains some helper code, not specific to silent
  payments
* send_and_receive_test_vectors.json contains the wallet unit test
  vectors

Co-Authored-By: S3RK <1466284+S3RK@users.noreply.github.com>
Co-Authored-By: Oghenovo Usiwoma <37949128+Eunovo@users.noreply.github.com>
Co-authored-by: S.Blagogee <34041358+setavenger@users.noreply.github.com>
This commit is contained in:
josibake 2024-01-15 12:05:08 +01:00
parent 96f4e5a4c4
commit 33a99a1a17
No known key found for this signature in database
GPG Key ID: 8ADCB558C4F33D65
6 changed files with 4049 additions and 0 deletions

View File

@ -370,6 +370,58 @@ If using a seed/seed phrase only style backup, the user can recover the wallet's
Silent payments introduces a new address format and protocol for sending and as such is not compatible with older wallet software or wallets which have not implemented the silent payments protocol.
== Test Vectors ==
A [[bip-0352/send_and_receive_test_vectors.json|collection of test vectors in JSON format]] are provided, along with a [[bip-0352/reference.py|python reference implementation]]. Each test vector consists of a sending test case and corresponding receiving test case. This is to allow sending and receiving to be implemented separately. To ensure determinism while testing, sort the array of ''B<sub>m</sub>'' by amount (see the [[bip-0352/reference.py|reference implementation]]). Test cases use the following schema:
''' test_case '''
{
"comment": "Comment describing the behavior being tested",
"sending": [<array of sender test objects>],
"receiving": [<array of recipient test objects>],
}
''' sender '''
{
"given": {
"vin": [<array of vin objects with an added field for the private key. These objects are structured to match the `vin` output field from `getrawtransaction verbosity=2`>],
"recipients": [<array of strings, where each string is a bech32m encoding representing a silent payment address>]
},
"expected": {
"outputs": [<array of strings, where each string is a hex encoding of 32-byte X-only public key; contains all possible output sets, test must match a subset of size `n_outputs`>],
"n_outouts": <integer for the exact number of expected outputs>,
},
}
''' recipient '''
{
"given": {
"vin": [<array of vin objects. These objects are structured to match the `vin` output field from `getrawtransaction verbosity=2`>],
"key_material": {
"scan_priv_key": <hex encoded scan private key>,
"spend_priv_key": <hex encoded spend private key>,
}
"labels": [<array of ints, representing labels the receiver has used>],
},
"expected": {
"addresses": [<array of bech32m strings, one for the silent payment address and each labeled address (if used)>],
"outputs": [<array of outputs with tweak and signature; contains all possible output sets, tester must match a subset of size `n_outputs`>
{
"priv_key_tweak": <hex encoded private key tweak data>,
"pub_key": <hex encoded X-only public key>,
"signature": <hex encoded signature for the output (produced with spend_priv_key + priv_key_tweak)>
},
...
],
"n_outputs": <integer for the exact number of expected outputs>
}
}
Wallets should include inputs not in the ''[[#inputs-for-shared-secret-derivation|Inputs For Shared Secret Derivation]]'' list when testing to ensure that only inputs from the list are being used for shared secret derivation. Additionally, receiving wallets should include non-silent payment outputs for themselves in testing to ensure silent payments scanning does not interfere with regular outputs detection.
=== Functional tests ===
Below is a list of functional tests which should be included in sending and receiving implementations.

135
bip-0352/bech32m.py Normal file
View File

@ -0,0 +1,135 @@
# Copyright (c) 2017, 2020 Pieter Wuille
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""Reference implementation for Bech32/Bech32m and segwit addresses."""
from enum import Enum
class Encoding(Enum):
"""Enumeration type to list the various supported encodings."""
BECH32 = 1
BECH32M = 2
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
BECH32M_CONST = 0x2bc830a3
def bech32_polymod(values):
"""Internal function that computes the Bech32 checksum."""
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
chk = 1
for value in values:
top = chk >> 25
chk = (chk & 0x1ffffff) << 5 ^ value
for i in range(5):
chk ^= generator[i] if ((top >> i) & 1) else 0
return chk
def bech32_hrp_expand(hrp):
"""Expand the HRP into values for checksum computation."""
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
def bech32_verify_checksum(hrp, data):
"""Verify a checksum given HRP and converted data characters."""
const = bech32_polymod(bech32_hrp_expand(hrp) + data)
if const == 1:
return Encoding.BECH32
if const == BECH32M_CONST:
return Encoding.BECH32M
return None
def bech32_create_checksum(hrp, data, spec):
"""Compute the checksum values given HRP and data."""
values = bech32_hrp_expand(hrp) + data
const = BECH32M_CONST if spec == Encoding.BECH32M else 1
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
def bech32_encode(hrp, data, spec):
"""Compute a Bech32 string given HRP and data values."""
combined = data + bech32_create_checksum(hrp, data, spec)
return hrp + '1' + ''.join([CHARSET[d] for d in combined])
def bech32_decode(bech):
"""Validate a Bech32/Bech32m string, and determine HRP and data."""
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
(bech.lower() != bech and bech.upper() != bech)):
return (None, None, None)
bech = bech.lower()
pos = bech.rfind('1')
# remove the requirement that bech32m be less than 90 chars
if pos < 1 or pos + 7 > len(bech):
return (None, None, None)
if not all(x in CHARSET for x in bech[pos+1:]):
return (None, None, None)
hrp = bech[:pos]
data = [CHARSET.find(x) for x in bech[pos+1:]]
spec = bech32_verify_checksum(hrp, data)
if spec is None:
return (None, None, None)
return (hrp, data[:-6], spec)
def convertbits(data, frombits, tobits, pad=True):
"""General power-of-2 base conversion."""
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
max_acc = (1 << (frombits + tobits - 1)) - 1
for value in data:
if value < 0 or (value >> frombits):
return None
acc = ((acc << frombits) | value) & max_acc
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad:
if bits:
ret.append((acc << (tobits - bits)) & maxv)
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
return None
return ret
def decode(hrp, addr):
"""Decode a segwit address."""
hrpgot, data, spec = bech32_decode(addr)
if hrpgot != hrp:
return (None, None)
decoded = convertbits(data[1:], 5, 8, False)
if decoded is None or len(decoded) < 2:
return (None, None)
if data[0] > 16:
return (None, None)
return (data[0], decoded)
def encode(hrp, witver, witprog):
"""Encode a segwit address."""
spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec)
if decode(hrp, ret) == (None, None):
return None
return ret

158
bip-0352/bitcoin_utils.py Normal file
View File

@ -0,0 +1,158 @@
import hashlib
import struct
from io import BytesIO
from secp256k1 import ECKey
from typing import Union
def from_hex(hex_string):
"""Deserialize from a hex string representation (e.g. from RPC)"""
return BytesIO(bytes.fromhex(hex_string))
def ser_uint32(u: int) -> bytes:
return u.to_bytes(4, "big")
def ser_uint256(u):
return u.to_bytes(32, 'little')
def deser_uint256(f):
return int.from_bytes(f.read(32), 'little')
def deser_txid(txid: str):
# recall that txids are serialized little-endian, but displayed big-endian
# this means when converting from a human readable hex txid, we need to first
# reverse it before deserializing it
dixt = "".join(map(str.__add__, txid[-2::-2], txid[-1::-2]))
return bytes.fromhex(dixt)
def deser_compact_size(f: BytesIO):
view = f.getbuffer()
nbytes = view.nbytes;
view.release()
if (nbytes == 0):
return 0 # end of stream
nit = struct.unpack("<B", f.read(1))[0]
if nit == 253:
nit = struct.unpack("<H", f.read(2))[0]
elif nit == 254:
nit = struct.unpack("<I", f.read(4))[0]
elif nit == 255:
nit = struct.unpack("<Q", f.read(8))[0]
return nit
def deser_string(f: BytesIO):
nit = deser_compact_size(f)
return f.read(nit)
def deser_string_vector(f: BytesIO):
nit = deser_compact_size(f)
r = []
for _ in range(nit):
t = deser_string(f)
r.append(t)
return r
class COutPoint:
__slots__ = ("hash", "n",)
def __init__(self, hash=b"", n=0,):
self.hash = hash
self.n = n
def serialize(self):
r = b""
r += self.hash
r += struct.pack("<I", self.n)
return r
def deserialize(self, f):
self.hash = f.read(32)
self.n = struct.unpack("<I", f.read(4))[0]
class VinInfo:
__slots__ = ("outpoint", "scriptSig", "txinwitness", "prevout", "private_key")
def __init__(self, outpoint=None, scriptSig=b"", txinwitness=None, prevout=b"", private_key=None):
if outpoint is None:
self.outpoint = COutPoint()
else:
self.outpoint = outpoint
if txinwitness is None:
self.txinwitness = CTxInWitness()
else:
self.txinwitness = txinwitness
if private_key is None:
self.private_key = ECKey()
else:
self.private_key = private_key
self.scriptSig = scriptSig
self.prevout = prevout
class CScriptWitness:
__slots__ = ("stack",)
def __init__(self):
# stack is a vector of strings
self.stack = []
def is_null(self):
if self.stack:
return False
return True
class CTxInWitness:
__slots__ = ("scriptWitness",)
def __init__(self):
self.scriptWitness = CScriptWitness()
def deserialize(self, f: BytesIO):
self.scriptWitness.stack = deser_string_vector(f)
return self
def is_null(self):
return self.scriptWitness.is_null()
def hash160(s: Union[bytes, bytearray]) -> bytes:
return hashlib.new("ripemd160", hashlib.sha256(s).digest()).digest()
def is_p2tr(spk: bytes) -> bool:
if len(spk) != 34:
return False
# OP_1 OP_PUSHBYTES_32 <32 bytes>
return (spk[0] == 0x51) & (spk[1] == 0x20)
def is_p2wpkh(spk: bytes) -> bool:
if len(spk) != 22:
return False
# OP_0 OP_PUSHBYTES_20 <20 bytes>
return (spk[0] == 0x00) & (spk[1] == 0x14)
def is_p2sh(spk: bytes) -> bool:
if len(spk) != 23:
return False
# OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUAL
return (spk[0] == 0xA9) & (spk[1] == 0x14) & (spk[-1] == 0x87)
def is_p2pkh(spk: bytes) -> bool:
if len(spk) != 25:
return False
# OP_DUP OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
return (spk[0] == 0x76) & (spk[1] == 0xA9) & (spk[2] == 0x14) & (spk[-2] == 0x88) & (spk[-1] == 0xAC)

335
bip-0352/reference.py Executable file
View File

@ -0,0 +1,335 @@
#!/usr/bin/env python3
# For running the test vectors, run this script:
# ./reference.py send_and_receive_test_vectors.json
import hashlib
import json
from typing import List, Tuple, Dict, cast
from sys import argv, exit
from functools import reduce
from itertools import permutations
# local files
from bech32m import convertbits, bech32_encode, decode, Encoding
from secp256k1 import ECKey, ECPubKey, TaggedHash, NUMS_H
from bitcoin_utils import (
deser_txid,
from_hex,
hash160,
is_p2pkh,
is_p2sh,
is_p2wpkh,
is_p2tr,
ser_uint32,
COutPoint,
CTxInWitness,
VinInfo,
)
def get_pubkey_from_input(vin: VinInfo) -> ECPubKey:
if is_p2pkh(vin.prevout):
# skip the first 3 op_codes and grab the 20 byte hash
# from the scriptPubKey
spk_hash = vin.prevout[3:3 + 20]
for i in range(len(vin.scriptSig), 0, -1):
if i - 33 >= 0:
# starting from the back, we move over the scriptSig with a 33 byte
# window (to match a compressed pubkey). we hash this and check if it matches
# the 20 byte has from the scriptPubKey. for standard scriptSigs, this will match
# right away because the pubkey is the last item in the scriptSig.
# if its a non-standard (malleated) scriptSig, we will still find the pubkey if its
# a compressed pubkey.
#
# note: this is an incredibly inefficient implementation, for demonstration purposes only.
pubkey_bytes = vin.scriptSig[i - 33:i]
pubkey_hash = hash160(pubkey_bytes)
if pubkey_hash == spk_hash:
pubkey = ECPubKey().set(pubkey_bytes)
if (pubkey.valid) & (pubkey.compressed):
return pubkey
if is_p2sh(vin.prevout):
redeem_script = vin.scriptSig[1:]
if is_p2wpkh(redeem_script):
pubkey = ECPubKey().set(vin.txinwitness.scriptWitness.stack[-1])
if (pubkey.valid) & (pubkey.compressed):
return pubkey
if is_p2wpkh(vin.prevout):
txin = vin.txinwitness
pubkey = ECPubKey().set(txin.scriptWitness.stack[-1])
if (pubkey.valid) & (pubkey.compressed):
return pubkey
if is_p2tr(vin.prevout):
witnessStack = vin.txinwitness.scriptWitness.stack
if (len(witnessStack) >= 1):
if (len(witnessStack) > 1 and witnessStack[-1][0] == 0x50):
# Last item is annex
witnessStack.pop()
if (len(witnessStack) > 1):
# Script-path spend
control_block = witnessStack[-1]
# control block is <control byte> <32 byte internal key> and 0 or more <32 byte hash>
internal_key = control_block[1:33]
if (internal_key == NUMS_H.to_bytes(32, 'big')):
# Skip if NUMS_H
return ECPubKey()
pubkey = ECPubKey().set(vin.prevout[2:])
if (pubkey.valid) & (pubkey.compressed):
return pubkey
return ECPubKey()
def get_input_hash(outpoints: List[COutPoint], sum_input_pubkeys: ECPubKey) -> bytes:
lowest_outpoint = sorted(outpoints, key=lambda outpoint: outpoint.serialize())[0]
return TaggedHash("BIP0352/Inputs", lowest_outpoint.serialize() + cast(bytes, sum_input_pubkeys.get_bytes(False)))
def encode_silent_payment_address(B_scan: ECPubKey, B_m: ECPubKey, hrp: str = "tsp", version: int = 0) -> str:
data = convertbits(cast(bytes, B_scan.get_bytes(False)) + cast(bytes, B_m.get_bytes(False)), 8, 5)
return bech32_encode(hrp, [version] + cast(List[int], data), Encoding.BECH32M)
def generate_label(b_scan: ECKey, m: int) -> bytes:
return TaggedHash("BIP0352/Label", b_scan.get_bytes() + ser_uint32(m))
def create_labeled_silent_payment_address(b_scan: ECKey, B_spend: ECPubKey, m: int, hrp: str = "tsp", version: int = 0) -> str:
G = ECKey().set(1).get_pubkey()
B_scan = b_scan.get_pubkey()
B_m = B_spend + generate_label(b_scan, m) * G
labeled_address = encode_silent_payment_address(B_scan, B_m, hrp, version)
return labeled_address
def decode_silent_payment_address(address: str, hrp: str = "tsp") -> Tuple[ECPubKey, ECPubKey]:
_, data = decode(hrp, address)
if data is None:
return ECPubKey(), ECPubKey()
B_scan = ECPubKey().set(data[:33])
B_spend = ECPubKey().set(data[33:])
return B_scan, B_spend
def create_outputs(input_priv_keys: List[Tuple[ECKey, bool]], input_hash: bytes, recipients: List[str], hrp="tsp") -> List[str]:
G = ECKey().set(1).get_pubkey()
negated_keys = []
for key, is_xonly in input_priv_keys:
k = ECKey().set(key.get_bytes())
if is_xonly and k.get_pubkey().get_y() % 2 != 0:
k.negate()
negated_keys.append(k)
a_sum = sum(negated_keys)
silent_payment_groups: Dict[ECPubKey, List[ECPubKey]] = {}
for recipient in recipients:
B_scan, B_m = decode_silent_payment_address(recipient, hrp=hrp)
if B_scan in silent_payment_groups:
silent_payment_groups[B_scan].append(B_m)
else:
silent_payment_groups[B_scan] = [B_m]
outputs = []
for B_scan, B_m_values in silent_payment_groups.items():
ecdh_shared_secret = input_hash * a_sum * B_scan
k = 0
for B_m in B_m_values:
t_k = TaggedHash("BIP0352/SharedSecret", ecdh_shared_secret.get_bytes(False) + ser_uint32(k))
P_km = B_m + t_k * G
outputs.append(P_km.get_bytes().hex())
k += 1
return list(set(outputs))
def scanning(b_scan: ECKey, B_spend: ECPubKey, A_sum: ECPubKey, input_hash: bytes, outputs_to_check: List[ECPubKey], labels: Dict[str, str] = {}) -> List[Dict[str, str]]:
G = ECKey().set(1).get_pubkey()
ecdh_shared_secret = input_hash * b_scan * A_sum
k = 0
wallet = []
while True:
t_k = TaggedHash("BIP0352/SharedSecret", ecdh_shared_secret.get_bytes(False) + ser_uint32(k))
P_k = B_spend + t_k * G
for output in outputs_to_check:
if P_k == output:
wallet.append({"pub_key": P_k.get_bytes().hex(), "priv_key_tweak": t_k.hex()})
outputs_to_check.remove(output)
k += 1
break
elif labels:
m_G_sub = output - P_k
if m_G_sub.get_bytes(False).hex() in labels:
P_km = P_k + m_G_sub
wallet.append({
"pub_key": P_km.get_bytes().hex(),
"priv_key_tweak": (ECKey().set(t_k).add(
bytes.fromhex(labels[m_G_sub.get_bytes(False).hex()])
)).get_bytes().hex(),
})
outputs_to_check.remove(output)
k += 1
break
else:
output.negate()
m_G_sub = output - P_k
if m_G_sub.get_bytes(False).hex() in labels:
P_km = P_k + m_G_sub
wallet.append({
"pub_key": P_km.get_bytes().hex(),
"priv_key_tweak": (ECKey().set(t_k).add(
bytes.fromhex(labels[m_G_sub.get_bytes(False).hex()])
)).get_bytes().hex(),
})
outputs_to_check.remove(output)
k += 1
break
else:
break
return wallet
if __name__ == "__main__":
if len(argv) != 2 or argv[1] in ('-h', '--help'):
print("Usage: ./reference.py send_and_receive_test_vectors.json")
exit(0)
with open(argv[1], "r") as f:
test_data = json.loads(f.read())
# G , needed for generating the labels "database"
G = ECKey().set(1).get_pubkey()
for case in test_data:
print(case["comment"])
# Test sending
for sending_test in case["sending"]:
given = sending_test["given"]
expected = sending_test["expected"]
vins = [
VinInfo(
outpoint=COutPoint(hash=deser_txid(input["txid"]), n=input["vout"]),
scriptSig=bytes.fromhex(input["scriptSig"]),
txinwitness=CTxInWitness().deserialize(from_hex(input["txinwitness"])),
prevout=bytes.fromhex(input["prevout"]["scriptPubKey"]["hex"]),
private_key=ECKey().set(bytes.fromhex(input["private_key"])),
)
for input in given["vin"]
]
# Conver the tuples to lists so they can be easily compared to the json list of lists from the given test vectors
input_priv_keys = []
input_pub_keys = []
for vin in vins:
pubkey = get_pubkey_from_input(vin)
if not pubkey.valid:
continue
input_priv_keys.append((
vin.private_key,
is_p2tr(vin.prevout),
))
input_pub_keys.append(pubkey)
sending_outputs = []
if (len(input_pub_keys) > 0):
A_sum = reduce(lambda x, y: x + y, input_pub_keys)
input_hash = get_input_hash([vin.outpoint for vin in vins], A_sum)
sending_outputs = create_outputs(input_priv_keys, input_hash, given["recipients"], hrp="sp")
# Note: order doesn't matter for creating/finding the outputs. However, different orderings of the recipient addresses
# will produce different generated outputs if sending to multiple silent payment addresses belonging to the
# same sender but with different labels. Because of this, expected["outputs"] contains all possible valid output sets,
# based on all possible permutations of recipient address orderings. Must match exactly one of the possible output sets.
assert(any(set(sending_outputs) == set(lst) for lst in expected["outputs"])), "Sending test failed"
else:
assert(sending_outputs == expected["outputs"][0] == []), "Sending test failed"
# Test receiving
msg = hashlib.sha256(b"message").digest()
aux = hashlib.sha256(b"random auxiliary data").digest()
for receiving_test in case["receiving"]:
given = receiving_test["given"]
expected = receiving_test["expected"]
outputs_to_check = [
ECPubKey().set(bytes.fromhex(p)) for p in given["outputs"]
]
vins = [
VinInfo(
outpoint=COutPoint(hash=deser_txid(input["txid"]), n=input["vout"]),
scriptSig=bytes.fromhex(input["scriptSig"]),
txinwitness=CTxInWitness().deserialize(from_hex(input["txinwitness"])),
prevout=bytes.fromhex(input["prevout"]["scriptPubKey"]["hex"]),
)
for input in given["vin"]
]
# Check that the given inputs for the receiving test match what was generated during the sending test
receiving_addresses = []
b_scan = ECKey().set(bytes.fromhex(given["key_material"]["scan_priv_key"]))
b_spend = ECKey().set(
bytes.fromhex(given["key_material"]["spend_priv_key"])
)
B_scan = b_scan.get_pubkey()
B_spend = b_spend.get_pubkey()
receiving_addresses.append(
encode_silent_payment_address(B_scan, B_spend, hrp="sp")
)
if given["labels"]:
for label in given["labels"]:
receiving_addresses.append(
create_labeled_silent_payment_address(
b_scan, B_spend, m=label, hrp="sp"
)
)
# Check that the silent payment addresses match for the given BIP32 seed and labels dictionary
assert (receiving_addresses == expected["addresses"]), "Receiving addresses don't match"
input_pub_keys = []
for vin in vins:
pubkey = get_pubkey_from_input(vin)
if not pubkey.valid:
continue
input_pub_keys.append(pubkey)
add_to_wallet = []
if (len(input_pub_keys) > 0):
A_sum = reduce(lambda x, y: x + y, input_pub_keys)
input_hash = get_input_hash([vin.outpoint for vin in vins], A_sum)
pre_computed_labels = {
(generate_label(b_scan, label) * G).get_bytes(False).hex(): generate_label(b_scan, label).hex()
for label in given["labels"]
}
add_to_wallet = scanning(
b_scan=b_scan,
B_spend=B_spend,
A_sum=A_sum,
input_hash=input_hash,
outputs_to_check=outputs_to_check,
labels=pre_computed_labels,
)
# Check that the private key is correct for the found output public key
for output in add_to_wallet:
pub_key = ECPubKey().set(bytes.fromhex(output["pub_key"]))
full_private_key = b_spend.add(bytes.fromhex(output["priv_key_tweak"]))
if full_private_key.get_pubkey().get_y() % 2 != 0:
full_private_key.negate()
sig = full_private_key.sign_schnorr(msg, aux)
assert pub_key.verify_schnorr(sig, msg), f"Invalid signature for {pub_key}"
output["signature"] = sig.hex()
# Note: order doesn't matter for creating/finding the outputs. However, different orderings of the recipient addresses
# will produce different generated outputs if sending to multiple silent payment addresses belonging to the
# same sender but with different labels. Because of this, expected["outputs"] contains all possible valid output sets,
# based on all possible permutations of recipient address orderings. Must match exactly one of the possible found output
# sets in expected["outputs"]
generated_set = {frozenset(d.items()) for d in add_to_wallet}
expected_set = {frozenset(d.items()) for d in expected["outputs"]}
assert generated_set == expected_set, "Receive test failed"
print("All tests passed")

696
bip-0352/secp256k1.py Normal file
View File

@ -0,0 +1,696 @@
# Copyright (c) 2019 Pieter Wuille
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test-only secp256k1 elliptic curve implementation
WARNING: This code is slow, uses bad randomness, does not properly protect
keys, and is trivially vulnerable to side channel attacks. Do not use for
anything but tests."""
import random
import hashlib
import hmac
def TaggedHash(tag, data):
ss = hashlib.sha256(tag.encode('utf-8')).digest()
ss += ss
ss += data
return hashlib.sha256(ss).digest()
def modinv(a, n):
"""Compute the modular inverse of a modulo n
See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers.
"""
t1, t2 = 0, 1
r1, r2 = n, a
while r2 != 0:
q = r1 // r2
t1, t2 = t2, t1 - q * t2
r1, r2 = r2, r1 - q * r2
if r1 > 1:
return None
if t1 < 0:
t1 += n
return t1
def jacobi_symbol(n, k):
"""Compute the Jacobi symbol of n modulo k
See http://en.wikipedia.org/wiki/Jacobi_symbol
For our application k is always prime, so this is the same as the Legendre symbol."""
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
n %= k
t = 0
while n != 0:
while n & 1 == 0:
n >>= 1
r = k & 7
t ^= (r == 3 or r == 5)
n, k = k, n
t ^= (n & k & 3 == 3)
n = n % k
if k == 1:
return -1 if t else 1
return 0
def modsqrt(a, p):
"""Compute the square root of a modulo p when p % 4 = 3.
The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm
Limiting this function to only work for p % 4 = 3 means we don't need to
iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd
is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4)
secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4.
"""
if p % 4 != 3:
raise NotImplementedError("modsqrt only implemented for p % 4 = 3")
sqrt = pow(a, (p + 1)//4, p)
if pow(sqrt, 2, p) == a % p:
return sqrt
return None
def int_or_bytes(s):
"Convert 32-bytes to int while accepting also int and returning it as is."
if isinstance(s, bytes):
assert(len(s) == 32)
s = int.from_bytes(s, 'big')
elif not isinstance(s, int):
raise TypeError
return s
class EllipticCurve:
def __init__(self, p, a, b):
"""Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p)."""
self.p = p
self.a = a % p
self.b = b % p
def affine(self, p1):
"""Convert a Jacobian point tuple p1 to affine form, or None if at infinity.
An affine point is represented as the Jacobian (x, y, 1)"""
x1, y1, z1 = p1
if z1 == 0:
return None
inv = modinv(z1, self.p)
inv_2 = (inv**2) % self.p
inv_3 = (inv_2 * inv) % self.p
return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1)
def has_even_y(self, p1):
"""Whether the point p1 has an even Y coordinate when expressed in affine coordinates."""
return not (p1[2] == 0 or self.affine(p1)[1] & 1)
def negate(self, p1):
"""Negate a Jacobian point tuple p1."""
x1, y1, z1 = p1
return (x1, (self.p - y1) % self.p, z1)
def on_curve(self, p1):
"""Determine whether a Jacobian tuple p is on the curve (and not infinity)"""
x1, y1, z1 = p1
z2 = pow(z1, 2, self.p)
z4 = pow(z2, 2, self.p)
return z1 != 0 and (pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p)) % self.p == 0
def is_x_coord(self, x):
"""Test whether x is a valid X coordinate on the curve."""
x_3 = pow(x, 3, self.p)
return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1
def lift_x(self, x):
"""Given an X coordinate on the curve, return a corresponding affine point."""
x_3 = pow(x, 3, self.p)
v = x_3 + self.a * x + self.b
y = modsqrt(v, self.p)
if y is None:
return None
return (x, y, 1)
def double(self, p1):
"""Double a Jacobian tuple p1
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling"""
x1, y1, z1 = p1
if z1 == 0:
return (0, 1, 0)
y1_2 = (y1**2) % self.p
y1_4 = (y1_2**2) % self.p
x1_2 = (x1**2) % self.p
s = (4*x1*y1_2) % self.p
m = 3*x1_2
if self.a:
m += self.a * pow(z1, 4, self.p)
m = m % self.p
x2 = (m**2 - 2*s) % self.p
y2 = (m*(s - x2) - 8*y1_4) % self.p
z2 = (2*y1*z1) % self.p
return (x2, y2, z2)
def add_mixed(self, p1, p2):
"""Add a Jacobian tuple p1 and an affine tuple p2
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)"""
x1, y1, z1 = p1
x2, y2, z2 = p2
assert(z2 == 1)
# Adding to the point at infinity is a no-op
if z1 == 0:
return p2
z1_2 = (z1**2) % self.p
z1_3 = (z1_2 * z1) % self.p
u2 = (x2 * z1_2) % self.p
s2 = (y2 * z1_3) % self.p
if x1 == u2:
if (y1 != s2):
# p1 and p2 are inverses. Return the point at infinity.
return (0, 1, 0)
# p1 == p2. The formulas below fail when the two points are equal.
return self.double(p1)
h = u2 - x1
r = s2 - y1
h_2 = (h**2) % self.p
h_3 = (h_2 * h) % self.p
u1_h_2 = (x1 * h_2) % self.p
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
y3 = (r*(u1_h_2 - x3) - y1*h_3) % self.p
z3 = (h*z1) % self.p
return (x3, y3, z3)
def add(self, p1, p2):
"""Add two Jacobian tuples p1 and p2
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition"""
x1, y1, z1 = p1
x2, y2, z2 = p2
# Adding the point at infinity is a no-op
if z1 == 0:
return p2
if z2 == 0:
return p1
# Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1
if z1 == 1:
return self.add_mixed(p2, p1)
if z2 == 1:
return self.add_mixed(p1, p2)
z1_2 = (z1**2) % self.p
z1_3 = (z1_2 * z1) % self.p
z2_2 = (z2**2) % self.p
z2_3 = (z2_2 * z2) % self.p
u1 = (x1 * z2_2) % self.p
u2 = (x2 * z1_2) % self.p
s1 = (y1 * z2_3) % self.p
s2 = (y2 * z1_3) % self.p
if u1 == u2:
if (s1 != s2):
# p1 and p2 are inverses. Return the point at infinity.
return (0, 1, 0)
# p1 == p2. The formulas below fail when the two points are equal.
return self.double(p1)
h = u2 - u1
r = s2 - s1
h_2 = (h**2) % self.p
h_3 = (h_2 * h) % self.p
u1_h_2 = (u1 * h_2) % self.p
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
y3 = (r*(u1_h_2 - x3) - s1*h_3) % self.p
z3 = (h*z1*z2) % self.p
return (x3, y3, z3)
def mul(self, ps):
"""Compute a (multi) point multiplication
ps is a list of (Jacobian tuple, scalar) pairs.
"""
r = (0, 1, 0)
for i in range(255, -1, -1):
r = self.double(r)
for (p, n) in ps:
if ((n >> i) & 1):
r = self.add(r, p)
return r
SECP256K1_FIELD_SIZE = 2**256 - 2**32 - 977
SECP256K1 = EllipticCurve(SECP256K1_FIELD_SIZE, 0, 7)
SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1)
SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2
NUMS_H = 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0
class ECPubKey():
"""A secp256k1 public key"""
def __init__(self):
"""Construct an uninitialized public key"""
self.valid = False
def __repr__(self):
return self.get_bytes().hex()
def __eq__(self, other):
assert isinstance(other, ECPubKey)
return self.get_bytes() == other.get_bytes()
def __hash__(self):
return hash(self.get_bytes())
def set(self, data):
"""Construct a public key from a serialization in compressed or uncompressed DER format or BIP340 format"""
if (len(data) == 65 and data[0] == 0x04):
p = (int.from_bytes(data[1:33], 'big'), int.from_bytes(data[33:65], 'big'), 1)
self.valid = SECP256K1.on_curve(p)
if self.valid:
self.p = p
self.compressed = False
elif (len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03)):
x = int.from_bytes(data[1:33], 'big')
if SECP256K1.is_x_coord(x):
p = SECP256K1.lift_x(x)
# if the oddness of the y co-ord isn't correct, find the other
# valid y
if (p[1] & 1) != (data[0] & 1):
p = SECP256K1.negate(p)
self.p = p
self.valid = True
self.compressed = True
else:
self.valid = False
elif (len(data) == 32):
x = int.from_bytes(data[0:32], 'big')
if SECP256K1.is_x_coord(x):
p = SECP256K1.lift_x(x)
# if the oddness of the y co-ord isn't correct, find the other
# valid y
if p[1]%2 != 0:
p = SECP256K1.negate(p)
self.p = p
self.valid = True
self.compressed = True
else:
self.valid = False
else:
self.valid = False
return self
@property
def is_compressed(self):
return self.compressed
@property
def is_valid(self):
return self.valid
def get_y(self):
return SECP256K1.affine(self.p)[1]
def get_x(self):
return SECP256K1.affine(self.p)[0]
def get_bytes(self, bip340=True):
assert(self.valid)
p = SECP256K1.affine(self.p)
if p is None:
return None
if bip340:
return bytes(p[0].to_bytes(32, 'big'))
elif self.compressed:
return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, 'big')
else:
return bytes([0x04]) + p[0].to_bytes(32, 'big') + p[1].to_bytes(32, 'big')
def verify_ecdsa(self, sig, msg, low_s=True):
"""Verify a strictly DER-encoded ECDSA signature against this pubkey.
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
ECDSA verifier algorithm"""
assert(self.valid)
# Extract r and s from the DER formatted signature. Return false for
# any DER encoding errors.
if (sig[1] + 2 != len(sig)):
return False
if (len(sig) < 4):
return False
if (sig[0] != 0x30):
return False
if (sig[2] != 0x02):
return False
rlen = sig[3]
if (len(sig) < 6 + rlen):
return False
if rlen < 1 or rlen > 33:
return False
if sig[4] >= 0x80:
return False
if (rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80)):
return False
r = int.from_bytes(sig[4:4+rlen], 'big')
if (sig[4+rlen] != 0x02):
return False
slen = sig[5+rlen]
if slen < 1 or slen > 33:
return False
if (len(sig) != 6 + rlen + slen):
return False
if sig[6+rlen] >= 0x80:
return False
if (slen > 1 and (sig[6+rlen] == 0) and not (sig[7+rlen] & 0x80)):
return False
s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big')
# Verify that r and s are within the group order
if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER:
return False
if low_s and s >= SECP256K1_ORDER_HALF:
return False
z = int.from_bytes(msg, 'big')
# Run verifier algorithm on r, s
w = modinv(s, SECP256K1_ORDER)
u1 = z*w % SECP256K1_ORDER
u2 = r*w % SECP256K1_ORDER
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)]))
if R is None or R[0] != r:
return False
return True
def verify_schnorr(self, sig, msg):
assert(len(msg) == 32)
assert(len(sig) == 64)
assert(self.valid)
r = int.from_bytes(sig[0:32], 'big')
if r >= SECP256K1_FIELD_SIZE:
return False
s = int.from_bytes(sig[32:64], 'big')
if s >= SECP256K1_ORDER:
return False
e = int.from_bytes(TaggedHash("BIP0340/challenge", sig[0:32] + self.get_bytes() + msg), 'big') % SECP256K1_ORDER
R = SECP256K1.mul([(SECP256K1_G, s), (self.p, SECP256K1_ORDER - e)])
if not SECP256K1.has_even_y(R):
return False
if ((r * R[2] * R[2]) % SECP256K1_FIELD_SIZE) != R[0]:
return False
return True
def __add__(self, other):
"""Adds two ECPubKey points."""
assert isinstance(other, ECPubKey)
assert self.valid
assert other.valid
ret = ECPubKey()
ret.p = SECP256K1.add(other.p, self.p)
ret.valid = True
ret.compressed = self.compressed
return ret
def __radd__(self, other):
"""Allows this ECPubKey to be added to 0 for sum()"""
if other == 0:
return self
else:
return self + other
def __mul__(self, other):
"""Multiplies ECPubKey point with a scalar(int/32bytes/ECKey)."""
if isinstance(other, ECKey):
assert self.valid
assert other.secret is not None
multiplier = other.secret
else:
# int_or_bytes checks that other is `int` or `bytes`
multiplier = int_or_bytes(other)
assert multiplier < SECP256K1_ORDER
multiplier = multiplier % SECP256K1_ORDER
ret = ECPubKey()
ret.p = SECP256K1.mul([(self.p, multiplier)])
ret.valid = True
ret.compressed = self.compressed
return ret
def __rmul__(self, other):
"""Multiplies a scalar(int/32bytes/ECKey) with an ECPubKey point"""
return self * other
def __sub__(self, other):
"""Subtract one point from another"""
assert isinstance(other, ECPubKey)
assert self.valid
assert other.valid
ret = ECPubKey()
ret.p = SECP256K1.add(self.p, SECP256K1.negate(other.p))
ret.valid = True
ret.compressed = self.compressed
return ret
def tweak_add(self, tweak):
assert(self.valid)
t = int_or_bytes(tweak)
if t >= SECP256K1_ORDER:
return None
tweaked = SECP256K1.affine(SECP256K1.mul([(self.p, 1), (SECP256K1_G, t)]))
if tweaked is None:
return None
ret = ECPubKey()
ret.p = tweaked
ret.valid = True
ret.compressed = self.compressed
return ret
def mul(self, data):
"""Multiplies ECPubKey point with scalar data."""
assert self.valid
other = ECKey()
other.set(data, True)
return self * other
def negate(self):
self.p = SECP256K1.affine(SECP256K1.negate(self.p))
def rfc6979_nonce(key):
"""Compute signing nonce using RFC6979."""
v = bytes([1] * 32)
k = bytes([0] * 32)
k = hmac.new(k, v + b"\x00" + key, 'sha256').digest()
v = hmac.new(k, v, 'sha256').digest()
k = hmac.new(k, v + b"\x01" + key, 'sha256').digest()
v = hmac.new(k, v, 'sha256').digest()
return hmac.new(k, v, 'sha256').digest()
class ECKey():
"""A secp256k1 private key"""
def __init__(self):
self.valid = False
def __repr__(self):
return str(self.secret)
def __eq__(self, other):
assert isinstance(other, ECKey)
return self.secret == other.secret
def __hash__(self):
return hash(self.secret)
def set(self, secret, compressed=True):
"""Construct a private key object from either 32-bytes or an int secret and a compressed flag."""
secret = int_or_bytes(secret)
self.valid = (secret > 0 and secret < SECP256K1_ORDER)
if self.valid:
self.secret = secret
self.compressed = compressed
return self
def generate(self, compressed=True):
"""Generate a random private key (compressed or uncompressed)."""
self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed)
return self
def get_bytes(self):
"""Retrieve the 32-byte representation of this key."""
assert(self.valid)
return self.secret.to_bytes(32, 'big')
def as_int(self):
return self.secret
def from_int(self, secret, compressed=True):
self.valid = (secret > 0 and secret < SECP256K1_ORDER)
if self.valid:
self.secret = secret
self.compressed = compressed
def __add__(self, other):
"""Add key secrets. Returns compressed key."""
assert isinstance(other, ECKey)
assert other.secret > 0 and other.secret < SECP256K1_ORDER
assert self.valid is True
ret_data = ((self.secret + other.secret) % SECP256K1_ORDER).to_bytes(32, 'big')
ret = ECKey()
ret.set(ret_data, True)
return ret
def __radd__(self, other):
"""Allows this ECKey to be added to 0 for sum()"""
if other == 0:
return self
else:
return self + other
def __sub__(self, other):
"""Subtract key secrets. Returns compressed key."""
assert isinstance(other, ECKey)
assert other.secret > 0 and other.secret < SECP256K1_ORDER
assert self.valid is True
ret_data = ((self.secret - other.secret) % SECP256K1_ORDER).to_bytes(32, 'big')
ret = ECKey()
ret.set(ret_data, True)
return ret
def __mul__(self, other):
"""Multiply a private key by another private key or multiply a public key by a private key. Returns compressed key."""
if isinstance(other, ECKey):
assert other.secret > 0 and other.secret < SECP256K1_ORDER
assert self.valid is True
ret_data = ((self.secret * other.secret) % SECP256K1_ORDER).to_bytes(32, 'big')
ret = ECKey()
ret.set(ret_data, True)
return ret
elif isinstance(other, ECPubKey):
return other * self
else:
# ECKey().set() checks that other is an `int` or `bytes`
assert self.valid
second = ECKey().set(other, self.compressed)
return self * second
def __rmul__(self, other):
return self * other
def add(self, data):
"""Add key to scalar data. Returns compressed key."""
other = ECKey()
other.set(data, True)
return self + other
def mul(self, data):
"""Multiply key secret with scalar data. Returns compressed key."""
other = ECKey()
other.set(data, True)
return self * other
def negate(self):
"""Negate a private key."""
assert self.valid
self.secret = SECP256K1_ORDER - self.secret
@property
def is_valid(self):
return self.valid
@property
def is_compressed(self):
return self.compressed
def get_pubkey(self):
"""Compute an ECPubKey object for this secret key."""
assert(self.valid)
ret = ECPubKey()
p = SECP256K1.mul([(SECP256K1_G, self.secret)])
ret.p = p
ret.valid = True
ret.compressed = self.compressed
return ret
def sign_ecdsa(self, msg, low_s=True, rfc6979=False):
"""Construct a DER-encoded ECDSA signature with this key.
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
ECDSA signer algorithm."""
assert(self.valid)
z = int.from_bytes(msg, 'big')
# Note: no RFC6979 by default, but a simple random nonce (some tests rely on distinct transactions for the same operation)
if rfc6979:
k = int.from_bytes(rfc6979_nonce(self.secret.to_bytes(32, 'big') + msg), 'big')
else:
k = random.randrange(1, SECP256K1_ORDER)
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)]))
r = R[0] % SECP256K1_ORDER
s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER
if low_s and s > SECP256K1_ORDER_HALF:
s = SECP256K1_ORDER - s
# Represent in DER format. The byte representations of r and s have
# length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33
# bytes).
rb = r.to_bytes((r.bit_length() + 8) // 8, 'big')
sb = s.to_bytes((s.bit_length() + 8) // 8, 'big')
return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb
def sign_schnorr(self, msg, aux=None):
"""Create a Schnorr signature (see BIP340)."""
if aux is None:
aux = bytes(32)
assert self.valid
assert len(msg) == 32
assert len(aux) == 32
t = (self.secret ^ int.from_bytes(TaggedHash("BIP0340/aux", aux), 'big')).to_bytes(32, 'big')
kp = int.from_bytes(TaggedHash("BIP0340/nonce", t + self.get_pubkey().get_bytes() + msg), 'big') % SECP256K1_ORDER
assert kp != 0
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, kp)]))
k = kp if SECP256K1.has_even_y(R) else SECP256K1_ORDER - kp
e = int.from_bytes(TaggedHash("BIP0340/challenge", R[0].to_bytes(32, 'big') + self.get_pubkey().get_bytes() + msg), 'big') % SECP256K1_ORDER
return R[0].to_bytes(32, 'big') + ((k + e * self.secret) % SECP256K1_ORDER).to_bytes(32, 'big')
def tweak_add(self, tweak):
"""Return a tweaked version of this private key."""
assert(self.valid)
t = int_or_bytes(tweak)
if t >= SECP256K1_ORDER:
return None
tweaked = (self.secret + t) % SECP256K1_ORDER
if tweaked == 0:
return None
ret = ECKey()
ret.set(tweaked.to_bytes(32, 'big'), self.compressed)
return ret
def generate_key_pair(secret=None, compressed=True):
"""Convenience function to generate a private-public key pair."""
d = ECKey()
if secret:
d.set(secret, compressed)
else:
d.generate(compressed)
P = d.get_pubkey()
return d, P
def generate_bip340_key_pair():
"""Convenience function to generate a BIP0340 private-public key pair."""
d = ECKey()
d.generate()
P = d.get_pubkey()
if P.get_y()%2 != 0:
d.negate()
P.negate()
return d, P
def generate_schnorr_nonce():
"""Generate a random valid BIP340 nonce.
See https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki.
This implementation ensures the y-coordinate of the nonce point is even."""
kp = random.randrange(1, SECP256K1_ORDER)
assert kp != 0
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, kp)]))
k = kp if R[1] % 2 == 0 else SECP256K1_ORDER - kp
k_key = ECKey()
k_key.set(k.to_bytes(32, 'big'), True)
return k_key

File diff suppressed because it is too large Load Diff