Skip to content

Commit

Permalink
changing how taproot is done
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmysong committed Oct 31, 2023
1 parent 53722a5 commit 55a7c13
Show file tree
Hide file tree
Showing 10 changed files with 366 additions and 337 deletions.
49 changes: 33 additions & 16 deletions buidl/cecc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import hmac
import secrets

from buidl.hash import hash_taptweak
from buidl.helper import (
big_endian_to_int,
encode_base58_checksum,
Expand Down Expand Up @@ -86,6 +87,12 @@ def __add__(self, scalar):
raise RuntimeError("libsecp256k1 serialize error")
return self.__class__(usec=bytes(serialized))

def even_point(self):
if self.parity:
return -1 * self
else:
return self

def verify(self, z, sig):
msg = int_to_big_endian(z, 32)
sig_data = sig.cdata()
Expand Down Expand Up @@ -147,19 +154,20 @@ def xonly(self):
return bytes(ffi.buffer(output32, 32))

def tweak(self, merkle_root=b""):
"""returns the tweak for use in p2tr if there's no script path"""
"""returns the tweak for use in p2tr"""
# take the hash_taptweak of the xonly and the merkle root
tweak = hash_taptweak(self.xonly() + merkle_root)
return tweak

def tweaked_key(self, merkle_root=b""):
"""Creates the tweaked external key for a particular tweak."""
def tweaked_key(self, merkle_root=b"", tweak=None):
"""Creates the tweaked external key for a particular merkle root/tweak."""
# Get the tweak with the merkle root
tweak = self.tweak(merkle_root)
if tweak is None:
tweak = self.tweak(merkle_root)
# t is the tweak interpreted as a big endian integer
t = big_endian_to_int(tweak)
# Q = P + tG
external_key = self + t
external_key = self.even_point() + t
# return the external key
return external_key

Expand Down Expand Up @@ -189,13 +197,13 @@ def p2sh_p2wpkh_redeem_script(self):
"""Returns the RedeemScript for a p2sh-p2wpkh redemption"""
return self.p2wpkh_script().redeem_script()

def p2tr_script(self):
def p2tr_script(self, merkle_root=b"", tweak=None):
"""Returns the p2tr Script object"""
external_pubkey = self.tweaked_key(merkle_root, tweak)
# avoid circular dependency
from buidl.script import P2TRScriptPubKey

external_pubkey = self.tweaked_key(merkle_root)
return P2TRScriptPubKey(self)
return P2TRScriptPubKey(external_pubkey)

def p2pk_tap_script(self):
"""Returns the p2tr Script object"""
Expand All @@ -215,9 +223,9 @@ def p2sh_p2wpkh_address(self, network="mainnet"):
"""Returns the p2sh-p2wpkh base58 address string"""
return self.p2wpkh_script().p2sh_address(network)

def p2tr_address(self, network="mainnet"):
def p2tr_address(self, merkle_root=b"", tweak=None, network="mainnet"):
"""Returns the p2tr bech32m address string"""
return self.p2tr_script().address(network)
return self.p2tr_script(merkle_root, tweak).address(network)

def verify_message(self, message, sig):
"""Verify a message in the form of bytes. Assumes that the z
Expand Down Expand Up @@ -365,6 +373,12 @@ def __init__(self, secret, network="mainnet", compressed=True):
def hex(self):
return "{:x}".format(self.secret).zfill(64)

def even_secret(self):
if self.point.parity:
return N - self.secret
else:
return self.secret

def sign(self, z):
# per libsecp256k1 documentation, this helps against side-channel attacks
if not lib.secp256k1_context_randomize(
Expand Down Expand Up @@ -452,12 +466,15 @@ def wif(self, compressed=True):
# encode_base58_checksum the whole thing
return encode_base58_checksum(prefix + secret_bytes + suffix)

def tweaked(self, tweak):
if self.point.parity:
s = N - self.secret
else:
s = self.secret
new_secret = (s + tweak) % N
def tweaked_key(self, merkle_root=b""):
e = self.even_secret()
# get the tweak from the point's tweak method
tweak = self.point.tweak(merkle_root)
# t is the tweak interpreted as big endian
t = big_endian_to_int(tweak)
# new secret is the secret plus t (make sure to mod by N)
new_secret = (e + t) % N
# create a new instance of this class using self.__class__
return self.__class__(new_secret, network=self.network)

@classmethod
Expand Down
1 change: 1 addition & 0 deletions buidl/ecc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
try:
raise ModuleNotFoundError
from buidl.cecc import * # noqa: F401,F403
except ModuleNotFoundError:
from buidl.pecc import * # noqa: F401,F403
6 changes: 3 additions & 3 deletions buidl/hd.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,9 +332,8 @@ def get_private_key(self, purpose, account_num=0, is_external=True, address_num=
return hd_priv.private_key

def _get_address(self, purpose, account_num=0, is_external=True, address_num=0):
"""Returns the proper address among purposes 44', 49' and 84'.
p2pkh for 44', p2sh-p2wpkh for 49' and p2wpkh for 84'."""
# if purpose is not one of 44', 49' or 84', raise ValueError
"""Returns the proper address among purposes 44', 49', 84' and 86'.
p2pkh for 44', p2sh-p2wpkh for 49', p2wpkh for 84', and p2tr for 86'."""
point = self.get_private_key(
purpose=purpose,
account_num=account_num,
Expand All @@ -353,6 +352,7 @@ def _get_address(self, purpose, account_num=0, is_external=True, address_num=0):
# if 86', return the p2tr_address
elif purpose == "86'":
return point.p2tr_address(network=self.network)
# if purpose is not one of 44', 49', 84' or 86', raise ValueError
else:
raise ValueError(
f"Cannot create an address without a proper purpose: {purpose}"
Expand Down
2 changes: 1 addition & 1 deletion buidl/libsec_build.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@

ffi = FFI()
ffi.cdef(source)
ffi.set_source("_libsec", header, libraries=["secp256k1"])
ffi.set_source("_libsec", header, libraries=["secp256k1"], include_dirs=["/opt/homebrew/Cellar/libsecp256k1/0.1/include"])
ffi.compile(verbose=True)
88 changes: 63 additions & 25 deletions buidl/pecc.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
hash_aux,
hash_challenge,
hash_nonce,
hash_taptweak,
)
from buidl.helper import (
big_endian_to_int,
Expand Down Expand Up @@ -248,6 +249,12 @@ def __add__(self, other):
else:
return super().__add__(other)

def even_point(self):
if self.parity:
return -1 * self
else:
return self

def sec(self, compressed=True):
# returns the binary version of the sec format, NOT hex
# if compressed, starts with b'\x02' if self.y.num is even, b'\x03' if self.y is odd
Expand Down Expand Up @@ -276,14 +283,15 @@ def tweak(self, merkle_root=b""):
tweak = hash_taptweak(self.xonly() + merkle_root)
return tweak

def tweaked_key(self, merkle_root=b""):
def tweaked_key(self, merkle_root=b"", tweak=None):
"""Creates the tweaked external key for a particular tweak."""
# Get the tweak with the merkle root
tweak = self.tweak(merkle_root)
if tweak is None:
tweak = self.tweak(merkle_root)
# t is the tweak interpreted as a big endian integer
t = big_endian_to_int(tweak)
# Q = P + tG
external_key = self + t
external_key = self.even_point() + t
# return the external key
return external_key

Expand Down Expand Up @@ -313,13 +321,13 @@ def p2sh_p2wpkh_redeem_script(self):
"""Returns the RedeemScript for a p2sh-p2wpkh redemption"""
return self.p2wpkh_script().redeem_script()

def p2tr_script(self):
def p2tr_script(self, merkle_root=b"", tweak=None):
"""Returns the p2tr Script object"""
external_pubkey = self.tweaked_key(merkle_root, tweak)
# avoid circular dependency
from buidl.script import P2TRScriptPubKey

external_pubkey = self.tweaked_key(merkle_root)
return P2TRScriptPubKey(self)
return P2TRScriptPubKey(external_pubkey)

def p2pk_tap_script(self):
"""Returns the p2tr Script object"""
Expand All @@ -340,9 +348,9 @@ def p2sh_p2wpkh_address(self, network="mainnet"):
"""Returns the p2sh-p2wpkh base58 address string"""
return self.p2wpkh_script().p2sh_address(network)

def p2tr_address(self, network="mainnet"):
def p2tr_address(self, merkle_root=b"", tweak=None, network="mainnet"):
"""Returns the p2tr bech32m address string"""
return self.p2tr_script().address(network)
return self.p2tr_script(merkle_root, tweak).address(network)

def verify(self, z, sig):
# remember sig.r and sig.s are the main things we're checking
Expand Down Expand Up @@ -555,28 +563,55 @@ def sign(self, z):
# Signature(r, s)
return Signature(r, s)

def sign_schnorr(self, msg, aux):
def even_secret(self):
if self.point.parity:
d = N - self.secret
return N - self.secret
else:
d = self.secret
return self.secret

def bip340_k(self, msg, aux=None):
# k is generated using the aux variable, which can be set
# to a known value to make k deterministic
# the idea of k generation here is to mix in the private key
# and the msg to ensure it's unique and not reused
if aux is None:
aux = b"\x00" * 32
e = self.even_secret()
if len(msg) != 32:
raise ValueError("msg needs to be 32 bytes")
if len(aux) != 32:
raise ValueError("aux needs to be 32 bytes")
t = xor_bytes(int_to_big_endian(d, 32), hash_aux(aux))
k = big_endian_to_int(hash_nonce(t + self.point.xonly() + msg)) % N
# t contains the secret, msg is added so it's unique to the
# message and private key
t = xor_bytes(int_to_big_endian(e, 32), hash_aux(aux))
return big_endian_to_int(hash_nonce(t + self.point.xonly() + msg)) % N

def sign_schnorr(self, msg, aux=None):
# e is the secret that generates an even y with the even_secret method
e = self.even_secret()
# get k using the self.bip340_k method
k = self.bip340_k(msg, aux)
# get the resulting R=kG point
r = k * G
# if R's y coordinate is odd (use parity property), flip the k
if r.parity:
# set k to N - k
k = N - k
# recalculate R
r = k * G
message = r.xonly() + self.point.xonly() + msg
e = big_endian_to_int(hash_challenge(message)) % N
s = (k + e * d) % N
sig = SchnorrSignature(r, s)
if not self.point.verify_schnorr(msg, sig):
# calculate the commitment which is: R || P || msg
commitment = r.xonly() + self.point.xonly() + msg
# h is hash_challenge of the commitment as a big endian integer mod N
h = big_endian_to_int(hash_challenge(commitment)) % N
# calculate s which is (k+eh) mod N
s = (k + e * h) % N
# create a SchnorrSignature object using the R and s
schnorr = SchnorrSignature(r, s)
# check that this schnorr signature verifies
if not self.point.verify_schnorr(msg, schnorr):
raise RuntimeError("Bad Signature")
return sig
# return the signature
return schnorr

def deterministic_k(self, z):
k = b"\x00" * 32
Expand Down Expand Up @@ -625,12 +660,15 @@ def wif(self, compressed=True):
# encode_base58_checksum the whole thing
return encode_base58_checksum(prefix + secret_bytes + suffix)

def tweaked(self, tweak):
if self.point.parity:
s = N - self.secret
else:
s = self.secret
new_secret = (s + tweak) % N
def tweaked_key(self, merkle_root=b""):
e = self.even_secret()
# get the tweak from the point's tweak method
tweak = self.point.tweak(merkle_root)
# t is the tweak interpreted as big endian
t = big_endian_to_int(tweak)
# new secret is the secret plus t (make sure to mod by N)
new_secret = (e + t) % N
# create a new instance of this class using self.__class__
return self.__class__(new_secret, network=self.network)

@classmethod
Expand Down
4 changes: 2 additions & 2 deletions buidl/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ def evaluate(self, tx_obj, input_index):
elif len(witness) > 1:
# this is a script path spend
control_block = witness.control_block()
tap_leaf = witness.tap_leaf()
tweak_point = control_block.tweak_point(tap_leaf)
tap_script = witness.tap_script()
tweak_point = control_block.external_pubkey(tap_script)
# the tweak point should be what's on the stack
if tweak_point.parity != control_block.parity:
print("bad tweak point parity")
Expand Down
Loading

0 comments on commit 55a7c13

Please sign in to comment.