Skip to content

Commit

Permalink
elements: add transaction blinding/unblinding API
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
romanz committed Aug 13, 2019
1 parent fe913da commit 549419f
Show file tree
Hide file tree
Showing 29 changed files with 1,782 additions and 0 deletions.
61 changes: 61 additions & 0 deletions common/protob/messages-liquid.proto
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 7 additions & 0 deletions common/protob/messages.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
8 changes: 8 additions & 0 deletions core/src/apps/liquid/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
123 changes: 123 additions & 0 deletions core/src/apps/liquid/blind.py
Original file line number Diff line number Diff line change
@@ -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]),
)
39 changes: 39 additions & 0 deletions core/src/apps/liquid/blind_tx.py
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions core/src/apps/liquid/unblind_output.py
Original file line number Diff line number Diff line change
@@ -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,
)
2 changes: 2 additions & 0 deletions core/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
35 changes: 35 additions & 0 deletions core/src/trezor/messages/LiquidAmount.py
Original file line number Diff line number Diff line change
@@ -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),
}
30 changes: 30 additions & 0 deletions core/src/trezor/messages/LiquidBalanceBlinds.py
Original file line number Diff line number Diff line change
@@ -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),
}
39 changes: 39 additions & 0 deletions core/src/trezor/messages/LiquidBlindOutput.py
Original file line number Diff line number Diff line change
@@ -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),
}
Loading

0 comments on commit 549419f

Please sign in to comment.