-
Notifications
You must be signed in to change notification settings - Fork 26
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
WIP implementation of BIP322 #140
Open
wip-abramson
wants to merge
5
commits into
buidl-bitcoin:main
Choose a base branch
from
LegReq:feature/bip322
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,3 +21,4 @@ | |
from .taproot import * | ||
from .tx import * | ||
from .witness import * | ||
from .message import * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
from buidl.tx import Tx, TxIn, TxOut | ||
from buidl.script import Script,P2WPKHScriptPubKey | ||
from buidl.helper import big_endian_to_int, base64_decode, base64_encode, str_to_bytes | ||
|
||
from buidl.script import address_to_script_pubkey | ||
from buidl.hash import hash_bip322message | ||
from buidl.witness import Witness | ||
|
||
from buidl.ecc import PrivateKey | ||
|
||
import io | ||
from enum import Enum | ||
|
||
|
||
class MessageSignatureFormat(Enum): | ||
LEGACY = 0 | ||
SIMPLE = 1 | ||
FULL = 2 | ||
|
||
################################################################ | ||
# | ||
# From BIP322 | ||
# The to_spend transaction is: | ||
# | ||
# nVersion = 0 | ||
# nLockTime = 0 | ||
# vin[0].prevout.hash = 0000...000 | ||
# vin[0].prevout.n = 0xFFFFFFFF | ||
# vin[0].nSequence = 0 | ||
# vin[0].scriptSig = OP_0 PUSH32[ message_hash ] | ||
# vin[0].scriptWitness = [] | ||
# vout[0].nValue = 0 | ||
# vout[0].scriptPubKey = message_challenge | ||
# | ||
def create_to_spend_tx(address, message): | ||
# Not a valid Tx hash. Will never be spendable on any BTC network. | ||
prevout_hash = bytes.fromhex('0000000000000000000000000000000000000000000000000000000000000000') | ||
# prevout.n | ||
prevout_index = 0xffffffff | ||
|
||
sequence = 0 | ||
|
||
b_msg = str_to_bytes(message) | ||
message_hash = hash_bip322message(b_msg) | ||
|
||
# Note BIP322 to_spend scriptSig commands = [0, 32, message_hash] | ||
# PUSH32 is implied and added by the size of the message added to the stack | ||
commands = [0, message_hash] | ||
script_sig = Script(commands) | ||
# Create Tx Input | ||
tx_in = TxIn(prevout_hash,prevout_index,script_sig,sequence) | ||
|
||
# Value of tx output | ||
value = 0 | ||
|
||
# Convert address to a ScriptPubKey | ||
script_pubkey = None | ||
try: | ||
script_pubkey = address_to_script_pubkey(address) | ||
except: | ||
raise ValueError("Invalid address") | ||
|
||
tx_out = TxOut(value,script_pubkey) | ||
|
||
# create transaction | ||
version=0 | ||
tx_inputs = [tx_in] | ||
tx_outputs = [tx_out] | ||
locktime=0 | ||
network="mainnet" | ||
|
||
# TODO: Should this always be True? What about for FULL BIP322 with p2pkh? | ||
segwit=True | ||
|
||
return Tx(version,tx_inputs,tx_outputs,locktime,network,segwit) | ||
|
||
################################################################################### | ||
# | ||
# From BIP322 | ||
# The to_sign Tx is: | ||
# nVersion = 0 or (FULL format only) as appropriate (e.g. 2, for time locks) | ||
# nLockTime = 0 or (FULL format only) as appropriate (for time locks) | ||
# vin[0].prevout.hash = to_spend.txid | ||
# vin[0].prevout.n = 0 | ||
# vin[0].nSequence = 0 or (FULL format only) as appropriate (for time locks) | ||
# vin[0].scriptWitness = message_signature | ||
# vout[0].nValue = 0 | ||
# vout[0].scriptPubKey = OP_RETURN | ||
# | ||
def create_to_sign_tx(to_spend_tx_hash, sig_bytes=None): | ||
|
||
if (sig_bytes and is_full_signature(sig_bytes)): | ||
|
||
sig_stream = io.BytesIO(sig_bytes) | ||
to_sign = Tx.parse(sig_stream) | ||
|
||
if (len(to_sign.tx_ins) > 1): | ||
# TODO: Implement Proof of Funds | ||
raise NotImplemented("Not yet implemented proof of funds yet") | ||
elif (len(to_sign.tx_ins) == 0): | ||
raise ValueError("No transaction input") | ||
elif (to_sign.tx_ins[0].prev_tx != to_spend_tx_hash): | ||
raise ValueError("The to_sign transaction input's prevtx id does not equal the calculated to_spend transaction id") | ||
elif (len(to_sign.tx_outs) != 1): | ||
raise ValueError("to_sign does not have a single TxOutput") | ||
elif (to_sign.tx_outs[0].amount != 0): | ||
raise ValueError("Value is Non 0", to_sign.tx_outs[0].amount) | ||
elif(to_sign.tx_outs[0].script_pubkey.commands != [106]): | ||
raise ValueError("ScriptPubKey incorrect", to_sign.tx_outs[0].script_pubkey) | ||
else: | ||
return to_sign | ||
|
||
else: | ||
# signature is either None or an encoded witness stack | ||
|
||
# Identifies the index of the output from the virtual to_spend tx to be "spent" | ||
prevout_index = 0 | ||
sequence = 0 | ||
# TxInput identifies the single output from the to_spend Tx | ||
tx_input = TxIn(to_spend_tx_hash,prevout_index,script_sig=None,sequence=sequence) | ||
|
||
value = 0 | ||
# OP Code 106 for OP_RETURN | ||
commands = [106] | ||
scriptPubKey = Script(commands) | ||
|
||
tx_output = TxOut(value,scriptPubKey) | ||
locktime=0 | ||
version=0 | ||
tx_inputs = [tx_input] | ||
tx_outputs = [tx_output] | ||
network="mainnet" | ||
# is to_sign always a segwit? | ||
segwit=True | ||
|
||
# create unsigned to_sign transaction | ||
to_sign_tx = Tx(version,tx_inputs,tx_outputs,locktime,network,segwit) | ||
|
||
if sig_bytes: | ||
try: | ||
stream = io.BytesIO(sig_bytes) | ||
witness = Witness.parse(stream) | ||
# Set the witness on the to_sign tx input | ||
to_sign_tx.tx_ins[0].witness = witness | ||
except: | ||
# TODO: Fall back to legacy ... | ||
print("Signature is niether an encoded witness or full transaction. Fall back to legacy") | ||
return None | ||
|
||
return to_sign_tx | ||
|
||
# Test if sig_bytes can be decoded to a transaction | ||
# TODO: Is there a better way to test than this? | ||
def is_full_signature(sig_bytes): | ||
try: | ||
sig_stream = io.BytesIO(sig_bytes) | ||
|
||
Tx.parse(sig_stream) | ||
# TODO: more specific exception handling | ||
except: | ||
return False | ||
return True | ||
|
||
###################################################################################### | ||
# | ||
# The sign_message function produces a signature on message in one of three formats | ||
# | ||
# Inputs | ||
# - format: enum MessageSignatureFormat (Either LEGACY, SIMPLE or FULL) | ||
# - private_key: The private key that is used to sign the message | ||
# - address: String representation of a bitcoin address controlled by the private_key | ||
# - message: String representation of a message to be signed | ||
# | ||
# Output: A Base64 encoding of one of three signature formats: | ||
# - LEGACY: A EcDSA signature and its associated public key | ||
# - SIMPLE: The witness stack from the signed BIP322 to_sign transaction | ||
# - FULL: The full signed BIP322 to_sign transaction in standard network serialization | ||
# | ||
# The outputted signature can be verified by passing to the verify_message function | ||
def sign_message(format, private_key, address, message): | ||
|
||
if (format != MessageSignatureFormat.LEGACY): | ||
return sign_message_bip322(format,private_key,address,message) | ||
|
||
script_pubkey = address_to_script_pubkey(address) | ||
|
||
if (not script_pubkey.is_p2pkh()): | ||
raise ValueError("Address must be p2pkh for LEGACY signatures") | ||
|
||
# TODO: This legacy signing needs to produce a compact encoding of signature AND public key | ||
# Needs implementing in the library I believe? | ||
raise NotImplementedError("Legacy signing not yet implemented. Require compact encoding of sig and pubkey") | ||
# b_msg = str_to_bytes(message) | ||
# signature = private_key.sign_message(b_msg) | ||
|
||
# return base64_encode(signature.der()) | ||
|
||
def sign_message_bip322(format, private_key, address, message): | ||
|
||
assert format != MessageSignatureFormat.LEGACY, "BIP 322 Signatures must be represented in either the Simple of Full Formats" | ||
|
||
to_spend = create_to_spend_tx(address, message) | ||
to_sign = create_to_sign_tx(to_spend.hash(), None) | ||
|
||
to_sign.tx_ins[0]._script_pubkey = to_spend.tx_outs[0].script_pubkey | ||
to_sign.tx_ins[0]._value = to_spend.tx_outs[0].amount | ||
|
||
sig_ok = to_sign.sign_input(0, private_key) | ||
|
||
# Force the format to FULL, to_sign tx signed using a p2pkh scriptPubKey | ||
if (len(to_sign.tx_ins[0].script_sig.commands) > 0 or len(to_sign.tx_ins[0].witness.items) == 0): | ||
format = MessageSignatureFormat.FULL | ||
|
||
if (not sig_ok): | ||
# TODO: this may be a multisig which successfully signed but needed additional signatures | ||
raise RuntimeError("Unable to sign message") | ||
|
||
if (format == MessageSignatureFormat.SIMPLE): | ||
return base64_encode(to_sign.serialize_witness()) | ||
else: | ||
return base64_encode(to_sign.serialize()) | ||
|
||
|
||
|
||
######################################################################################## | ||
# | ||
# Verifies a signature on a message against a specific bitcoin address | ||
# | ||
# Inputs: | ||
# - address: String representation of a bitcoin address | ||
# - signature: Base64 encoded BIP322 signature in either LEGACY,SIMPLE or FULL format | ||
# - message: The message to verify the signature against | ||
# | ||
# Outputs True or False | ||
def verify_message(address, signature, message): | ||
try: | ||
sig_bytes = base64_decode(signature) | ||
except: | ||
raise ValueError("Signature is not base64 encoded") | ||
|
||
to_spend = create_to_spend_tx(address, message) | ||
to_sign = create_to_sign_tx(to_spend.hash(), sig_bytes) | ||
|
||
if to_sign == None: | ||
# try LEGACY | ||
# Check address is a p2pkh | ||
# Recover Secp256 point from BIP322 signature? | ||
# Verify signature | ||
raise NotImplementedError("Unable to verify LEGACY signatures currently") | ||
|
||
to_sign.tx_ins[0]._script_pubkey = to_spend.tx_outs[0].script_pubkey | ||
to_sign.tx_ins[0]._value = to_spend.tx_outs[0].amount | ||
return to_sign.verify_input(0) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,13 @@ | ||
from unittest import TestCase | ||
|
||
from buidl.hash import hash_keyaggcoef | ||
from buidl.hash import hash_keyaggcoef,hash_bip322message | ||
|
||
|
||
class HashTest(TestCase): | ||
def test_keyaggcoef(self): | ||
want = "55a02026378a033a97431c5ac6a72eeec43069940a330431216895c11eff3cc7" | ||
self.assertEqual(hash_keyaggcoef(b"").hex(), want) | ||
|
||
def test_bip322message(self): | ||
want = "c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1" | ||
self.assertEqual(hash_bip322message(b"").hex(), want) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have run into some issues with the current hard coded
segwit=True
that is set of theto_sign
transaction. Currently, I am attempting to create a bip322 p2sh multisig.@jimmysong is there a recommended way for me to test whether this to_sign transation should have it's segwit flag set to True or not.
It is to do with the scriptPubKey on the tx_out of the to_sign transaction right?