Skip to content

Commit

Permalink
Change taproot functions (#153)
Browse files Browse the repository at this point in the history
* changed name from bip340 to xonly
* changing how taproot is done
* fixed formatting
  • Loading branch information
jimmysong authored Nov 10, 2023
1 parent 67dc0de commit c0b7d57
Show file tree
Hide file tree
Showing 24 changed files with 489 additions and 427 deletions.
2 changes: 1 addition & 1 deletion buidl/blinding.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def secure_secret_path(depth=4):
8: 248
9: 279
"""
if type(depth) != int:
if not isinstance(depth, int):
raise ValueError(f"depth must be an int: {depth}")
if depth >= 32:
raise ValueError(
Expand Down
67 changes: 51 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 @@ -134,8 +141,8 @@ def sec(self, compressed=True):
self.usec = bytes(ffi.buffer(serialized, 65))
return self.usec

def bip340(self):
# returns the binary version of BIP340 pubkey
def xonly(self):
# returns the binary version of XONLY pubkey
xonly_key = ffi.new("secp256k1_xonly_pubkey *")
if not lib.secp256k1_xonly_pubkey_from_pubkey(
GLOBAL_CTX, xonly_key, ffi.NULL, self.c
Expand All @@ -146,6 +153,24 @@ def bip340(self):
raise RuntimeError("libsecp256k1 xonly serialize error")
return bytes(ffi.buffer(output32, 32))

def tweak(self, merkle_root=b""):
"""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"", tweak=None):
"""Creates the tweaked external key for a particular merkle root/tweak."""
# Get the tweak with the 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.even_point() + t
# return the external key
return external_key

def hash160(self, compressed=True):
# get the sec
sec = self.sec(compressed)
Expand All @@ -172,12 +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.taproot import TapRoot
from buidl.script import P2TRScriptPubKey

return TapRoot(self).script_pubkey()
return P2TRScriptPubKey(external_pubkey)

def p2pk_tap_script(self):
"""Returns the p2tr Script object"""
Expand All @@ -198,9 +224,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 All @@ -214,9 +240,9 @@ def verify_message(self, message, sig):

@classmethod
def parse(cls, binary):
"""returns a Point object from a SEC or BIP340 pubkey"""
"""returns a Point object from a SEC or XONLY pubkey"""
if len(binary) == 32:
return cls.parse_bip340(binary)
return cls.parse_xonly(binary)
elif len(binary) in (33, 65):
return cls.parse_sec(binary)
else:
Expand All @@ -231,7 +257,7 @@ def parse_sec(cls, sec_bin):
return cls(csec=sec_bin)

@classmethod
def parse_bip340(cls, binary):
def parse_xonly(cls, binary):
sec_bin = b"\x02" + binary
return cls(csec=sec_bin)

Expand Down Expand Up @@ -348,6 +374,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 @@ -435,12 +467,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: 0 additions & 1 deletion buidl/compactfilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,6 @@ def parse(cls, s):


class GetCFCheckPointMessage:

command = b"getcfcheckpt"
define_network = False

Expand Down
1 change: 0 additions & 1 deletion buidl/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ def parse_any_key_record(key_record_str):


class P2WSHSortedMulti:

# TODO: make an inheritable base descriptor class that this inherits from

def __init__(
Expand Down
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
7 changes: 6 additions & 1 deletion buidl/libsec_build.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,10 @@

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)
6 changes: 3 additions & 3 deletions buidl/mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def secure_mnemonic(num_bits=256, extra_entropy=0):
"""
if num_bits not in (128, 160, 192, 224, 256):
raise ValueError(f"Invalid num_bits: {num_bits}")
if type(extra_entropy) is not int:
if not isinstance(extra_entropy, int):
raise TypeError(f"extra_entropy must be an int: {extra_entropy}")
if extra_entropy < 0:
raise ValueError(f"extra_entropy cannot be negative: {extra_entropy}")
Expand Down Expand Up @@ -115,9 +115,9 @@ def __init__(self, filename, num_words):
self.lookup[word[:4]] = i

def __getitem__(self, key):
if type(key) == str:
if isinstance(key, str):
return self.lookup[key]
elif type(key) == int:
elif isinstance(key, int):
return self.words[key]
else:
raise KeyError("key needs to be a str or int")
Expand Down
4 changes: 2 additions & 2 deletions buidl/op.py
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,7 @@ def op_checksig_schnorr(stack, tx_obj, input_index):
return False
pubkey = stack.pop()
signature = stack.pop()
point = S256Point.parse_bip340(pubkey)
point = S256Point.parse_xonly(pubkey)
if len(signature) == 65:
hash_type = signature[-1]
signature = signature[:-1]
Expand Down Expand Up @@ -777,7 +777,7 @@ def op_checksigadd_schnorr(stack, tx_obj, input_index):
pubkey = stack.pop()
n = decode_num(stack.pop())
signature = stack.pop()
point = S256Point.parse_bip340(pubkey)
point = S256Point.parse_xonly(pubkey)
if len(signature) == 65:
hash_type = signature[-1]
signature = signature[:-1]
Expand Down
Loading

0 comments on commit c0b7d57

Please sign in to comment.