From 549419f7ef60097efcfcc9f4eaa764f2aa20528a Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 1 Jun 2019 21:17:01 +0300 Subject: [PATCH] elements: add transaction blinding/unblinding API Re-generated messages using: $ (cd ../tools; ./build_protobuf) $ (cd python; pip3 install --user -e .) Supports balancing Pedersen commitments, rangeproofs and surjection proofs. TODOs: * Randomness is passed as arguments. --- common/protob/messages-liquid.proto | 61 ++ common/protob/messages.proto | 7 + core/src/apps/liquid/__init__.py | 8 + core/src/apps/liquid/blind.py | 123 +++++ core/src/apps/liquid/blind_tx.py | 39 ++ core/src/apps/liquid/unblind_output.py | 28 + core/src/main.py | 2 + core/src/trezor/messages/LiquidAmount.py | 35 ++ .../trezor/messages/LiquidBalanceBlinds.py | 30 + core/src/trezor/messages/LiquidBlindOutput.py | 39 ++ core/src/trezor/messages/LiquidBlindTx.py | 32 ++ .../trezor/messages/LiquidBlindTxRequest.py | 26 + .../trezor/messages/LiquidBlindedOutput.py | 41 ++ .../trezor/messages/LiquidUnblindOutput.py | 31 ++ core/src/trezor/messages/MessageType.py | 5 + python/src/trezorlib/liquid.py | 33 ++ python/src/trezorlib/messages/LiquidAmount.py | 35 ++ .../trezorlib/messages/LiquidBalanceBlinds.py | 30 + .../trezorlib/messages/LiquidBlindOutput.py | 39 ++ .../src/trezorlib/messages/LiquidBlindTx.py | 32 ++ .../messages/LiquidBlindTxRequest.py | 26 + .../trezorlib/messages/LiquidBlindedOutput.py | 41 ++ .../trezorlib/messages/LiquidUnblindOutput.py | 31 ++ python/src/trezorlib/messages/MessageType.py | 5 + python/src/trezorlib/messages/__init__.py | 7 + tests/device_tests/REGISTERED_MARKERS | 1 + tests/device_tests/secp256k1_zkp.py | 520 ++++++++++++++++++ tests/device_tests/test_msg_liquid.py | 474 ++++++++++++++++ tools/build_protobuf | 1 + 29 files changed, 1782 insertions(+) create mode 100644 common/protob/messages-liquid.proto create mode 100644 core/src/apps/liquid/__init__.py create mode 100644 core/src/apps/liquid/blind.py create mode 100644 core/src/apps/liquid/blind_tx.py create mode 100644 core/src/apps/liquid/unblind_output.py create mode 100644 core/src/trezor/messages/LiquidAmount.py create mode 100644 core/src/trezor/messages/LiquidBalanceBlinds.py create mode 100644 core/src/trezor/messages/LiquidBlindOutput.py create mode 100644 core/src/trezor/messages/LiquidBlindTx.py create mode 100644 core/src/trezor/messages/LiquidBlindTxRequest.py create mode 100644 core/src/trezor/messages/LiquidBlindedOutput.py create mode 100644 core/src/trezor/messages/LiquidUnblindOutput.py create mode 100644 python/src/trezorlib/liquid.py create mode 100644 python/src/trezorlib/messages/LiquidAmount.py create mode 100644 python/src/trezorlib/messages/LiquidBalanceBlinds.py create mode 100644 python/src/trezorlib/messages/LiquidBlindOutput.py create mode 100644 python/src/trezorlib/messages/LiquidBlindTx.py create mode 100644 python/src/trezorlib/messages/LiquidBlindTxRequest.py create mode 100644 python/src/trezorlib/messages/LiquidBlindedOutput.py create mode 100644 python/src/trezorlib/messages/LiquidUnblindOutput.py create mode 100644 tests/device_tests/secp256k1_zkp.py create mode 100644 tests/device_tests/test_msg_liquid.py diff --git a/common/protob/messages-liquid.proto b/common/protob/messages-liquid.proto new file mode 100644 index 00000000000..3d7600a7c63 --- /dev/null +++ b/common/protob/messages-liquid.proto @@ -0,0 +1,61 @@ +syntax = "proto2"; +package hw.trezor.messages.liquid; + +// Sugar for easier handling in Java +option java_package = "com.satoshilabs.trezor.lib.protobuf"; +option java_outer_classname = "TrezorMessageLiquid"; + +message LiquidAmount { + optional uint64 value = 1; + optional bytes value_blind = 2; // 32-bytes or None (-> should be derived on-device) + optional bytes asset = 3; // 32-bytes + optional bytes asset_blind = 4; // 32-bytes or None (-> should be derived on-device) +} + +/** + * Request: Blind specified output + * @start + * @next LiquidBlindedOutput + */ +message LiquidBlindOutput { + optional LiquidAmount amount = 1; + optional bytes ecdh_pubkey = 2; // recipient ECDH pubkey, for ECDH nonce generation + optional bytes ecdh_privkey = 3; // our ECDH private key, 32-bytes or None (-> should be derived on-device) + optional bytes script_pubkey = 4; + + // Used for surjection proof generation: + optional bytes random_seed32 = 5; // for input asset index selection +} + +/** + * Response: blinded output + * @end + */ +message LiquidBlindedOutput { + optional bytes conf_value = 1; + optional bytes conf_asset = 2; + optional bytes ecdh_pubkey = 3; + optional bytes script_pubkey = 4; // same from LiquidBlindOutput + optional bytes range_proof = 5; + optional bytes surjection_proof = 6; +} + + +message LiquidUnblindOutput { + optional LiquidBlindedOutput blinded = 1; + optional bytes ecdh_privkey = 2; // -> should be derived on-device +} + +message LiquidBalanceBlinds { + repeated LiquidAmount inputs = 1; // only `value`, `value_blind` and `asset_blind` are used + repeated LiquidAmount outputs = 2; // only `value`, `value_blind` and `asset_blind` are used +} + +message LiquidBlindTx { + repeated LiquidAmount inputs = 1; + repeated LiquidBlindOutput outputs = 2; +} + +message LiquidBlindTxRequest { + optional uint32 output_index = 1; +} diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 86b205f0b9f..5394bcc4505 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -237,4 +237,11 @@ enum MessageType { MessageType_BinanceOrderMsg = 707 [(wire_in) = true]; MessageType_BinanceCancelMsg = 708 [(wire_in) = true]; MessageType_BinanceSignedTx = 709 [(wire_out) = true]; + + // Liquid + MessageType_LiquidBlindTx = 801 [(wire_in) = true]; + MessageType_LiquidBlindTxRequest = 802 [(wire_in) = true]; + MessageType_LiquidBlindedOutput = 803 [(wire_out) = true]; + MessageType_LiquidUnblindOutput = 804 [(wire_in) = true]; + MessageType_LiquidAmount = 805 [(wire_out) = true]; } diff --git a/core/src/apps/liquid/__init__.py b/core/src/apps/liquid/__init__.py new file mode 100644 index 00000000000..678d147f03f --- /dev/null +++ b/core/src/apps/liquid/__init__.py @@ -0,0 +1,8 @@ +from trezor import wire +from trezor.messages import MessageType + + +def boot(): + ns = [["slip21"]] + wire.add(MessageType.LiquidBlindTx, __name__, "blind_tx", ns) + wire.add(MessageType.LiquidUnblindOutput, __name__, "unblind_output", ns) diff --git a/core/src/apps/liquid/blind.py b/core/src/apps/liquid/blind.py new file mode 100644 index 00000000000..e4b0d80a99a --- /dev/null +++ b/core/src/apps/liquid/blind.py @@ -0,0 +1,123 @@ +from trezor.crypto.curve import secp256k1_zkp +from trezor.crypto.hashlib import sha256 +from trezor.messages.LiquidAmount import LiquidAmount +from trezor.messages.LiquidBlindedOutput import LiquidBlindedOutput + +BLIND_SIZE = 32 # in bytes + + +def balance_blinds(context, inputs, outputs): + amounts = inputs + outputs + num_of_inputs = len(inputs) + for a in amounts: + if len(a.asset_blind) != BLIND_SIZE: + raise ValueError("incorrect asset_blind length") + if len(a.value_blind) != BLIND_SIZE: + raise ValueError("incorrect value_blind length") + + values = tuple(a.value for a in amounts) + assets = tuple(a.asset for a in amounts) + value_blinds = b"".join(bytes(a.value_blind) for a in amounts) + asset_blinds = b"".join(bytes(a.asset_blind) for a in amounts) + + value_blinds = bytearray(value_blinds) # to be updated + context.balance_blinds(values, value_blinds, asset_blinds, num_of_inputs) + value_blinds = bytes(value_blinds) + + assert len(value_blinds) == BLIND_SIZE * len(amounts) + assert len(asset_blinds) == BLIND_SIZE * len(amounts) + + balanced = [ + LiquidAmount( + value=values[i], + value_blind=value_blinds[i * BLIND_SIZE : (i + 1) * BLIND_SIZE], + asset=assets[i], + asset_blind=asset_blinds[i * BLIND_SIZE : (i + 1) * BLIND_SIZE], + ) + for i in range(len(amounts)) + ] + balanced_inputs = balanced[:num_of_inputs] + balanced_outputs = balanced[num_of_inputs:] + + assert balanced_inputs == inputs # inputs should not change + assert len(balanced_outputs) == len(outputs) + for output, balanced_output in zip(outputs, balanced_outputs): + output.value_blind = balanced_output.value_blind + assert balanced_outputs == outputs # only value blinders may change + + +def ecdh(context, our_privkey, peer_pubkey): + compressed = True + shared_pubkey = context.multiply(our_privkey, peer_pubkey, compressed) + return sha256(sha256(shared_pubkey).digest()).digest() + + +def blind_output(context, output, inputs, proof_buffer): + peer_pubkey = output.ecdh_pubkey + our_privkey = output.ecdh_privkey # TODO: derive via BIP-32 + our_pubkey = context.publickey(our_privkey) + nonce = ecdh(context, our_privkey, peer_pubkey) + + conf_asset = context.blind_generator(output.amount.asset, output.amount.asset_blind) + # TODO: derive value_blind via HMAC with BIP-32 derived private key + conf_value = context.pedersen_commit( + output.amount.value, output.amount.value_blind, conf_asset + ) + yield LiquidBlindedOutput( + conf_value=conf_value, + conf_asset=conf_asset, + ecdh_pubkey=our_pubkey, + script_pubkey=output.script_pubkey, + ) + del peer_pubkey, our_privkey, our_pubkey + + asset_message = output.amount.asset + output.amount.asset_blind + range_proof_view = context.rangeproof_sign( + secp256k1_zkp.RangeProofConfig(min_value=1, exponent=0, bits=36), + output.amount.value, + conf_value, + output.amount.value_blind, + nonce, + asset_message, + output.script_pubkey, + conf_asset, + proof_buffer, + ) + del asset_message, nonce, conf_asset, conf_value + yield LiquidBlindedOutput(range_proof=range_proof_view) + + input_assets = b"".join(bytes(i.asset) for i in inputs) + input_assets_blinds = b"".join(bytes(i.asset_blind) for i in inputs) + surjection_proof = context.surjection_proof( + output.amount.asset, + output.amount.asset_blind, + input_assets, + input_assets_blinds, + len(inputs), + output.random_seed32, + proof_buffer, + ) + del input_assets, input_assets_blinds + yield LiquidBlindedOutput(surjection_proof=surjection_proof) + + +def unblind_output(context, blinded, ecdh_privkey, message_buffer): + peer_pubkey = blinded.ecdh_pubkey + our_privkey = ecdh_privkey # TODO: derive via BIP-32 + nonce = ecdh(context, our_privkey, peer_pubkey) + + (value, value_blind) = context.rangeproof_rewind( + blinded.conf_value, + blinded.conf_asset, + nonce, + blinded.range_proof, + blinded.script_pubkey, + message_buffer, + ) + + return LiquidAmount( + value=value, + value_blind=value_blind, + asset=bytes(message_buffer[0:32]), + asset_blind=bytes(message_buffer[32:64]), + ) diff --git a/core/src/apps/liquid/blind_tx.py b/core/src/apps/liquid/blind_tx.py new file mode 100644 index 00000000000..474c99236eb --- /dev/null +++ b/core/src/apps/liquid/blind_tx.py @@ -0,0 +1,39 @@ +import gc + +from trezor.crypto.curve import secp256k1_zkp +from trezor.messages.LiquidBlindedOutput import LiquidBlindedOutput +from trezor.messages.LiquidBlindTx import LiquidBlindTx +from trezor.messages.LiquidBlindTxRequest import LiquidBlindTxRequest + +from . import blind + + +async def blind_tx(ctx, msg, keychain): + gc.collect() + with secp256k1_zkp.Context() as context: + proof_buffer = secp256k1_zkp.allocate_proof_buffer() + # Allow the device to allocate memory before the parsing the actual message + dummy_ack = LiquidBlindedOutput() + msg = await ctx.call(dummy_ack, LiquidBlindTx) + blind.balance_blinds( + context=context, + inputs=msg.inputs, + outputs=[out.amount for out in msg.outputs], + ) + req = await ctx.call(dummy_ack, LiquidBlindTxRequest) + while req.output_index is not None: + blinded_iter = blind.blind_output( + context=context, + output=msg.outputs[req.output_index], + inputs=msg.inputs, + proof_buffer=proof_buffer, + ) + for blinded in blinded_iter: + dummy = await ctx.call(blinded, LiquidBlindTxRequest) + assert dummy.output_index == req.output_index + del blinded # MUST be discarded before next iteration + + sentinel = LiquidBlindedOutput() # sentinel value + req = await ctx.call(sentinel, LiquidBlindTxRequest) # next output + del proof_buffer + return dummy_ack diff --git a/core/src/apps/liquid/unblind_output.py b/core/src/apps/liquid/unblind_output.py new file mode 100644 index 00000000000..10349d5db46 --- /dev/null +++ b/core/src/apps/liquid/unblind_output.py @@ -0,0 +1,28 @@ +import gc + +from trezor.crypto.curve import secp256k1_zkp +from trezor.messages.LiquidAmount import LiquidAmount +from trezor.messages.LiquidUnblindOutput import LiquidUnblindOutput + +from . import blind + +from apps.common.seed import Slip77 + + +async def unblind_output(ctx, msg, keychain): + gc.collect() + # NOTE: we can reuse {range,surjection}-proof buffer for message recovery + message_buffer = secp256k1_zkp.allocate_proof_buffer() + with secp256k1_zkp.Context() as context: + # Allow the device to allocate memory before the parsing the actual message + msg = await ctx.call(LiquidAmount(), LiquidUnblindOutput) + ecdh_privkey = msg.ecdh_privkey or Slip77(keychain).derive_blinding_private_key( + script=msg.blinded.script_pubkey + ) + + return blind.unblind_output( + context=context, + blinded=msg.blinded, + ecdh_privkey=ecdh_privkey, + message_buffer=message_buffer, + ) diff --git a/core/src/main.py b/core/src/main.py index 737661825b4..11e4c2df73d 100644 --- a/core/src/main.py +++ b/core/src/main.py @@ -42,6 +42,7 @@ def _boot_default() -> None: import apps.tezos import apps.eos import apps.binance + import apps.liquid if __debug__: import apps.debug @@ -62,6 +63,7 @@ def _boot_default() -> None: apps.tezos.boot() apps.eos.boot() apps.binance.boot() + apps.liquid.boot() if __debug__: apps.debug.boot() else: diff --git a/core/src/trezor/messages/LiquidAmount.py b/core/src/trezor/messages/LiquidAmount.py new file mode 100644 index 00000000000..9b323280379 --- /dev/null +++ b/core/src/trezor/messages/LiquidAmount.py @@ -0,0 +1,35 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidAmount(p.MessageType): + MESSAGE_WIRE_TYPE = 805 + + def __init__( + self, + value: int = None, + value_blind: bytes = None, + asset: bytes = None, + asset_blind: bytes = None, + ) -> None: + self.value = value + self.value_blind = value_blind + self.asset = asset + self.asset_blind = asset_blind + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('value', p.UVarintType, 0), + 2: ('value_blind', p.BytesType, 0), + 3: ('asset', p.BytesType, 0), + 4: ('asset_blind', p.BytesType, 0), + } diff --git a/core/src/trezor/messages/LiquidBalanceBlinds.py b/core/src/trezor/messages/LiquidBalanceBlinds.py new file mode 100644 index 00000000000..48577f3ce9d --- /dev/null +++ b/core/src/trezor/messages/LiquidBalanceBlinds.py @@ -0,0 +1,30 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .LiquidAmount import LiquidAmount + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidBalanceBlinds(p.MessageType): + + def __init__( + self, + inputs: List[LiquidAmount] = None, + outputs: List[LiquidAmount] = None, + ) -> None: + self.inputs = inputs if inputs is not None else [] + self.outputs = outputs if outputs is not None else [] + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('inputs', LiquidAmount, p.FLAG_REPEATED), + 2: ('outputs', LiquidAmount, p.FLAG_REPEATED), + } diff --git a/core/src/trezor/messages/LiquidBlindOutput.py b/core/src/trezor/messages/LiquidBlindOutput.py new file mode 100644 index 00000000000..c70efd8c344 --- /dev/null +++ b/core/src/trezor/messages/LiquidBlindOutput.py @@ -0,0 +1,39 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .LiquidAmount import LiquidAmount + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidBlindOutput(p.MessageType): + + def __init__( + self, + amount: LiquidAmount = None, + ecdh_pubkey: bytes = None, + ecdh_privkey: bytes = None, + script_pubkey: bytes = None, + random_seed32: bytes = None, + ) -> None: + self.amount = amount + self.ecdh_pubkey = ecdh_pubkey + self.ecdh_privkey = ecdh_privkey + self.script_pubkey = script_pubkey + self.random_seed32 = random_seed32 + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('amount', LiquidAmount, 0), + 2: ('ecdh_pubkey', p.BytesType, 0), + 3: ('ecdh_privkey', p.BytesType, 0), + 4: ('script_pubkey', p.BytesType, 0), + 5: ('random_seed32', p.BytesType, 0), + } diff --git a/core/src/trezor/messages/LiquidBlindTx.py b/core/src/trezor/messages/LiquidBlindTx.py new file mode 100644 index 00000000000..3b88c663cc8 --- /dev/null +++ b/core/src/trezor/messages/LiquidBlindTx.py @@ -0,0 +1,32 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .LiquidAmount import LiquidAmount +from .LiquidBlindOutput import LiquidBlindOutput + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidBlindTx(p.MessageType): + MESSAGE_WIRE_TYPE = 801 + + def __init__( + self, + inputs: List[LiquidAmount] = None, + outputs: List[LiquidBlindOutput] = None, + ) -> None: + self.inputs = inputs if inputs is not None else [] + self.outputs = outputs if outputs is not None else [] + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('inputs', LiquidAmount, p.FLAG_REPEATED), + 2: ('outputs', LiquidBlindOutput, p.FLAG_REPEATED), + } diff --git a/core/src/trezor/messages/LiquidBlindTxRequest.py b/core/src/trezor/messages/LiquidBlindTxRequest.py new file mode 100644 index 00000000000..037d21ee11d --- /dev/null +++ b/core/src/trezor/messages/LiquidBlindTxRequest.py @@ -0,0 +1,26 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidBlindTxRequest(p.MessageType): + MESSAGE_WIRE_TYPE = 802 + + def __init__( + self, + output_index: int = None, + ) -> None: + self.output_index = output_index + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('output_index', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/LiquidBlindedOutput.py b/core/src/trezor/messages/LiquidBlindedOutput.py new file mode 100644 index 00000000000..edd3b2ba04b --- /dev/null +++ b/core/src/trezor/messages/LiquidBlindedOutput.py @@ -0,0 +1,41 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidBlindedOutput(p.MessageType): + MESSAGE_WIRE_TYPE = 803 + + def __init__( + self, + conf_value: bytes = None, + conf_asset: bytes = None, + ecdh_pubkey: bytes = None, + script_pubkey: bytes = None, + range_proof: bytes = None, + surjection_proof: bytes = None, + ) -> None: + self.conf_value = conf_value + self.conf_asset = conf_asset + self.ecdh_pubkey = ecdh_pubkey + self.script_pubkey = script_pubkey + self.range_proof = range_proof + self.surjection_proof = surjection_proof + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('conf_value', p.BytesType, 0), + 2: ('conf_asset', p.BytesType, 0), + 3: ('ecdh_pubkey', p.BytesType, 0), + 4: ('script_pubkey', p.BytesType, 0), + 5: ('range_proof', p.BytesType, 0), + 6: ('surjection_proof', p.BytesType, 0), + } diff --git a/core/src/trezor/messages/LiquidUnblindOutput.py b/core/src/trezor/messages/LiquidUnblindOutput.py new file mode 100644 index 00000000000..aa688ce992c --- /dev/null +++ b/core/src/trezor/messages/LiquidUnblindOutput.py @@ -0,0 +1,31 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .LiquidBlindedOutput import LiquidBlindedOutput + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidUnblindOutput(p.MessageType): + MESSAGE_WIRE_TYPE = 804 + + def __init__( + self, + blinded: LiquidBlindedOutput = None, + ecdh_privkey: bytes = None, + ) -> None: + self.blinded = blinded + self.ecdh_privkey = ecdh_privkey + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('blinded', LiquidBlindedOutput, 0), + 2: ('ecdh_privkey', p.BytesType, 0), + } diff --git a/core/src/trezor/messages/MessageType.py b/core/src/trezor/messages/MessageType.py index d38c70b53cb..f84241f8463 100644 --- a/core/src/trezor/messages/MessageType.py +++ b/core/src/trezor/messages/MessageType.py @@ -177,3 +177,8 @@ BinanceOrderMsg = 707 BinanceCancelMsg = 708 BinanceSignedTx = 709 +LiquidBlindTx = 801 +LiquidBlindTxRequest = 802 +LiquidBlindedOutput = 803 +LiquidUnblindOutput = 804 +LiquidAmount = 805 diff --git a/python/src/trezorlib/liquid.py b/python/src/trezorlib/liquid.py new file mode 100644 index 00000000000..53b6a17479e --- /dev/null +++ b/python/src/trezorlib/liquid.py @@ -0,0 +1,33 @@ +from . import messages +from .tools import expect + + +def blind_tx(client, inputs, outputs): + # Allow the device to allocate memory before the parsing the actual message + ack = client.call(messages.LiquidBlindTx()) + ack = client.call(messages.LiquidBlindTx(inputs=inputs, outputs=outputs)) + blinded = [] + for i in range(len(outputs)): + response = messages.LiquidBlindedOutput() + while True: + response_part = client.call(messages.LiquidBlindTxRequest(output_index=i)) + assert isinstance(response, messages.LiquidBlindedOutput) + if response_part == messages.LiquidBlindedOutput(): + break + # HACK: there must be a better way :) + for k, v in response_part.__dict__.items(): + if response.__dict__.get(k) is not None: + continue + if v is not None: + response.__dict__[k] = v + + blinded.append(response) + ack = client.call(messages.LiquidBlindTxRequest()) # no more outputs + return blinded + + +@expect(messages.LiquidAmount) +def unblind_output(client, unblind): + # Allow the device to allocate memory before the parsing the actual message + dummy_ack = client.call(messages.LiquidUnblindOutput()) + return client.call(unblind) diff --git a/python/src/trezorlib/messages/LiquidAmount.py b/python/src/trezorlib/messages/LiquidAmount.py new file mode 100644 index 00000000000..53dd27a207a --- /dev/null +++ b/python/src/trezorlib/messages/LiquidAmount.py @@ -0,0 +1,35 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidAmount(p.MessageType): + MESSAGE_WIRE_TYPE = 805 + + def __init__( + self, + value: int = None, + value_blind: bytes = None, + asset: bytes = None, + asset_blind: bytes = None, + ) -> None: + self.value = value + self.value_blind = value_blind + self.asset = asset + self.asset_blind = asset_blind + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('value', p.UVarintType, 0), + 2: ('value_blind', p.BytesType, 0), + 3: ('asset', p.BytesType, 0), + 4: ('asset_blind', p.BytesType, 0), + } diff --git a/python/src/trezorlib/messages/LiquidBalanceBlinds.py b/python/src/trezorlib/messages/LiquidBalanceBlinds.py new file mode 100644 index 00000000000..6f08bfbd02e --- /dev/null +++ b/python/src/trezorlib/messages/LiquidBalanceBlinds.py @@ -0,0 +1,30 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +from .LiquidAmount import LiquidAmount + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidBalanceBlinds(p.MessageType): + + def __init__( + self, + inputs: List[LiquidAmount] = None, + outputs: List[LiquidAmount] = None, + ) -> None: + self.inputs = inputs if inputs is not None else [] + self.outputs = outputs if outputs is not None else [] + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('inputs', LiquidAmount, p.FLAG_REPEATED), + 2: ('outputs', LiquidAmount, p.FLAG_REPEATED), + } diff --git a/python/src/trezorlib/messages/LiquidBlindOutput.py b/python/src/trezorlib/messages/LiquidBlindOutput.py new file mode 100644 index 00000000000..494bbca5dcc --- /dev/null +++ b/python/src/trezorlib/messages/LiquidBlindOutput.py @@ -0,0 +1,39 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +from .LiquidAmount import LiquidAmount + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidBlindOutput(p.MessageType): + + def __init__( + self, + amount: LiquidAmount = None, + ecdh_pubkey: bytes = None, + ecdh_privkey: bytes = None, + script_pubkey: bytes = None, + random_seed32: bytes = None, + ) -> None: + self.amount = amount + self.ecdh_pubkey = ecdh_pubkey + self.ecdh_privkey = ecdh_privkey + self.script_pubkey = script_pubkey + self.random_seed32 = random_seed32 + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('amount', LiquidAmount, 0), + 2: ('ecdh_pubkey', p.BytesType, 0), + 3: ('ecdh_privkey', p.BytesType, 0), + 4: ('script_pubkey', p.BytesType, 0), + 5: ('random_seed32', p.BytesType, 0), + } diff --git a/python/src/trezorlib/messages/LiquidBlindTx.py b/python/src/trezorlib/messages/LiquidBlindTx.py new file mode 100644 index 00000000000..ce2051195fa --- /dev/null +++ b/python/src/trezorlib/messages/LiquidBlindTx.py @@ -0,0 +1,32 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +from .LiquidAmount import LiquidAmount +from .LiquidBlindOutput import LiquidBlindOutput + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidBlindTx(p.MessageType): + MESSAGE_WIRE_TYPE = 801 + + def __init__( + self, + inputs: List[LiquidAmount] = None, + outputs: List[LiquidBlindOutput] = None, + ) -> None: + self.inputs = inputs if inputs is not None else [] + self.outputs = outputs if outputs is not None else [] + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('inputs', LiquidAmount, p.FLAG_REPEATED), + 2: ('outputs', LiquidBlindOutput, p.FLAG_REPEATED), + } diff --git a/python/src/trezorlib/messages/LiquidBlindTxRequest.py b/python/src/trezorlib/messages/LiquidBlindTxRequest.py new file mode 100644 index 00000000000..b5a253c4361 --- /dev/null +++ b/python/src/trezorlib/messages/LiquidBlindTxRequest.py @@ -0,0 +1,26 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidBlindTxRequest(p.MessageType): + MESSAGE_WIRE_TYPE = 802 + + def __init__( + self, + output_index: int = None, + ) -> None: + self.output_index = output_index + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('output_index', p.UVarintType, 0), + } diff --git a/python/src/trezorlib/messages/LiquidBlindedOutput.py b/python/src/trezorlib/messages/LiquidBlindedOutput.py new file mode 100644 index 00000000000..ea8a9002d6a --- /dev/null +++ b/python/src/trezorlib/messages/LiquidBlindedOutput.py @@ -0,0 +1,41 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidBlindedOutput(p.MessageType): + MESSAGE_WIRE_TYPE = 803 + + def __init__( + self, + conf_value: bytes = None, + conf_asset: bytes = None, + ecdh_pubkey: bytes = None, + script_pubkey: bytes = None, + range_proof: bytes = None, + surjection_proof: bytes = None, + ) -> None: + self.conf_value = conf_value + self.conf_asset = conf_asset + self.ecdh_pubkey = ecdh_pubkey + self.script_pubkey = script_pubkey + self.range_proof = range_proof + self.surjection_proof = surjection_proof + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('conf_value', p.BytesType, 0), + 2: ('conf_asset', p.BytesType, 0), + 3: ('ecdh_pubkey', p.BytesType, 0), + 4: ('script_pubkey', p.BytesType, 0), + 5: ('range_proof', p.BytesType, 0), + 6: ('surjection_proof', p.BytesType, 0), + } diff --git a/python/src/trezorlib/messages/LiquidUnblindOutput.py b/python/src/trezorlib/messages/LiquidUnblindOutput.py new file mode 100644 index 00000000000..469ef2870bc --- /dev/null +++ b/python/src/trezorlib/messages/LiquidUnblindOutput.py @@ -0,0 +1,31 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +from .LiquidBlindedOutput import LiquidBlindedOutput + +if __debug__: + try: + from typing import Dict, List, Optional + from typing_extensions import Literal # noqa: F401 + except ImportError: + Dict, List, Optional = None, None, None # type: ignore + + +class LiquidUnblindOutput(p.MessageType): + MESSAGE_WIRE_TYPE = 804 + + def __init__( + self, + blinded: LiquidBlindedOutput = None, + ecdh_privkey: bytes = None, + ) -> None: + self.blinded = blinded + self.ecdh_privkey = ecdh_privkey + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('blinded', LiquidBlindedOutput, 0), + 2: ('ecdh_privkey', p.BytesType, 0), + } diff --git a/python/src/trezorlib/messages/MessageType.py b/python/src/trezorlib/messages/MessageType.py index d38c70b53cb..f84241f8463 100644 --- a/python/src/trezorlib/messages/MessageType.py +++ b/python/src/trezorlib/messages/MessageType.py @@ -177,3 +177,8 @@ BinanceOrderMsg = 707 BinanceCancelMsg = 708 BinanceSignedTx = 709 +LiquidBlindTx = 801 +LiquidBlindTxRequest = 802 +LiquidBlindedOutput = 803 +LiquidUnblindOutput = 804 +LiquidAmount = 805 diff --git a/python/src/trezorlib/messages/__init__.py b/python/src/trezorlib/messages/__init__.py index 89f4c5fc99f..2894d274cb0 100644 --- a/python/src/trezorlib/messages/__init__.py +++ b/python/src/trezorlib/messages/__init__.py @@ -105,6 +105,13 @@ from .HDNodeType import HDNodeType from .IdentityType import IdentityType from .Initialize import Initialize +from .LiquidAmount import LiquidAmount +from .LiquidBalanceBlinds import LiquidBalanceBlinds +from .LiquidBlindOutput import LiquidBlindOutput +from .LiquidBlindTx import LiquidBlindTx +from .LiquidBlindTxRequest import LiquidBlindTxRequest +from .LiquidBlindedOutput import LiquidBlindedOutput +from .LiquidUnblindOutput import LiquidUnblindOutput from .LiskAddress import LiskAddress from .LiskDelegateType import LiskDelegateType from .LiskGetAddress import LiskGetAddress diff --git a/tests/device_tests/REGISTERED_MARKERS b/tests/device_tests/REGISTERED_MARKERS index c61862dbb67..830983514ee 100644 --- a/tests/device_tests/REGISTERED_MARKERS +++ b/tests/device_tests/REGISTERED_MARKERS @@ -6,6 +6,7 @@ eos ethereum komodo lisk +liquid monero nem ontology diff --git a/tests/device_tests/secp256k1_zkp.py b/tests/device_tests/secp256k1_zkp.py new file mode 100644 index 00000000000..ca9ad856f7a --- /dev/null +++ b/tests/device_tests/secp256k1_zkp.py @@ -0,0 +1,520 @@ +""" +Source: https://github.com/Simplexum/python-bitcointx/blob/confidential_transactions/bitcointx/core/secp256k1.py +""" + +# Copyright (C) 2018 The python-bitcointx developers +# +# This file is part of python-bitcointx. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of python-bitcointx, including this file, may be copied, modified, +# propagated, or distributed except according to the terms contained in the +# LICENSE file. + +# pylama:ignore=E501,E221 + +# NOTE: for simplicity, when we need to pass an array of structs to secp256k1 +# function, we will build an array of bytes out of elements, and than pass +# this array. we are dealing with 32 or 64-byte aligned data, +# so this should be safe. You can use build_aligned_data_array() for this. + +# NOTE: special care should be taken with functions that may write to parts +# of their arguments, like secp256k1_pedersen_blind_generator_blind_sum, +# which will overwrite the element pointed to by blinding_factor. +# python's byte instance is supposed to be immutable, and for mutable byte +# buffers you should use ctypes.create_string_buffer(). + +import ctypes +import ctypes.util +import os +import threading + +_cwd = os.path.dirname(__file__) +_path = os.path.join(_cwd, "../../vendor/secp256k1-zkp/.libs/libsecp256k1.so") +secp256k1 = ctypes.cdll.LoadLibrary(_path) + + +def format_args(args): + return "".join("\n\t" + format_arg(a) for a in args) + + +def format_arg(arg): + if isinstance(arg, bytes): + return f"bytes({arg})" + if hasattr(arg, "raw"): + return f"Array({len(arg.raw)})" + if hasattr(arg, "_obj"): + return f"Ref({arg._obj})" + return f"{str(arg)}" + + +PUBLIC_KEY_SIZE = 65 +COMPRESSED_PUBLIC_KEY_SIZE = 33 +SIGNATURE_SIZE = 72 +COMPACT_SIGNATURE_SIZE = 65 + + +class Libsecp256k1Exception(EnvironmentError): + pass + + +SECP256K1_FLAGS_TYPE_CONTEXT = 1 << 0 +SECP256K1_FLAGS_BIT_CONTEXT_SIGN = 1 << 9 +SECP256K1_FLAGS_BIT_CONTEXT_VERIFY = 1 << 8 + +SECP256K1_CONTEXT_SIGN = SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_SIGN +SECP256K1_CONTEXT_VERIFY = ( + SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_VERIFY +) + +SECP256K1_FLAGS_TYPE_COMPRESSION = 1 << 1 +SECP256K1_FLAGS_BIT_COMPRESSION = 1 << 8 + +SECP256K1_EC_COMPRESSED = ( + SECP256K1_FLAGS_TYPE_COMPRESSION | SECP256K1_FLAGS_BIT_COMPRESSION +) +SECP256K1_EC_UNCOMPRESSED = SECP256K1_FLAGS_TYPE_COMPRESSION + +_secp256k1_error_storage = threading.local() + +_ctypes_functype = getattr(ctypes, "WINFUNCTYPE", getattr(ctypes, "CFUNCTYPE")) + + +@_ctypes_functype(ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p) +def _secp256k1_error_callback_fn(error_str, _data): + _secp256k1_error_storage.last_error = { + "code": -1, + "type": "internal_error", + "message": str(error_str), + } + + +@_ctypes_functype(ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p) +def _secp256k1_illegal_callback_fn(error_str, _data): + _secp256k1_error_storage.last_error = { + "code": -2, + "type": "illegal_argument", + "message": str(error_str), + } + + +def secp256k1_get_last_error(): + return getattr(_secp256k1_error_storage, "last_error", None) + + +def _check_ressecp256k1_void_p(val, _func, _args): + if val == 0: + err = _secp256k1_error_storage.last_error + raise Libsecp256k1Exception(err["code"], err["message"]) + return ctypes.c_void_p(val) + + +def _set_zkp_func_types(): + secp256k1.secp256k1_rangeproof_info.restype = ctypes.c_int + secp256k1.secp256k1_rangeproof_info.argtypes = [ + ctypes.c_void_p, + ctypes.POINTER(ctypes.c_int), + ctypes.POINTER(ctypes.c_int), + ctypes.POINTER(ctypes.c_uint64), + ctypes.POINTER(ctypes.c_uint64), + ctypes.c_char_p, + ctypes.c_size_t, + ] + secp256k1.secp256k1_generator_parse.restype = ctypes.c_int + secp256k1.secp256k1_generator_parse.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ] + secp256k1.secp256k1_generator_generate.restype = ctypes.c_int + secp256k1.secp256k1_generator_generate.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ] + secp256k1.secp256k1_generator_generate_blinded.restype = ctypes.c_int + secp256k1.secp256k1_generator_generate_blinded.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_char_p, + ] + secp256k1.secp256k1_generator_serialize.restype = ctypes.c_int + secp256k1.secp256k1_generator_serialize.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ] + secp256k1.secp256k1_pedersen_commit.restype = ctypes.c_int + secp256k1.secp256k1_pedersen_commit.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_uint64, + ctypes.c_char_p, + ] + secp256k1.secp256k1_pedersen_commitment_serialize.restype = ctypes.c_int + secp256k1.secp256k1_pedersen_commitment_serialize.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ] + secp256k1.secp256k1_pedersen_commitment_parse.restype = ctypes.c_int + secp256k1.secp256k1_pedersen_commitment_parse.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ] + secp256k1.secp256k1_pedersen_blind_generator_blind_sum.restype = ctypes.c_int + secp256k1.secp256k1_pedersen_blind_generator_blind_sum.argtypes = [ + ctypes.c_void_p, # const secp256k1_context* ctx + ctypes.POINTER(ctypes.c_uint64), # const uint64_t *value + ctypes.POINTER(ctypes.c_char_p), # const unsigned char* const* generator_blind + ctypes.POINTER(ctypes.c_char_p), # unsigned char* const* blinding_factor + ctypes.c_size_t, # size_t n_total + ctypes.c_size_t, # size_t n_inputs + ] + secp256k1.secp256k1_pedersen_verify_tally.restype = ctypes.c_int + secp256k1.secp256k1_pedersen_verify_tally.argtypes = [ + ctypes.c_void_p, # const secp256k1_context* ctx + ctypes.POINTER( + ctypes.c_char_p + ), # const secp256k1_pedersen_commitment * const* commits, + ctypes.c_size_t, # size_t pcnt, + ctypes.POINTER( + ctypes.c_char_p + ), # const secp256k1_pedersen_commitment * const* ncommits, + ctypes.c_size_t, # size_t ncnt, + ] + secp256k1.secp256k1_rangeproof_sign.restype = ctypes.c_int + secp256k1.secp256k1_rangeproof_sign.argtypes = [ + ctypes.c_void_p, # const secp256k1_context* ctx + ctypes.c_char_p, # unsigned char *proof + ctypes.POINTER(ctypes.c_size_t), # size_t *plen + ctypes.c_uint64, # uint64_t min_value, + ctypes.c_char_p, # const secp256k1_pedersen_commitment *commit, + ctypes.c_char_p, # const unsigned char *blind, + ctypes.c_char_p, # const unsigned char *nonce, + ctypes.c_int, # int exp, + ctypes.c_int, # int min_bits, + ctypes.c_uint64, # uint64_t value, + ctypes.c_char_p, # const unsigned char *message, + ctypes.c_size_t, # size_t msg_len, + ctypes.c_char_p, # const unsigned char *extra_commit, + ctypes.c_size_t, # size_t extra_commit_len, + ctypes.c_char_p, # const secp256k1_generator* gen + ] + secp256k1.secp256k1_rangeproof_verify.restype = ctypes.c_int + secp256k1.secp256k1_rangeproof_verify.argtypes = [ + ctypes.c_void_p, # const secp256k1_context* ctx + ctypes.POINTER(ctypes.c_uint64), # uint64_t *min_value + ctypes.POINTER(ctypes.c_uint64), # uint64_t *max_value + ctypes.c_char_p, # const secp256k1_pedersen_commitment *commit + ctypes.c_char_p, # const unsigned char *proof + ctypes.c_size_t, # size_t plen + ctypes.c_char_p, # const unsigned char *extra_commit + ctypes.c_size_t, # size_t extra_commit_len, + ctypes.c_char_p, # const secp256k1_generator* gen + ] + secp256k1.secp256k1_rangeproof_rewind.restype = ctypes.c_int + secp256k1.secp256k1_rangeproof_rewind.argtypes = [ + ctypes.c_void_p, # const secp256k1_context* ctx + ctypes.c_char_p, # unsigned char *blind_out + ctypes.POINTER(ctypes.c_uint64), # uint64_t *value_out + ctypes.c_char_p, # unsigned char *message_out, + ctypes.POINTER(ctypes.c_size_t), # size_t *outlen + ctypes.c_char_p, # const unsigned char *nonce + ctypes.POINTER(ctypes.c_uint64), # uint64_t *min_value + ctypes.POINTER(ctypes.c_uint64), # uint64_t *max_value + ctypes.c_char_p, # const secp256k1_pedersen_commitment *commit + ctypes.c_char_p, # const unsigned char *proof + ctypes.c_size_t, # size_t plen + ctypes.c_char_p, # const unsigned char *extra_commit + ctypes.c_size_t, # size_t extra_commit_len, + ctypes.c_char_p, # const secp256k1_generator* gen + ] + secp256k1.secp256k1_surjectionproof_initialize.restype = ctypes.c_int + secp256k1.secp256k1_surjectionproof_initialize.argtypes = [ + ctypes.c_void_p, # const secp256k1_context* ctx + ctypes.c_char_p, # secp256k1_surjectionproof* proof // proof size in bytes is not specified + ctypes.POINTER(ctypes.c_size_t), # size_t *input_index + # NOTE: use build_aligned_data_array() + ctypes.c_char_p, # const secp256k1_fixed_asset_tag* fixed_input_tags + ctypes.c_size_t, # const size_t n_input_tags + ctypes.c_size_t, # const size_t n_input_tags_to_use + ctypes.c_char_p, # const secp256k1_fixed_asset_tag* fixed_output_tag + ctypes.c_size_t, # const size_t n_max_iterations + ctypes.c_char_p, # const unsigned char *random_seed32 + ] + + secp256k1.secp256k1_surjectionproof_generate.restype = ctypes.c_int + secp256k1.secp256k1_surjectionproof_generate.argtypes = [ + ctypes.c_void_p, # const secp256k1_context* ctx + ctypes.c_char_p, # secp256k1_surjectionproof* proof + # NOTE: use build_aligned_data_array() + ctypes.c_char_p, # const secp256k1_generator* ephemeral_input_tags + ctypes.c_size_t, # size_t n_ephemeral_input_tags + ctypes.c_char_p, # const secp256k1_generator* ephemeral_output_tag + ctypes.c_size_t, # size_t input_index + ctypes.c_char_p, # const unsigned char *input_blinding_key + ctypes.c_char_p, # const unsigned char *output_blinding_key + ] + + secp256k1.secp256k1_surjectionproof_verify.restype = ctypes.c_int + secp256k1.secp256k1_surjectionproof_verify.argtypes = [ + ctypes.c_void_p, # const secp256k1_context* ctx + ctypes.c_char_p, # const secp256k1_surjectionproof* proof + # NOTE: use build_aligned_data_array() + ctypes.c_char_p, # const secp256k1_generator* ephemeral_input_tags + ctypes.c_size_t, # size_t n_ephemeral_input_tags + ctypes.c_char_p, # const secp256k1_generator* ephemeral_output_tag + ] + secp256k1.secp256k1_surjectionproof_serialized_size.restype = ctypes.c_int + secp256k1.secp256k1_surjectionproof_serialized_size.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ] + + secp256k1.secp256k1_ec_pubkey_serialize.restype = ctypes.c_int + secp256k1.secp256k1_ec_pubkey_serialize.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_size_t), + ctypes.c_char_p, + ctypes.c_uint, + ] + + secp256k1.secp256k1_surjectionproof_serialize.restype = ctypes.c_int + secp256k1.secp256k1_surjectionproof_serialize.argtypes = [ + ctypes.c_void_p, # const secp256k1_context* ctx + ctypes.c_char_p, # unsigned char *output + ctypes.POINTER(ctypes.c_size_t), # size_t *outputlen + ctypes.c_char_p, # const secp256k1_surjectionproof *proof + ] + + secp256k1.secp256k1_surjectionproof_parse.restype = ctypes.c_int + secp256k1.secp256k1_surjectionproof_parse.argtypes = [ + ctypes.c_void_p, # const secp256k1_context* ctx + ctypes.c_char_p, # secp256k1_surjectionproof *proof + ctypes.c_char_p, # const unsigned char *input + ctypes.c_size_t, # size_t inputlen + ] + + +secp256k1_has_pubkey_recovery = False +if getattr(secp256k1, "secp256k1_ecdsa_sign_recoverable", None): + secp256k1_has_pubkey_recovery = True + secp256k1.secp256k1_ecdsa_sign_recoverable.restype = ctypes.c_int + secp256k1.secp256k1_ecdsa_sign_recoverable.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_void_p, + ctypes.c_void_p, + ] + + secp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact.restype = ( + ctypes.c_int + ) + secp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_int), + ctypes.c_char_p, + ] + + secp256k1.secp256k1_ecdsa_recover.restype = ctypes.c_int + secp256k1.secp256k1_ecdsa_recover.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_char_p, + ] + + secp256k1.secp256k1_ecdsa_recoverable_signature_parse_compact.restype = ctypes.c_int + secp256k1.secp256k1_ecdsa_recoverable_signature_parse_compact.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_int, + ] + +secp256k1_has_zkp = False +if getattr(secp256k1, "secp256k1_rangeproof_info", None): + secp256k1_has_zkp = True + _set_zkp_func_types() + +secp256k1.secp256k1_context_create.restype = ctypes.c_void_p +secp256k1.secp256k1_context_create.errcheck = _check_ressecp256k1_void_p +secp256k1.secp256k1_context_create.argtypes = [ctypes.c_uint] + +secp256k1.secp256k1_context_randomize.restype = ctypes.c_int +secp256k1.secp256k1_context_randomize.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + +secp256k1.secp256k1_context_set_illegal_callback.restype = None +secp256k1.secp256k1_context_set_illegal_callback.argtypes = [ + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, +] + +secp256k1.secp256k1_context_set_error_callback.restype = None +secp256k1.secp256k1_context_set_error_callback.argtypes = [ + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, +] + +secp256k1.secp256k1_ecdsa_sign.restype = ctypes.c_int +secp256k1.secp256k1_ecdsa_sign.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_void_p, + ctypes.c_void_p, +] + +secp256k1.secp256k1_ecdsa_signature_serialize_der.restype = ctypes.c_int +secp256k1.secp256k1_ecdsa_signature_serialize_der.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_size_t), + ctypes.c_char_p, +] + +secp256k1.secp256k1_ec_pubkey_create.restype = ctypes.c_int +secp256k1.secp256k1_ec_pubkey_create.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, +] + +secp256k1.secp256k1_ec_seckey_verify.restype = ctypes.c_int +secp256k1.secp256k1_ec_seckey_verify.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + +secp256k1.secp256k1_ecdsa_signature_parse_der.restype = ctypes.c_int +secp256k1.secp256k1_ecdsa_signature_parse_der.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_size_t, +] + +secp256k1.secp256k1_ecdsa_signature_normalize.restype = ctypes.c_int +secp256k1.secp256k1_ecdsa_signature_normalize.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, +] + +secp256k1.secp256k1_ecdsa_verify.restype = ctypes.c_int +secp256k1.secp256k1_ecdsa_verify.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_char_p, +] + +secp256k1.secp256k1_ec_pubkey_parse.restype = ctypes.c_int +secp256k1.secp256k1_ec_pubkey_parse.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_size_t, +] + +secp256k1.secp256k1_ec_pubkey_tweak_add.restype = ctypes.c_int +secp256k1.secp256k1_ec_pubkey_tweak_add.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, +] + +secp256k1.secp256k1_ec_privkey_tweak_add.restype = ctypes.c_int +secp256k1.secp256k1_ec_privkey_tweak_add.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, +] + +secp256k1_has_ecdh = False +if getattr(secp256k1, "secp256k1_ecdh", None): + secp256k1_has_ecdh = True + secp256k1.secp256k1_ecdh.restype = ctypes.c_int + secp256k1.secp256k1_ecdh.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_void_p, + ctypes.c_void_p, + ] + +secp256k1_context_sign = secp256k1.secp256k1_context_create(SECP256K1_CONTEXT_SIGN) +assert secp256k1_context_sign is not None +secp256k1_context_verify = secp256k1.secp256k1_context_create(SECP256K1_CONTEXT_VERIFY) +assert secp256k1_context_verify is not None + + +def _set_error_callback(ctx): + secp256k1.secp256k1_context_set_error_callback(ctx, _secp256k1_error_callback_fn, 0) + secp256k1.secp256k1_context_set_illegal_callback( + ctx, _secp256k1_illegal_callback_fn, 0 + ) + + +_set_error_callback(secp256k1_context_sign) +_set_error_callback(secp256k1_context_verify) + + +def randomize_context(ctx): + seed = os.urandom(32) + assert secp256k1.secp256k1_context_randomize(ctx, seed) == 1 + + +randomize_context(secp256k1_context_sign) + +secp256k1_blind_context = None +if secp256k1_has_zkp: + secp256k1_blind_context = secp256k1.secp256k1_context_create( + SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY + ) + assert secp256k1_blind_context is not None + _set_error_callback(secp256k1_blind_context) + randomize_context(secp256k1_blind_context) + + +def build_aligned_data_array(data_list, expected_len): + assert expected_len % 32 == 0, "we only deal with 32-byte aligned data" + # It is much simpler to just build buffer by concatenating the data, + # than create a ctypes array of N-byte arrays. + # with fixed byte size, we do not need to bother about alignment. + buf = b"".join(data_list) + # but we must check that our expectation of the data size is correct + assert len(buf) % expected_len == 0 + assert len(buf) // expected_len == len(data_list) + + return buf + + +SECP256K1_GENERATOR_SIZE = 64 +SECP256K1_PEDERSEN_COMMITMENT_SIZE = 64 + +__all__ = ( + "secp256k1", + "secp256k1_context_sign", + "secp256k1_context_verify", + "secp256k1_blind_context", + "SIGNATURE_SIZE", + "COMPACT_SIGNATURE_SIZE", + "PUBLIC_KEY_SIZE", + "COMPRESSED_PUBLIC_KEY_SIZE", + "SECP256K1_EC_COMPRESSED", + "SECP256K1_EC_UNCOMPRESSED", + "secp256k1_has_pubkey_recovery", + "secp256k1_has_zkp", + "build_aligned_data_array", +) diff --git a/tests/device_tests/test_msg_liquid.py b/tests/device_tests/test_msg_liquid.py new file mode 100644 index 00000000000..6b305c640b6 --- /dev/null +++ b/tests/device_tests/test_msg_liquid.py @@ -0,0 +1,474 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2018 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +import ctypes + +import pytest + +from trezorlib import liquid +from trezorlib.messages import ( + LiquidAmount, + LiquidBlindedOutput, + LiquidBlindOutput, + LiquidUnblindOutput, +) + +from . import secp256k1_zkp as lib +from .common import TrezorTest + + +@pytest.mark.liquid +@pytest.mark.skip_t1 +class TestMsgLiquidFixed(TrezorTest): + + INPUT_AMOUNTS = [ + LiquidAmount( + value=2099999199946660, + value_blind=bytes.fromhex( + "5fa920cecd0db99028e5191e60001b29d36e853d240b78461b18aec61231d206" + ), + asset=bytes.fromhex( + "230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2" + ), + asset_blind=bytes.fromhex( + "e3eab79f0bf70ef52008cb5f3d4f0c837a5345ffaa32061c5f9326118eeb2901" + ), + ) + ] + # To be blinded + OUTPUT_AMOUNTS = [ + LiquidAmount( + value=2099997399936660, + value_blind=bytes.fromhex( + "4420823cfde6f1c26b30f90ec7dd01e4887534a20f0b0d04c36ed80e71e0fd77" + ), + asset=bytes.fromhex( + "230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2" + ), + asset_blind=bytes.fromhex( + "b07670eb940bd5335f973daad8619b91ffc911f57cced458bbbf2ce03753c9bd" + ), + ), + LiquidAmount( + value=11_0000_0000, + value_blind=bytes.fromhex( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ), # to be rebalanced on device + asset=bytes.fromhex( + "230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2" + ), + asset_blind=bytes.fromhex( + "189c24279e9851d5814204136feb5713c166b13269dd63fc35c797ff08a6cd90" + ), + ), + ] + EXPLICIT_AMOUNTS = [ + LiquidAmount( + value=7_0000_0000, # explicit amount (not blinded) + asset=bytes.fromhex( + "230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2" + ), + ), + LiquidAmount( + value=1_0000, # fee (not blinded) + asset=bytes.fromhex( + "230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2" + ), + ), + ] + + BLIND_OUTPUTS = [ + LiquidBlindOutput( + amount=OUTPUT_AMOUNTS[0], + ecdh_pubkey=bytes.fromhex( + "03e51618ad58667e40208978c4dff3683b694154dc7552143c3779d30d56881220" + ), + ecdh_privkey=bytes.fromhex( + "fa0ff0169dc9575674066676cfb0b4eb8902c44269da1cf6ba66d3f8b6d4b100" + ), + script_pubkey=bytes.fromhex( + "76a914f24bb0c13089fb711ecf5c133e7df0818e1b1a5988ac" + ), + random_seed32=bytes.fromhex( + "a9ea0e755a5c2e8210242a08e7078f7f89385eb09423555182568b96e8a4fef2" + ), + ), + LiquidBlindOutput( + amount=OUTPUT_AMOUNTS[1], + ecdh_pubkey=bytes.fromhex( + "023407bcc7467fbd727f408a87e129a0c9a61ae0f05da8cb916ed1d69e8b7290a2" + ), + ecdh_privkey=bytes.fromhex( + "095066a745addb6d8831c2b0f87821142b4456556d89aa82bcadae3a9578fa45" + ), + script_pubkey=bytes.fromhex( + "76a914248036444a09a71e10ff91c8034ba213a179af9d88ac" + ), + random_seed32=bytes.fromhex( + "35a414d025c24b40ae3ac127722988ba973aea8d37179706072ed33a14607ad7" + ), + ), + ] + + BLINDED_OUTPUTS = [ + LiquidBlindedOutput( + conf_value=bytes.fromhex( + "0916f3edd39bf22ccb0fd0a88ef58bffdc664c91d1f36b5b362de28be17ef43239" + ), + conf_asset=bytes.fromhex( + "0a706c19c4b7698acfb620a8966d5c256b938c100f8e885e57e21e8c3761916853" + ), + ecdh_pubkey=bytes.fromhex( + "03fab5f773776dfb8b14226ae7b9981757c56c667fda28e90d343786a8fbaa72d1" + ), + script_pubkey=BLIND_OUTPUTS[0].script_pubkey, + surjection_proof=bytes.fromhex( + "01000188f65964cf16dd257486fc4331928dbba045a50f702dc948c38eb7c2d9684ff5ebd22d101c5bb2d0060264eabcc94e630bbf9eb5d0205c0bd63166a53b261194" + ), + range_proof=bytes.fromhex( + "603200000000000000010f3f370161b262fc9e0f200d15d38f1c7452f35ca4ab091f325551c4a060c94eee160dc5881b42ae4b384482b0b9dbf9c25a72fbbaa0545a7d2a784d2be471a22af7b444efc416ca77d6ae24146c63b766da445daac910d9ae911007905ec096cb74066cdb728a81c6a2aaa62de69352da60491a6788eee093429757832a6cc22ba2948042d898a8a94eee22e16292e1f78df489ee4f07a17b406a21fe75a2a312661af694e33be69e0b6bf20dca39650736ebcfad392bfdfbf768d66cad2e7dded69322827e1de7db608441c1e38bde98afc1e96d3de3933673ecfbc8c586a5a382012167bb25e5fd895fe613a6a6d9851f56e2db1c0d6a224e190a7e37821244864446e0178886d05ab12260eee78068e26007d4e83afc67e483c3c49295d44b17cff02ebe6bf27d225aa771ec4dd5aa10020f19d494990274b13b7c0022a7ed86ce07a8d21d30cd9d4fb35c3194fe3d6c127f600f5fc2966bc90cfbecf996f8e8fba86a1d473fc54f3a5f9d99d5379b7b3a41285f494926f439ff510e1d3e8d6c20e87f8ed1c242e3219fe4c4f6de31b16c00b3d43bd86fe15765dd9d8a40a9831d80e1dba603bba36097127eec11416bc45679b9b35a1e6b07086a63c3bbe22fd5bdb044371936bcf7aad27da16be98c3db24e43df3c9dccb081fd55fe361806ed56d49e9c71d38978efd31f531c25b80d89c43e476782823d52e45efdf84c4fd2cf283c66a0b3d9517c1d5b1a25b37ec88635c0b867e89191cff5c5fa0acf0c9d6d26d1f7e403e8e2af02f9f5d133678fa19276243ed0b93f390e06224271bb8d7f29967600c71feb9b5497a278108a1c2f7fc50fc7209945b3360181e8ef3ce5476d7c287c25d5d0581ecad1d30f453f68d67e0523356e212c78d4d9080f7011c1b4baae1962560b8c6e28e74ed56b4fea78f1639a2edd4a34ee848268eff30cc9cdfb049bc91bb1c9968a6291da7ae1bfc7ee97a1e76b361fe92dec4dedfd3c4227b2e8d8d8c948b9402893658dfff14dc18b79d65da17ce5c4c5309f2d3948e3a69ffac29e66f35cc1a40f502a314950378260524e655175f8477307139540fa9905e3e57268e3f3bcfda8a062811854c02be60f71111c6ca44d5eaef80953cc3a6eb9ef7e24e9cd416776eae5a21b8c5d351f203529ef495cc54188b34cb6c66c8362467cf9089fd2b0ed3ccd698d867d566551600143281aa802143a01a2d85c9a85bd1bd75518b767788c57c04f1040d566f05c9293a615510415b9ec3186f815293dc0376988bf4e356020ff0d2ec4ee769dead1ab8c32c63f9ebd85d595fd995472c5cbd778db75d1d53bc0391f9e9ce5cf9d42b9ad919087ea6d7ec0b6b7749c6e91ef062fa9c3803805a55a0532be785a7172454acf73e7132cb9e0b036a823dd895c86fa880786aaace6900709a3c29abc7b69e7dfb523ba482ed324f547d5e9a20bfadb9d85175e5ee8b8e1aba9a5c8ad22409f8fd6deac641f33c9bd7c6b4a8b8c05510bf6e06ad19f9f90177f7511db1972db8579143c7d4709da6b50c968bfe1eae9fb7e11c67b2dd1b53bfb1e1fa5c500fe752101af8793010bc38129e17c9fcb71891012b250bf78bac68358383a14c8c8e26a30594457074d6d530c26472e821ea153311fcb5be6b433b5ee716b208802fb27887e75642f382aaf81ec1f77abc9ea71e9bdb589d7b3ec74af7ba022c90c4ab1acd00c5d9810b0243c9c91e988ad709edc9d9f664791cf6c4ed5b30b0d63dace054f635605a990d6d7288d5095fffae6316d5bb03f095a8fb4c118801ffc16e4dddf9afb05e38cff24605e0403d05a10f6234fca829e93ed6086fb9c255517213e39bb724375c310a99a194d016c17f7eac8ec953879e07efb72f79b5ba5e3fbeaf734023327fc867e825450afa23a18b27183396f7a46683d11f81f68db99a6753b9a3670e51fe81c2482b15a4e0938626c712b4b1be0ef83f2db7727455eac52e76af3e13ad2e12c7ba4c50f68f53b036ddf2c69140e8a84ab14279b34000e16fdc9bab5ec3e5b00cc0c7e4739ae778c496f94808429006e7dea90294e68c9d2c4e4e55d31f78bfe19da947bdf4963fe27516d4a464d72a26685b7ad8d471a7e54a79f20f95124fdae04d89bb8a057562a066e1dece6b3d9c00dbf6d7da910e3513de0b0db99e2c9eef538b86b55c46969a798be9150305cff5de1e1588e1cc4d5d42521e306aaec91b276f1f97f031e4b9e50991f75cd8baec2940feca28ef1d2205d9f887037cc4cb0b8d30f6ffbd5e878b7f82391f1094764d2c6bfa1a476ac24b3c63f4263381df535760ab6054239f517efb5a3efba63a2b747d01e5d064f0cf712f3682379c83ec55a87d302aefca3d91c78ba1d6203d0dfb1d71804f4930f2565eccebbb59af9f6be4f66301b86151cdb3d6ef22dc0752fa24ad96cfe33753e37e86862ce76fe8482b68d6ad97ad3f92cf118e0e4fc5e8114d6c3d300e7987b00d6def749f17a2fae044acf24dd7f8efb939357ca16cf32129bf98de0ad2fe82a312ccb456af4b4f2a881f8345201211343d01e3d97fffe272e968775edfa83636cfe2865956f4d80fcda467a861ebb326722b2f0f5db4ffae60f999c5bf1e97a74bc987e317c1dd94aaced7f8dcab3a3fc8a35cb9ed6b68fb715885fd7f6196ca1d7c104381eb4795bbe533c64b5b2407d74c19c9acb2bb83d0d680d475696c4f7277f8fc75e5cdc032b0237092df706f98a126883f160c8d24293b0e426a93cf3b49a28cd93a9d28e6eed7f1d21630c4390534e317fcdc32518da0cef28320dd9ea2d79ef4b7f72e59b675d42fcba94d146ba6efe71e1f8272484daf9c261d57095698376db8af2b39d29f9ae23e7f25bb36b414134f5d6286fe6afea6424648a6c78c930bf666799a499778f20e93a713ed725e3e26c2d28e02359250ac35b7c8239285a9e72594f94f656ed737829c16f8d096b8640c6fa993471cdbdd08875d4c230e5ca48303c2021beb4fa0b6755ffaccdd94cdc9f6b2421afbc9b224aa76be8e6a684170b6b0022c714d252212f615296b083319c0ed9cbadba7d3a5be0931bb5154ab55ec51ca583e487c4addcb8bd5855fa070828adea592daa04a40ad69191d089ac71a9f5dd673df9c33e3f55332a788756fb7c0a97386d51fdda6c1b083d4345837dec96a4a56fe9a4f52afb626f173f171bbad5a7b6fe836cc551338f68389bcd67956eab8299dd3a4dd1bdecf5bd4f643e3d738fe3da589ff45236f0c74b29e110046dfc4415aefa3e31011de80aa92b268bd0a5e14bd63e6d084a0e4a3adc791c247e1e702330806f50531240a6c14b6af990c846b937a72b35bcf15c4851332f9e62efb8107da3442c6d46106f5a2105ef1826349f71c5175986598699c6573abb881ac18bd7d6a70711af49bad0c88605cab8366709c251b22defcaffba5019c4daa5a6cf521933d42b9a2629e48500597df369215e2b115d89ff2bd85fbeada7599542b6afb3526e62cbda2b243f6388fa7cf664ed6ff68df09060af7bcf5200ff2e2f77bb591d265288f1a6b4918793ea9a20eddf9ce4a0d4855e6cd6976bff3e2009466d0802dffe612275df9fc9efa50d8b00df5727a06a47cee7bbfbf8821e89d4318c90a397c8957dfd81e24ead3ae46c5c86dbac766492b94f30976ebfc355f1dc25cd5c2a214a6ca451cab26e0b825bd228993583a454775f43cc97b97ea30ebaac9b565d6366e5ba0b18eaff98c8d6b6cc957e07f2896d42a20809ecc6d4f25f40090bce7d80ff6eeae90b865443ddd56c2746380a71826420cb29466fff63f697f574bd51143eb201a61f8afdddfce871389826105bd0ebefb0dd01f1f3d844d7d179ec249daa756789d9e369596c79cfb2c5896c8c27cf20ec79dd7a1188eb1ea938cd22229e73c359cd5f537acd02bb843cbe2100cf36a8212d3d2fca6737ddea5f9c99859280ca407fe879bd0b6adaf5f37d91d9c629f214235a4d92942a5e4dae6e710406c175aab3b39aadf792343ea9716ce975dc256852d80ab88c6bc03f89095e9915d863fb29346950bd40af6f0e363ac908d26ce6cfe87a4d20674b7528df0e80a2490417b73d9808a53780409e7e8d39025ecfd0ae7836ae21b934a3b09a9e8aa3fb10532f1640d987779d502cf86d0035feb1c39b9e0eac58f27b29bdebedd2a3af0415774955d28788481c89c954843b89d3e2db9da774fde76b661f37534685ae28bda52455c077cebcfac901da021e6fb10e6627e41bb719424ee6b51f90038fad9aca3fe4329cd1b0d566f609301abb3ff88b1a5ed4d1129a7bc088bd9fa1da88be439a39c291f5accd9ada308599e46f333d2d3cf25335a5b9421c9709556df3f4f6a3a8c7ca708b412b64dac7cb0452d47dcaf4f3a71feb13b6ae5a9c49c071c622a1793312346739fdbf4318e0fb2149c2f83181c3802c8df9ee0ea14404c3a3ccf40afecf3ef18976c9fa274b946162fbf9f4dd0de764874a969520337fa034a69ad55ba799f6c32171526823430ab1bf79a57af74c4c775760761942b430b49618bd9127d06fa9394bd4bfa2efe9a3ce740a7c0da66a1b13ac1f64a1db5ddc28ed29253f4e3b7fd64b3ad2d95a4fb342d8e9335e42bc186736b72b9fc6cecc3faf0566d60222b95e09cbde3cd47079f0bc14a35d48d18eb30ffd45c8ed844f897aa0723f25ab54b8268af927d604257325457347ac849dd97af2fd63dcb1807977d9ef9c0496b3d9717e7d36cf4f9f7e0d3fd5a251bcffb643693f9b4d9448c902ab029a1ca73319ef87b98e45f94c7869f3207a6e825a26b93a0c7ca40dbd616e60f039b420359fbc598e70f0683c214338316327e904739963c6448c9cadef52331d52755a26cc820cd5a73a106a2467b4569e906c11798cbd0cecca0b9400a64a7268904c5a2d9c4c22410e80393f3c29293c2bcf26b20a38cf31432019eae0d0eb60cf70350e1dac095ce8f55f65928ddb633ed04d9a82c4ea53311c7d3405da89e1f13010565be8c3ad16371e93feeb846d7b4e8b1b49b5164814f59f9943ce36000f08049a8b782637356df3d5d4f1c04007b3ae1700256903a5fc72b3efdd23ad74200e37ac74eb98ff6add09c0d83429722697a8155e0aa73c8a626b83897c516240241c981d554190700992681d1c80269a27474d762ab817c71652d35ca9a7563ec911e138a29412b690efe7c6d17b1b2b0aa4a23de2599015c1fe0784bd44614fe498b8204928531e7944083bb0c01196770ce0ee7752f539b9f876c9660c821694daee5d100008cb76e2dee919227deabf735b4ba5fdd0b5ff6b26cab9d03689fef2884ff3993171a8718d906e872066144458315f6c7a1178d4e50e55b6a059e8fca900e99a7bd54530f071c01fd7d92367de76557275a0dccda1241d96dcab684719d128217551df2e593ebd75f5c58f4f8a6b415dc60f13f639ccec4987b26751a53d9d065ccc7e3b836bb3f4ed974060d4ab5708473665bdc7b3a71f9df4ef88a946253b83bf7ccd999c14015b717a60f0399f85e4f499b5e811c26b8aac1820afbdb4e239f290bef1e6eca90f36d58b85722c29de40d3bb4bec875a8861af7b553181fe4139b01f9ea12e0dc5a32195766222e23add937580fff4fbec96e817c0fd8c717b898d115be1946fef895cf663fed351fa8196261cd60ba79fd5ce01abed4b1e0ee78123e95756f2850dd63b19f2bb50f611dd8f1d2eda7cdbdc929725f1c9106f6ffda10cee89974140caab66a2b315f6cb609b090dbbfd2e52f4fd6a" + ), + ), + LiquidBlindedOutput( + conf_value=bytes.fromhex( + "08cbb1068fcc876832b14633b9d048a678398abe4a10b0497f8351810a3fa2046e" + ), + conf_asset=bytes.fromhex( + "0a37c228ce75c9d15c9a45b39b837675705eed3f5fcd32681c7bd2b91019f35ede" + ), + ecdh_pubkey=bytes.fromhex( + "02b7b6f81ad08e284922d8b0d7bb0f73a35e3fdfe97a91cad787f7bbfb416fa1bd" + ), + script_pubkey=BLIND_OUTPUTS[1].script_pubkey, + surjection_proof=bytes.fromhex( + "0100019da19bccf1d87248d87057daa99ec55b7ba44a76488f234f895f943419404631758c156ce2307e2f5d739da7356a705e820ccf730a812f7e01f471842fa5329e" + ), + range_proof=bytes.fromhex( + "60230000000000000001ac9c01ab64cf8d65a5fdb3b2198ad241e6688380f0c690eec965c441c236ba88b2fe32ee71a0979e751f8df7ea29d5227380a9e92416fb9cb43793eb41d536d986a94edb5bfc1ee5983ed7fad2c7f3fd4bd8a1de875938bc5a32ad377ac8edd044d876c71c8f95e51aa9eab5190ba31049a3cf295e745528c9cb61d7229978e212ae5197764ad76c05d31f6887792e3f25f2d5aef29fa114707434302164e6aa5e2f521fd55e80759d7e45a02c502846c36b23613c1c62aaee78715541d4346cce2d74baa08c33c91800f049514d9c09f4d451adb411c9a5128f4047e4bfe025f277a906a3c9318b2d032b2b1b76b61f231c6b95bb3c9a67d4d39a3140d7e05d2f2073af8b35814533b497c09275409e6aa81c8521297f82cf23c363f7637d53cee6dfb5c44c10e535d66ce2bc26e423d0eeaeaa520ad671e24f287985a39349b5c694490e25400293fcb4d4e1557fc1b0ad4bb3dfeefe486d6b72c40d0e1d789bb1cc4b4528a470f65fdd4560ded449bff313e5fa39dc9534ce02446c354a9a1e597b13e8fbc816d8247b1057559b7f410042241a91f67df831a3688c78a2d7363f7c59b5be5db86097347abe1ccbd5fc8a4f64fbd3b61143801f21d05229950210544bf44adc12ecf86fecce2314cac965d581dbf6163f9752e7bf89f63c4d0d5e3682f8b769feb37400d43054d9f103de50002a6cec869973ab67c4dd8f0e9ef075e161744f6489fe05d1ee9f02fd3dd8e160d4c045d9f4407d5ebd47fcb0f4a0fc6809c8fbcf083cc50d60fc5d2ceb2b259f404d7aaffc837d31e63b61537fbbf9847cde74b52f3e7cb96b286c4be51d9f9af3ae7190b7a530a97b172774be8a3969ae2de0cb4c6e7d2437d85cfd215b268eddb5e21df66d30f10cb42fdf6a3652d2e2140b36b1830a5b7a9c3008d4c9e49c6b47e30f5d9d51d768f977b6257c74e6a9959ec63b6906fdd3d2e1341af08938790845682d2484df46b5b472be751019825c9394da295a03a695a23ac2a9af78c37f5daaeab3f29e526311a677792d4551e01bbc8d86a99a649fe69ee2895a672483de7a5ed277cc9770beb05432e4f68bc6c3a16294a4c34510b099db17827f1af8d679ae1ba65da7279819c2d2fd9002b67d071e8f4bbb1d02d53041d7bac4503f9e39413ab7dcfd0f3f36c1217ba3bd5f4c534ca183c1ee1cbf15b4dc477aeddef5426acf8983ecfc2685ad6cf13a0c70beaff5e9be045037b55ef44d1aa357ef833bd422b1f88badbc38efb8ec4a8d6410d42eb9898f229d801649623b01ffaea326ccc1b123b051c239035b011b84e0e5a0041d45ab3b9c68e55fe1afe4611aa9f7e2f87a2b295229ee4c7b46283969c52e1c91304e2c08283579a53b425caca16ee9dec2e86f3d98623a984a8a36c5f1220b890f3eeefea9f4996f2df154aa17eeab760fc7e357c21596ca553b6042401fd3f68dce2b682a3ac49dc5a3b14506d36df2d506b222bce216bc066f9da5dc78bb536551db67ab5decd42ec957f38d0ba25db7a4ff0d8515ebb2e1ae11b28a882a5bfa679c8bc54484f370ac8a5eef4fc9f2ba3f54ab66e587304590d69ecf358a80ef60fade5f4b17be0aa60ccd6c98040638c7ae8832521f39b799284e94a4e2f6f41d084f0ad6418daf879cb12f456a0da5b5d1000896ffc85403b6290ce0d1431e6008a0ab1a2ec843f1466370a2b5a5afc2ecbb029aab32cfe8960a4a41130d69ab9ec30b9675027e61cadf098c3bc6aca6083f31312e7b83e46324ab27bda9896924b77a12409c709b39d7c649398451244d6f1847e93ac72ececc59f39d86fcf3f57765e092176a7789b1c0c4d9184cd34f1e09389e57a6052663104a95803e79001e79440e63cfed99467d43b16231733800f5d71c1f1955bee4d9aa34054b56d55bd7143aadd67be00f3e998601599cab277db79e5817a06bbb67676bf0f775507f42517e314801b766ae274f2ccad512c2402505ad62d145e47c51007344d4fb8a71e5bb1309367c51a2ab53b355592635423ac71b22e7622f078d6d0011fb18b5438196ee771694b7be9042a1de0332bb7a2e3245ee287467e32699df6bc65ec0be19c26d5b6ad7a966483a351feb4efe35759af3464d89fc62afa21a05ca511b60088ba38c7537210382f599bdb7387395279b0647bcf367c7e396f9c6fcb77f8ac161f0c8e4e5747f0754ed26b549aee99ce10edaebd1111231451a0a3c718d543df03efe89053a9deef89c66b4149da9460b6c6221aab7db2d36b9d49b63b488f560fbb51bbffd69044e210dfe5ef29d68c5480c7d17f0861b44d80d21c6ac4bde6198b3697da36984673abfc7ec5c5985bbf8d83cc9eb3e0663e590d54e85be8f9ce53d61f7da1f567b150a2a72ab7f575bf90c757f6c9f0179df79134a86058d2854fdb17573962c5f74068c48d53f81344eb6261161be8e7b9c424a716f1e9bf1a6d999e620dd0143657e892466bfbdeaa56ce9d81450aa3b48b1ede61cb1d4407003799fe26c23f3ae2d1cab9761b18409fdeb1df3b26cb2eb7750881f0237f97c0e0c78408d1a4ae434220813b87c66d1d8c5dbed283296faaa2ff7d42ef0e3c2acabf0b64e4e2f7f194c37ea280fa8ee723de207a20341a15fc718fec3b3df77a6d987e67d0cc5080d748e33e868193c05d0bb8686e1b06775d8f1c66605df7c979439b7860c75dd4772c6875c360afdd3ec12692c201d4adf423d6ad90c4bbfb9ac14e8baf2728b31d1a46174002a3a873308175cb5b652fc1b3ef5e35f469de2f3b635d8acd43fce4caaaf17958d6dd22fb7d56de774688885c41e459160d897d803959eac132ebaa92789c1b5196dfc2aa45f758e18c444ad706f84dc4a51fd68fe0dd6ed96a850e55683ae4430b41a5bad1c0421185a54a10068d51b1fa72f054cb61f1384c29d1669620b38d955a386199092e74287585e1465fd1eb254d1926af88046fee48040fa515a341407aad9f7a8149473324f250af82b9e09d20529118afeecbe9f33620023d7fe701e8bb2bfb1676179785611ae9896000639ba944bfd21eb9ef66028dd5752a174559a7e359c7a6d84d0c68a9b09289a904af911b5bf54aef938365e65ad98a578b4c8b0bd306fe8984c38f398574ef24319337feeceb2cb232f786c8246fea1631574be37f318aac1c51cb6f90a4be10c5a355e3aebb68c158f0bc7252e3cb1d4be40a2bd73ac761342e1b1528a8f370b239de79ff489123371d11d6617f7513fe572c53ff215bdd03b3d6662c0fc78a70b30077ca4e95d83b24042cfe26a39ec09e59dbd3079a35d7a942591abd261d26e9cddd339050a6f1aaa158cf8be3253297327845c021490829a31474bd17a482d035f9ee6b65424f3286ff6e837bbd0c7834798560a0a9bca1a1f4b25c4b3dc515bb78dd147e58ccfd821830ec5277386128d1da82aae61c8f2b741c5caa5fe12bd6cd1c4d4d5b889b8a120dea39919f8e22b36111a674ce0e3e7676a191e9bd2ffc3e4d08e45a64cc4e577e9ec757935f604acc58fbfc39ba644363a9631a4c20aeb2dd0bb5fcf5ab5e2d8b760eed65a6782665e3d777847465ec749c67dc43bb078ed0381ab95602842b8ec7edda475df97552ad2af30a450a5367632854c8454c686e67bdaafe596de39260a2b9103f365391df3238194498f7551b8532c064ba0aa656feb5e8d1c9bbee56527ee158a6674fdc38fdf502ffaedd7ebd9824bfceda559210d4be3200f45b78a9b032b28800a54c4eacd2153ca22aeb2b69ba4bdd0cc85d5f379f6c6fce1c1457706f0fdaf36f2e46f04a766b9623dbf343b3df2469dbe99ea79d20f40d514add83e7d41cbdc763994965cd7fa74195c9f8299fc923210500233c35360899a12640babd3b14cc78ff25f5d3c40770fb2f456867af0364ad182603a92f116f5846ca892f98f5dc262526f7744d4be2bddd0857c46ce628d9cd694131d41155a07f2f1f69ead242a35198588dd6609bab3bb38e543ae2c2e17a647b16f69b11d010d14c7289ee6b128f96052d3c85053083086b544da45d7f3f76c684892c3cdb007f38c97" + ), + ), + ] + + UNBLIND_OUTPUTS = [ + LiquidUnblindOutput( + blinded=BLINDED_OUTPUTS[0], + ecdh_privkey=bytes.fromhex( + "8ba892ac8508abb576daab4f966c3ed3832cc4284b4632749c1ee496d9dfe6db" + ), + ), + LiquidUnblindOutput( + blinded=BLINDED_OUTPUTS[1], + ecdh_privkey=bytes.fromhex( + "6527f295b57d3788edbfca7a911da6d62e1238946736d47ae04c714502b409d2" + ), + ), + ] + + def test_blind_fixed(self): + self.setup_mnemonic_nopin_nopassphrase() + + blinded = liquid.blind_tx( + self.client, inputs=self.INPUT_AMOUNTS, outputs=self.BLIND_OUTPUTS + ) + assert blinded[0].__dict__ == self.BLINDED_OUTPUTS[0].__dict__ + assert blinded[1].__dict__ == self.BLINDED_OUTPUTS[1].__dict__ + for i, blinded_output in enumerate(blinded): + _verify_range_proof(blinded_output) + _verify_surjection_proof(blinded_output, inputs=self.INPUT_AMOUNTS) + + _verify_balance( + inputs=list(map(_blind_amount, self.INPUT_AMOUNTS)), + outputs=(self.EXPLICIT_AMOUNTS + blinded), + ) + + def test_unblind_fixed(self): + self.setup_mnemonic_nopin_nopassphrase() + + unblinded_amounts = [] + for i, unblind_output in enumerate(self.UNBLIND_OUTPUTS): + unblinded = liquid.unblind_output(self.client, unblind_output) + amount = self.OUTPUT_AMOUNTS[i] + assert unblinded.value == amount.value + assert unblinded.asset == amount.asset + assert unblinded.asset_blind == amount.asset_blind + if i != len(self.BLINDED_OUTPUTS) - 1: + # Last value_blind is updated for balancing the commitments + assert unblinded.value_blind == amount.value_blind + unblinded_amounts.append(unblinded) + + for i, unblinded_amount in enumerate(unblinded_amounts): + c = _blind_amount(unblinded_amount) + assert c.conf_value == self.BLINDED_OUTPUTS[i].conf_value + + def test_unblind_regtest_txn1(self): + """Sent 0.1248 L-BTC using elements-cli to CTExWxzfzRCb6TMgk4F1Hjqmba3ovvPJn5HBjKuhoZPpqScXxUc3yx2dwFAYKLrLSHwUjueLe81VH56G blinded address.""" + self.setup_mnemonic_nopin_nopassphrase() + + unblind_output = LiquidUnblindOutput( + blinded=LiquidBlindedOutput( + conf_value=bytes.fromhex( + "09a25e59c2573b6bf7ec35671b972686877e53a8b76c1373a6f4a68631add9e68d" + ), + conf_asset=bytes.fromhex( + "0bf88b19f515a5487a70612cd50480390cb8b13f3c91fe1ccbe28b3a1dfa5b00a3" + ), + ecdh_pubkey=bytes.fromhex( + "03d78b3a48cffa8a2f63679d163afbf861eb4deb7d13410cffd50c954ac9f7f920" + ), + script_pubkey=bytes.fromhex( + "76a9143a56a008ea50c69036f345a8b274b35bb156329288ac" + ), + surjection_proof=None, + range_proof=bytes.fromhex( + "60230000000000000001ee0e01422814168cc985c87f33f98fdfdedf181ea041d501464b2e53d5fd4711e02d7b3e0cf7b18b54f6e1693abf453a5a2b8aaffeb71efacbe38107f6e320e7d390db3c9383b8794a4d88725002e993acaa93531aae8c51e6e1c1020c966c4717cffc6ab86016ee280bab462e3a2e1df87312fa2bdc0d052a206c77cce53d15689c9d6ccbcf1e82847b90e0dee95ae890ccd4ae06f9130ebd2ddc1ef696f3e7599e25ee90f0d2fbe6dc190ce0ca60faefe4c788e6e4f1051214df925de0d88ca561fb28058893a687acbd5c98bd4cfd92dca1c52c22ffd5b7b7db107f25c468207f5d099239c26027f29d6638e92480e7e80ffb70d201589b0923bcdf05086cdc326341c5a06a6cf8f9e18f0196db93315933f6f9f2ad740e6d2619a3dac11dc018717c41173781d15404a31eec09a33c4e547687b0b44cefa92945f2a5d8e704f435596ae33833132bd689bf5fa1e35aa41432488646bd05c6b3e9a32b22f510b5b6203d67f55d7a508e007f7b9467243db32f7be5e071ba12aecc8b9ed208afbbccf9328e927732b8ffe2b13897a88ecbc0a45313e6047982e17bf5b68ad016c8873086ac0f9e01e4c7d5e86b367be98baf3a9668bfcb1fdf080e3571a950eb2a734e4cb871732690371c3783ebac2dee364377477d6cc8a46364c59d8b7795ae0f3ed5fadb395a15236906f8ce91a0f31cca9285c3b7d40c69f9d74758d59ea513194b7e2f340fb93433c455b45f70366fb8c373956bbdb8dd0f1b4ea1f456d6139e6d4c9660b6d80a8fcee2c2805f8dfa988974c551b1fc9638eebd3726185845c1a8fa5293d9910eefb2174cf44fba7b039124826a07eb1f9bef76c3c8f5eb06a2257d309689aaba1559583f8f42baed38887b3474c40bee8fa9f3136105af3953605c6f8376d48c1003c02b8724292faec3ca50d835acc989401ae295da5305f1cb7142c532648215f898528102ece2f0c538500980f310bb832c8804f59e7e23c3a3ff9fec5d2936113ab3af329264a24addfa750443fb91621b6ae0b84d9b5babb7242c6090abd4ee378e0604fb8b796e744bc73f018224d167aac0070ecd35d6297da3137af97baa52912fcd121eda2c948b860b0c07c09fed66a844e3a07c4f5fd63fb931d967001a29138a5337483153aadb5d2633680517b91eecc921c8975367d69aef12b33b7a9a6a2d6c296490ae52c9789203848be6589df4a29f9b6bc92adfac9590620d814ff63d517b3093f7df9cff1276ba94121d3ea4ace3d0474a2ebde8fb5c9eec3b1e3fb93760ec3b56f24cff0677a92a236d6c507051661475fa6f3dcd99cba97af310993ce0a30dfdcd214c5435334581337b94b39fa4599588501dd6746efffacf46f3387b731dd96a755be0ade541fc2a4cb3274b2eac974361d669a9c6563bd905e97f0cbd8c2199fe39754546e4cde69611cb3e6fe64ed2ad810f7a74510f71e2e26bf988be40ae7e613826f2d34656fe6e358c082d800f58797b6adb13c98f14fae9f168014f836936266c257f53f99996c379bb5f97dbc31c31dd16cdf8ce504717772d890808213adcb6546a36e1b14629e4b38ccfff0631c3287459b5700737da831280a51d1d6888e708d6c8d519744f01e242939deb2e15c2c5a64d82b63d45ce360e7f86f75456d81c8aa451144d2f72f81fd69093d393d9516076e1f901b9fe81565e5de214e97f6a43921d982cf0de00b3883625887b541da4b103e01edef6138d9d0c698af727f3df242cec5a6f21b70aeb85c8a8b3975461df55d9b21b7f34f7d93584f5558a68d84a91b309c4de908e38dc23c97abddffe1a01250ffc46eca203540984628ca9ea12bf6d7bca9e09fe6fcd2dc4269895d6f256c652dec04816c833c2ee6a5502f5ecf876cab6a8edf4b6e824dd14bb86ec250a515dc8c2ab63879b11dd5fa33984bd244be2c58a62366687c48bd7982b0817fdece29369543af3084b113923661ee85f01c6733741453c6829bbc3a167b1b569afe7719da3d6b8f91161a1b05cc7e9f7c8c6798aeddc5a0aa544e50e43d857d9000770ccde17ed1c1e302c2801cfe54f8ed3fec210e4896b42f2a649e5f1faff614a8cc66a4b0481522d2890239189834373d5d993c09598893ebd01a2ef444c6124906ce1310ffd9c3152d665ba82169f451b9a186cde857246debd57c81d3daade74283ecd9130f7d3c4c9e437700b75521d5b4d3a7a75284915fcfd61fe52b50f508887fa4cfda1d6e662fda4912f6ae9a39baea02253cf2eb4bfb5f80eed7ad3f35999db28192841caf13b730a8f816798b26fd57da9eca6395b095cda83939b594c58c1a682fc7dbdc605e995275b0203b385d9a76254807b9bce66c653e769beee6058966c9442a54a5566a9e5ed9da525e110827b9164f3e9bdddffbb04acbcacf866c8be8f34c5ad7750a4cfe8c806ffa60009306aa0f773c5c95e335b9a36f7c02e1558ea961858b53f73baa3addaf4bae1d989f051b52f111735927b74837ef092fca5f5f4825399863551ca36df15f933f135b1374cea247e9e59b62ecbc17859fd3d0842a443709258467d587351ec8fef02fdd6bf10b2509d0bf648074d6513c9d18e5985649313b95a5acb5c97897787eda61fd219e8d76c2dcb7887830c715eb7305a6d948d620294b0f0b1988dade14c3679cffae15991b2041a787c652f00eb5a4ff6e239b6ec4e312e4e9628b5cfd0eb7a7ea9d4d35c599578e5ff2ec726fbb15f6ed6afd327f02d7e121735c3ccd17d46f73c1f30da1157cbba791cf06a56438f11c01764165a1dd62427c0c48a4e269f40833bf9c430ba4daa5e42d9768859639729058f019b681b01a8aff85b62efa893d14f75e4925e2db6e9b2c9440e5dba9feeda754860cda645cf05c469fa8a3a4ddde3be4f536219f7cab26bb45dc8d86d963d654060be75afa4a0e88e64dc9d9d01e7d07a1ca184212205065c95a614be8e279c6372268b4bad0397e29a448fcd66e48d145296e04be7b385b84eade6c9786cf2d42302050637298b22abe203dcf2318467d3f7fd4c3034fc04980b711c5c2e10c2666e0f57d1197c55601b4eb7debfdeb5fed8662031dd2a4e5983454b889a4ed8c9d5e06f54bab441d1cb59a233e1d8f91f8382a3cd4b44375490ea0b69c22961b7aae7ca55d98f1e55b792509a37a936566403f14239d241685893a13c96c7dbc703c165a7a4c0849a6a63a08122b3016850be8a7ef401c9de1d74eb21bd948d19749ea68fb0c6b608a84341cab6dcb142a07816925bea861af3939c7519b6ef6c814e12cbe7cd7de6304003e10049622e52a213bec6edb7a7396eaac522b1e1dfcbf07dc179d9ee9cb4a6c019ced2450c50211df2c90e8c0e1b4e42a315310fa7f2c8a91f568f180e7db747b9cd769f6eecce78710014baf7a9d8a642208e6179a67881a3051b8c2ed9d7e19d9cf46ff669dc265390d016f962eeba0aabc7a45d5ab0bc80be7f51d662c35eb88f6ad9c38ae3c6bc27c05a7b640dab025f9a1ad394bb1c1bda2428c8ff3162cde7b2d651388b541426552d579604ac5e1de291373492b11982688eeea12129bdf934b461f4000ad2b49c9c99e78710c5b15766bc749960061f233a6a5ec4700f06720967eea8ed38f4c0ad86af7a42f296e721d4d91eae6dfaf87fdb71b2851d78cec8c2ff486a8fb2f6e4a60a9936c5fd3215af9c5f78417813b521d78bed5bcfa8cc118c430865cec4ebd4d278dabcfc8ef88b0070d1ae0988e1ad1fe92ad35347196a92cdb8c96f16bc0ce1a4ce90246d0028edc6f380f857a7f8fb7b9ff05b98b063cecd006e5b1004cd20ae24212e9060e46e5498626d87e8879ed4ca4d32f8ac7da6d220e3eb149e6f6b4b01b3eb8c17db22fd49b6d121e3615caaf92b64af4423fb69d3b83a7ad58fe3304f7e5cdafca7ddf4f99bd3830926d95940065236175395eed81fe8cf1a798e61dfe710683bc9ead1db5ef5e7da73423d081c2cc70760437cb5eaa802dccef4f2580fbb2d59cf4b004cb9fedf1a54c0edbf460ff630dc25c951d3a530f4eb031863d8f2bb7bd606bc5df2" + ), + ), + # Use fixed ECDH private key (skip SLIP-0077 derivation): + ecdh_privkey=bytes.fromhex( + "6279c0444ebf95061b9fdf1aa035ee0861ab28f33708fa4ef7a9e33a5f3462fa" + ), + ) + unblinded = liquid.unblind_output(self.client, unblind_output) + assert unblinded == LiquidAmount( + value=12480000, + value_blind=bytes.fromhex( + "148f415252440c062fd7d65a9b6a7d8b30212bbf507a8c842810a42b713219fc" + ), + asset=bytes.fromhex( + "230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2" + ), + asset_blind=bytes.fromhex( + "3c21809acd1b592f07bf304af72179c8b000c14ebce712629788727103f7ee0b" + ), + ) + + def test_unblind_regtest_txn2(self): + """Sent 0.12345 L-BTC using elements-cli to el1qqf6phclaj5xd8x5t8a94vjtwu7tgh636x5tz6pkpxr9kx8wjuu8vnvcac23rv5z6djujq8aqgyw28z39ffalz62w06u8hgg4g blinded address.""" + self.setup_mnemonic_allallall() + + unblind_output = LiquidUnblindOutput( + blinded=LiquidBlindedOutput( + conf_value=bytes.fromhex( + "088c783fc7dac650506e9aa6d334d4ff9e91536ff132633e1b8bc740df19f974b4" + ), + conf_asset=bytes.fromhex( + "0a0a3c297a713cfc120ba519c2450abe7c90f6a4fb187e6db7edf3fb8130ceab26" + ), + ecdh_pubkey=bytes.fromhex( + "02d9fd9cc80d449c276821323f46c0c5ee4a7a8aa07cc403f46b3cbf8130127b4f" + ), + script_pubkey=bytes.fromhex( + "0014b31dc2a236505a6cb9201fa0411ca38a254a7bf1" + ), + surjection_proof=None, + range_proof=bytes.fromhex( + "602300000000000000012d5f01d33ac82186a76677a727b3830667a602880ffdefed417ff8c690b73f5cfc5dcc7acb78a4cc9c1af9b252b8ea7662acb43c43e65703c2a94439ca1b6a1b42b7029f8a767f4e922636a4661f9af3e35436def22b91331586166233550cf2f35ecfa2d32452bd9e1bf05d1e00257045745ff76607db09a18928f6c02b45236b216189e4ebd488b566cbb01ceb0412a3c25e59c11afbc791e6b19186d051b99b33ad95b93839fa843bca4c7e09cdbf28d8c3e2797fbd97bd48d892c632e8d3f27caab6f7d467caa4bb4a6cb9d37ac8ed97bbd18a37d856d02e018a4c1b2407347dc06d6c64eab22aeb2d6f4263574146a69ce495127c12204df487e4d93d35f39446a7842a10b346ef39edaa6a3d39f4888de180cc71e48b54826bb263dc9c6af79d13fb02b1fb1e4ee4cc261d901662c83fe9b0564c34139a4080a618421ee9c643c1601b05789ba4d0ffa5bee26275e876809bf283a90ae502af25e607552ccbb574492e11b3f4f8447ddb93ae7aedd1ef4ba7cb915b5310e740cebae5fe1e75575f9eadcb43c71e99c65c00205cc90320920b7ee82bd35eccc361c93a9056665e06e2cae31a0b9164a26be98e2ea6d8261484b4507bb40ed203e0870e9f84c87cc042318422921bf742f1294ba2096490a459b74b5d39bc87fb2e125c318d4d295cf2f8c89b31c56cae4a4795f137b89fa33c04c8a990277c88c81f4fd4f0cac18a4648d581054fabd4f1b530c9a59437295422bf7aa76a710dd6a92a102c5a6bff7794c166e995f301adcf6004cc9a21221bd1767edc87b279783b8008ceb47c0c4eadc37df551b868c0e318a1d8a09201490bd679b5fecc637934536002c21d1bbfe3c5135878a0abba9eb7e32c2a01e5c12fe71866ad9f4f935010dcd83048c179f4876ee794f471431024960f7502a55713439ae18ebbb00849ecdc59db726454ccbdc06dcdb5ea6c66627825813fe0d2351eda829154ecb9b59e055f5f24386e093f3cdc7a45547f5af250c7e1ef0f3ce07e5cd274732ac09eb9664754789dfec59b750ad005fbe5860ef90602b5e8fe6209f389e66e4db4d09bf638de0ef1a42ffc71a8f161ade734a841e081135558326baf6487dd60547310fba7b4af7a52cccdd4ac3dc65a766f9131e9cfa3846162c74b1df70195b0622abd24f3bcc926b2991d052e4bb41613798314d7b9190d480f0c46b52249bc6b8408a507acc4abfd570a4b2e1b5058cfcfe9bd8c951c630487bd6b82ca0c74f8612652101f5ece8101d176999817da0826e647458c9722553ef73d85bf74f06ad9d1d97d70f9354d83c018e11b91aa19378eab5b8f8b476a862a014a81b9734d1f1922db469b01455f7cde6b079e0b6cc71fc5ca9c9af542ef8c3ea354f613e089ba77bc1d96065ffae1ad09b84f01e57827ad0479fc09b7297c7877678c557aed1a4acfe2a8fee2c8d6b26304dee77817b5e519aff666ce34c8b2057634ecf2406c21b3a755a38c1b4c9cffebfcdce6c00f98826722e3857a17258e8bef028344a87bf410b56a734b38f042e59da37175011821c3564a6cfdb27bf4bfd6b99ea3620ee14aa5e5a87f99fdef11306ba4f8f16203c54f347cebe016347ba0fb794c7fae9f3fd060fad7be6a11884affd9de77cb876f3398e45b1e63d77ba6de755ec2719d8680c3ba81fd30fd6141d032d26d6d17104dbcbc69f165da8e8bd22e6bc80950136f3217b2de5dccd02c825e04fc5bb3cc69e355a84f7d87640ed155be9772984a3494e90d2ebe0378a2a326829a2bb293bbd2d08795ca79de98cf3e6d8e7d4f60a668f69925e0354539cbc2d02cbde907ff23384ba62afd0414332e3c5586fcaca44d43dbcd0b4c9d542e4839af29ea82118ac9e8cbc6aa8f2cf28cd291ee4c141119411143be04f5fd374368fd5557c870ca887b8508d3c45bd7f4eee24263f30d89786acc0979e194fb1e515ff0a59ff4a37f84d6e05bc4bda8b747d9be1db991cbff311818bc868f338f1d667736654811982899c53fa565baa13df6723d2c3a5f42cda6e839e197b10b94001fd1847080d1b189c08952bab59b8b663fe26d7a6c3bafc988a5a9f4e7895355e036370e7f1b108fdf0e4e3178a3f33b7e144961af3a03d4e4c65e87786b3c62ba893e1a516994049342f00a07e04fe48e85e5abb54538efb9ff5290245c950630062b5dd2d7dcc80d0e8211eba36a0eddaa1da8ef2063c8e3f48dab1d9eb027af719e050c9424a83c14d4b62c902500ae8304d101626f6abceaffc1405742881b7b26ad446d5dff3eaefe908e31e0a7820c89961f4e3a8ad147bb0d9fa9c0e17328ad0aec52f270e2e960b0a0ab77e5a738e53295d4f690dff9e368e36e839714ad5b89bf0b0a95e403d365bc2fa090cd89b9338fc942235f0184d7b41673624bb47ae568c19ab33a28614ec5097bf2c52ca5fef800113c665c85828af2042a31e08c29d64ca5cbd78b340a0a6346dcf2854e4b6a0f315bc0a094ee9d29cc7ed4a4f4e09b707c551c75643b9cfe7cd43908319752d315e17da9f9b800521135c31ec336aa9111bca8ad8b120f8f22053512ece7ed5934f7d634b24bb3627f4293bd2671e605447190fa6b81ff92f0c966b41f597f89721f2b2ec5cf98c4694e1d451da59e7b989d1953ccbe43088edc0fd7cafff5a271c25636ab8c4b7ca5e164327e2de88ae25a2165a921c4b08751bd53e02bae77a1d5f0538fdf2a4ec515e257cee2dedf984a9cffa6a8007b185166d924315be7ad122b4ce96b5c92db75cd4b3b57346ec59396b786735d8b9413229b22417b854f3d0def3fdd6b3e7affd14ab2e453f4b6e969d05b0491441ebc0cad2d38ef4032cf0c10db7b735beaf3fecebf1ffa1b1e463996364881fe67cc190b3691dc08ce316666ce3c3faedb7ea39d89c91838f1805c1051ab876eb417ea905e67ca5187248cc97aff6d8441c04568e60eecc7b680bca49eb94484ed3deee1b1443d956d6e9264b2f226bf9cee99007ecdaf08a5934d8a11a70eecc5f62a9f1887bd35ec3e8f2d17db17c1b42177ab67b498dedd91cfdee06cd1206b60b12f8c5da13c9f90aecb54d36a573a3d46abac3d1aea05d7e924fbff8838d10940669c9f00462e3033b2687d1f2b9357e9b9a0c40eab03f26da6b09a92a41a82709e836e8fcbc27cf2108597d59a34a9359ac772c71ad909b3c64f65e5392c23e0c88152f2e88155caa4027a291653efa013e8419931f61a6379210c6d4f42a6251acb85df2a10cddfe0ae27dfb5eff0623cc38eb6d3aae3cce9aea04f087794f8d2bfe0cd43f589249870718856cb374a2bb9f6c57666256b3847db5cba09267a53ed5ba6568795d2195f7f4dd271075a91f8fabf800f968bcd2d8b832cb407783acd60fede6eb77689e375d597b228f8d04ce81ec81e08c0f67595f143bd415466507b5766bcde93f06145c373f941618a097aaeed2ec5b156c15419b27037cb94982e9690c9e68362e184ce47ff67a0183de2dbebb3926eee929044be4b70303221b4a1d0d754631ceb930a02455a6fb12862082e0032f4f7880502f282ce0394217e4829e59b6cbb77273e23dce0a66f8daa5188f648b6abb47ef65057afeb77b912b8f497dbc0d89ab5c726894bd808f48e959e783db567031fbc94754f31856dc7d96b48addb090a2ba9ca7241e416ac7789950f47d65e0553983c9b880da56ed9aa78cdae2b6e4da96f7674b667e48a4947bb2ae52d69de89b49fbd05b6c7eebf352a0c00656e3a6a3042a55265699b4a3eaa1f55e6b76535f6436955ba81a89d8c4841d233c315a185090aad707731c3d7054d2d63d3d40243239930b5d6653bd7545c0681b26ce2fc98b07933fa11749fb392c98717552bc5517abb0d6b7b0f23a213880b842f24257760bc1b020f8b40143d8ac9f31ab0ad70892b035d7f14c4152d26dc53b7aa28398a6a7f2a731d84a0bf1bb2cfb4b08b398b28b510aa09fed9818121739092f3bf9159f2fd7e1f4a9bdc17ad2721e05b4a4c6734a216606b78deb88437bcb4c7005d788b52fb06e6f56979d49969565153f3b52024ce" + ), + ), + # Use SLIP-0077 internal derivation. + ) + unblinded = liquid.unblind_output(self.client, unblind_output) + assert unblinded == LiquidAmount( + value=12345000, + value_blind=bytes.fromhex( + "b55bbbefeccce37df39f592046e74d4585dd6c5bb2849f7ab9e9a156fc906311" + ), + asset=bytes.fromhex( + "230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2" + ), + asset_blind=bytes.fromhex( + "b5f1b784b675c2ec943a167c8b57ad16543f44bfe8d059a8d8b7e8a4c0bbe6e1" + ), + ) + + +# TODO: check output surjection using libsecp256k1 + +# Build by: +# $ cd vendor/secp256k1-zkp/ +# $ ./autogen.sh +# $ ./configure --enable-experimental --enable-module-generator --enable-module-rangeproof --enable-module-surjectionproof --enable-module-ecdh --enable-module-recovery +# $ make + + +ctx = lib.secp256k1_blind_context + + +def _verify_range_proof(blinded_output): + conf_value = ctypes.create_string_buffer(lib.SECP256K1_PEDERSEN_COMMITMENT_SIZE) + assert ( + lib.secp256k1.secp256k1_pedersen_commitment_parse( + ctx, conf_value, blinded_output.conf_value + ) + == 1 + ) + conf_asset = ctypes.create_string_buffer(lib.SECP256K1_GENERATOR_SIZE) + assert ( + lib.secp256k1.secp256k1_generator_parse( + ctx, conf_asset, blinded_output.conf_asset + ) + == 1 + ) + + min_value = ctypes.c_uint64(0) + max_value = ctypes.c_uint64(0) + extra_commit = blinded_output.script_pubkey + res = lib.secp256k1.secp256k1_rangeproof_verify( + ctx, + ctypes.byref(min_value), + ctypes.byref(max_value), + conf_value, + blinded_output.range_proof, + len(blinded_output.range_proof), + extra_commit, + len(extra_commit), + conf_asset, + ) + assert res == 1 + + # TODO: free allocated memory + + min_value = min_value.value + max_value = max_value.value + assert min_value < max_value + assert min_value >= 1 + assert max_value <= 2 ** 51 + + +def _blind_amount(a: LiquidAmount): + generator = ctypes.create_string_buffer(lib.SECP256K1_GENERATOR_SIZE) + assert ( + lib.secp256k1.secp256k1_generator_generate_blinded( + ctx, generator, a.asset, a.asset_blind + ) + == 1 + ) + + commit = ctypes.create_string_buffer(lib.SECP256K1_PEDERSEN_COMMITMENT_SIZE) + assert ( + lib.secp256k1.secp256k1_pedersen_commit( + ctx, commit, a.value_blind, a.value, generator + ) + == 1 + ) + + serialized = ctypes.create_string_buffer(33) + assert ( + lib.secp256k1.secp256k1_pedersen_commitment_serialize(ctx, serialized, commit) + == 1 + ) + + return LiquidBlindedOutput(conf_value=bytes(serialized)) + + +def _get_commitment(v): + """v may be an explicit LiquidAmount or BlindedOutput""" + if isinstance(v, LiquidAmount): + zero_blinder = b"\x00" * 32 + generator = ctypes.create_string_buffer(lib.SECP256K1_GENERATOR_SIZE) + assert lib.secp256k1.secp256k1_generator_generate(ctx, generator, v.asset) == 1 + + commit = ctypes.create_string_buffer(lib.SECP256K1_PEDERSEN_COMMITMENT_SIZE) + assert ( + lib.secp256k1.secp256k1_pedersen_commit( + ctx, commit, zero_blinder, v.value, generator + ) + == 1 + ) + return commit + + if isinstance(v, LiquidBlindedOutput): + conf_value = ctypes.create_string_buffer(lib.SECP256K1_PEDERSEN_COMMITMENT_SIZE) + assert ( + lib.secp256k1.secp256k1_pedersen_commitment_parse( + ctx, conf_value, v.conf_value + ) + == 1 + ) + return conf_value + + raise ValueError(v) + + +def _get_blinded_generator(v): + if isinstance(v, LiquidAmount): + generator = ctypes.create_string_buffer(lib.SECP256K1_GENERATOR_SIZE) + assert ( + lib.secp256k1.secp256k1_generator_generate_blinded( + ctx, generator, v.asset, v.asset_blind + ) + == 1 + ) + return generator + + if isinstance(v, LiquidBlindedOutput): + conf_asset = ctypes.create_string_buffer(lib.SECP256K1_GENERATOR_SIZE) + assert ( + lib.secp256k1.secp256k1_generator_parse(ctx, conf_asset, v.conf_asset) == 1 + ) + return conf_asset + + raise ValueError(v) + + +def _collect_commits(values): + commits = list(map(_get_commitment, values)) + # Return array of pointers to commitments + result = (ctypes.c_char_p * len(commits))() + for i, commit in enumerate(commits): + result[i] = ctypes.cast(commit, ctypes.c_char_p) + return result + + +def _verify_balance(inputs, outputs): + input_commits = _collect_commits(inputs) + output_commits = _collect_commits(outputs) + res = lib.secp256k1.secp256k1_pedersen_verify_tally( + ctx, input_commits, len(input_commits), output_commits, len(output_commits) + ) + assert res == 1 + + +def _verify_surjection_proof(blinded_output, inputs): + output_generator = _get_blinded_generator(blinded_output) + input_generators = list(map(_get_blinded_generator, inputs)) + assert len(output_generator) == lib.SECP256K1_GENERATOR_SIZE + for g in input_generators: + assert len(g) == lib.SECP256K1_GENERATOR_SIZE + + n_input_generators = len(input_generators) + input_generators = b"".join(input_generators) + assert len(input_generators) == n_input_generators * lib.SECP256K1_GENERATOR_SIZE + + proof = ctypes.create_string_buffer( + 10_000 + ) # TODO: use sizeof(secp256k1_surjectionproof) + assert ( + lib.secp256k1.secp256k1_surjectionproof_parse( + ctx, + proof, + blinded_output.surjection_proof, + len(blinded_output.surjection_proof), + ) + == 1 + ) + + assert ( + lib.secp256k1.secp256k1_surjectionproof_verify( + ctx, proof, input_generators, n_input_generators, output_generator + ) + == 1 + ) diff --git a/tools/build_protobuf b/tools/build_protobuf index 731feb5dfc6..6cc5e26f235 100755 --- a/tools/build_protobuf +++ b/tools/build_protobuf @@ -13,6 +13,7 @@ CORE_PROTOBUF_SOURCES="\ $PROTOB/messages-debug.proto \ $PROTOB/messages-eos.proto \ $PROTOB/messages-ethereum.proto \ + $PROTOB/messages-liquid.proto \ $PROTOB/messages-lisk.proto \ $PROTOB/messages-management.proto \ $PROTOB/messages-monero.proto \