You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
171 lines
6.5 KiB
171 lines
6.5 KiB
# Implementation of proposal as per |
|
# https://gist.github.com/AdamISZ/2c13fb5819bd469ca318156e2cf25d79 |
|
# (BIP SNICKER) |
|
# TODO: BIP69 is removed in this implementation, will update BIP draft. |
|
|
|
from jmbitcoin.secp256k1_ecies import * |
|
from jmbitcoin.secp256k1_main import * |
|
from jmbitcoin.secp256k1_transaction import * |
|
from collections import Counter |
|
|
|
from bitcointx.core.key import CKey, CPubKey |
|
from bitcointx.wallet import CCoinAddressError |
|
|
|
SNICKER_MAGIC_BYTES = b'SNICKER' |
|
|
|
# Flags may be added in future versions |
|
SNICKER_FLAG_NONE = b"\x00" |
|
|
|
def snicker_pubkey_tweak(pub, tweak): |
|
""" use secp256k1 library to perform tweak. |
|
Both `pub` and `tweak` are expected as byte strings |
|
(33 and 32 bytes respectively). |
|
Return value is also a 33 byte string serialization |
|
of the resulting pubkey (compressed). |
|
""" |
|
base_pub = CPubKey(pub) |
|
# convert the tweak to a new pubkey |
|
tweak_pub = CKey(tweak, compressed=True).pub |
|
return add_pubkeys([base_pub, tweak_pub]) |
|
|
|
def snicker_privkey_tweak(priv, tweak): |
|
""" use secp256k1 library to perform tweak. |
|
Both `priv` and `tweak` are expected as byte strings |
|
(32 or 33 and 32 bytes respectively). |
|
Return value isa 33 byte string serialization |
|
of the resulting private key/secret (with compression flag). |
|
""" |
|
if len(priv) == 32: |
|
priv += b"\x01" |
|
if len(tweak) == 32: |
|
tweak += b"\x01" |
|
assert priv[-1] == 1 |
|
assert tweak[-1] == 1 |
|
return add_privkeys(priv, tweak) |
|
|
|
def verify_snicker_output(tx, pub, tweak, spk_type="p2wpkh"): |
|
""" A convenience function to check that one output address in a transaction |
|
is a SNICKER-type tweak of an existing key. Returns the index of the output |
|
for which this is True (and there must be only 1), and the derived spk, |
|
or -1 and None if it is not found exactly once. |
|
Only standard segwit spk types (as used in Joinmarket) are supported. |
|
""" |
|
assert isinstance(tx, btc.CTransaction) |
|
expected_destination_pub = snicker_pubkey_tweak(pub, tweak) |
|
if spk_type == "p2wpkh": |
|
expected_destination_spk = pubkey_to_p2wpkh_script( |
|
expected_destination_pub) |
|
elif spk_type == "p2sh-p2wpkh": |
|
expected_destination_spk = pubkey_to_p2sh_p2wpkh_script( |
|
expected_destination_pub) |
|
else: |
|
assert False, "JM SNICKER only supports p2sh/p2wpkh" |
|
found = 0 |
|
for i, o in enumerate(tx.vout): |
|
if o.scriptPubKey == expected_destination_spk: |
|
found += 1 |
|
found_index = i |
|
if found != 1: |
|
return -1, None |
|
return found_index, expected_destination_spk |
|
|
|
def construct_snicker_outputs(proposer_input_amount, receiver_input_amount, |
|
receiver_addr, proposer_addr, change_addr, |
|
network_fee, net_transfer): |
|
""" This is abstracted from full SNICKER transaction proposal (see |
|
`jmclient.wallet.SNICKERWalletMixin`) construction, as it is also useful |
|
for making fake SNICKERs. |
|
total_input_amount (int) : value of sum of inputs in sats |
|
receiver_input_amount (int): value of single utxo input of receiver in sats |
|
receiver_addr (str): address for tweaked destination of receiver |
|
proposer_addr (str): address for proposer's coinjoin output |
|
change_addr (str): address for proposer's change output |
|
network_fee (int): bitcoin network transaction fee in sats |
|
net_transfer (int): how much the proposer gives to the receiver in sats |
|
|
|
Returns: |
|
list of outputs, each is of form {"address": x, "value": y} |
|
""" |
|
total_input_amount = proposer_input_amount + receiver_input_amount |
|
total_output_amount = total_input_amount - network_fee |
|
receiver_output_amount = receiver_input_amount + net_transfer |
|
proposer_output_amount = total_output_amount - receiver_output_amount |
|
change_output_amount = total_output_amount - 2 * receiver_output_amount |
|
# callers should only request sane values: |
|
assert all([x>0 for x in [receiver_output_amount, change_output_amount]]) |
|
|
|
# now we must construct the three outputs with correct output amounts. |
|
outputs = [{"address": receiver_addr, "value": receiver_output_amount}] |
|
outputs.append({"address": proposer_addr, "value": receiver_output_amount}) |
|
outputs.append({"address": change_addr, |
|
"value": change_output_amount}) |
|
|
|
return outputs |
|
|
|
def is_snicker_tx(tx, snicker_version=bytes([1])): |
|
""" Returns True if the CTransaction object `tx` |
|
fits the pattern of a SNICKER coinjoin of type |
|
defined in `snicker_version`, or False otherwise. |
|
""" |
|
if not snicker_version == b"\x01": |
|
raise NotImplementedError("Only v1 SNICKER currently implemented.") |
|
return is_snicker_v1_tx(tx) |
|
|
|
def is_snicker_v1_tx(tx): |
|
""" We expect: |
|
* 2 equal outputs, same script type, pubkey hash variant. |
|
* 1 other output (0 is negligible probability hence ignored - if it |
|
was included it would create a lot of false positives). |
|
* >=2 inputs, same script type, pubkey hash variant. |
|
* Input sequence numbers are both 0xffffffff |
|
* nVersion 2 |
|
* nLockTime 0 |
|
The above rules are for matching the v1 variant of SNICKER. |
|
""" |
|
assert isinstance(tx, CTransaction) |
|
if tx.nVersion != 2: |
|
return False |
|
if tx.nLockTime != 0: |
|
return False |
|
if len(tx.vin) < 2: |
|
return False |
|
if len(tx.vout) != 3: |
|
return False |
|
for vi in tx.vin: |
|
if vi.nSequence != 0xffffffff: |
|
return False |
|
# identify if there are two equal sized outs |
|
c = Counter([vo.nValue for vo in tx.vout]) |
|
equal_out = -1 |
|
for x in c: |
|
if c[x] not in [1, 2]: |
|
# note three equal outs technically agrees |
|
# with spec, but negligible prob and will |
|
# create false positives. |
|
return False |
|
if c[x] == 2: |
|
equal_out = x |
|
|
|
if equal_out == -1: |
|
return False |
|
|
|
# ensure that the equal sized outputs have the |
|
# same script type |
|
matched_spk = None |
|
for vo in tx.vout: |
|
if vo.nValue == equal_out: |
|
if not matched_spk: |
|
try: |
|
matched_spk = btc.CCoinAddress.from_scriptPubKey( |
|
vo.scriptPubKey).get_scriptPubKey_type() |
|
except CCoinAddressError: |
|
return False |
|
else: |
|
try: |
|
if not btc.CCoinAddress.from_scriptPubKey( |
|
vo.scriptPubKey).get_scriptPubKey_type() == matched_spk: |
|
return False |
|
except CCoinAddressError: |
|
return False |
|
assert matched_spk |
|
return True
|
|
|