Skip to content

Commit

Permalink
elements: derive confidential addresses using SLIP-0077
Browse files Browse the repository at this point in the history
Following #66 and #398, it would allow receiving confidential
transactions to addresses generated on the devices.

Blinding, unblinding and signing will be added in separate PRs.
  • Loading branch information
romanz committed Jan 18, 2020
1 parent 50773c2 commit 682bdad
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 25 deletions.
1 change: 1 addition & 0 deletions common/protob/messages-bitcoin.proto
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ message GetAddress {
optional bool show_display = 3; // optionally show on display before sending the result
optional MultisigRedeemScriptType multisig = 4; // filled if we are showing a multisig address
optional InputScriptType script_type = 5 [default=SPENDADDRESS]; // used to distinguish between various address formats (non-segwit, segwit, etc.)
optional bool confidential = 6; // generate confidetial address (for Confidential Transactions)
}

/**
Expand Down
5 changes: 4 additions & 1 deletion core/src/apps/wallet/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from trezor import wire
from trezor import utils, wire
from trezor.messages import MessageType


Expand All @@ -13,6 +13,9 @@ def boot() -> None:
["secp256k1-groestl"],
["secp256k1-smart"],
]
if not utils.BITCOIN_ONLY:
ns.append(["slip21"])

wire.add(MessageType.GetPublicKey, __name__, "get_public_key", ns)
wire.add(MessageType.GetAddress, __name__, "get_address", ns)
wire.add(MessageType.GetEntropy, __name__, "get_entropy")
Expand Down
13 changes: 12 additions & 1 deletion core/src/apps/wallet/get_address.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from trezor import utils
from trezor.crypto import bip32
from trezor.messages import InputScriptType
from trezor.messages.Address import Address
Expand Down Expand Up @@ -48,7 +49,17 @@ async def get_address(ctx, msg, keychain):
)

node = keychain.derive(msg.address_n, coin.curve_name)
address = addresses.get_address(msg.script_type, coin, node, msg.multisig)
derive_blinding_pubkey = None
if not utils.BITCOIN_ONLY and msg.confidential:
derive_blinding_pubkey = keychain.derive_slip77_blinding_public_key

address = addresses.get_address(
msg.script_type,
coin,
node,
msg.multisig,
derive_blinding_pubkey=derive_blinding_pubkey,
)
address_short = addresses.address_short(coin, address)
if msg.script_type == InputScriptType.SPENDWITNESS:
address_qr = address.upper() # bech32 address
Expand Down
73 changes: 56 additions & 17 deletions core/src/apps/wallet/sign_tx/addresses.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
from trezor.crypto import base58, bech32, cashaddr
from trezor.crypto.hashlib import sha256
from trezor.messages import FailureType, InputScriptType
from trezor.utils import ensure
from trezor.utils import BITCOIN_ONLY, ensure

from apps.common import HARDENED, address_type, paths
from apps.common.coininfo import CoinInfo
from apps.wallet.sign_tx.multisig import multisig_get_pubkeys, multisig_pubkey_index
from apps.wallet.sign_tx.scripts import (
output_script_multisig,
output_script_native_p2wpkh_or_p2wsh,
output_script_p2pkh,
output_script_p2sh,
)

if False:
Expand All @@ -25,15 +27,19 @@ class AddressError(Exception):


def get_address(
script_type: InputScriptType, coin: CoinInfo, node, multisig=None
script_type: InputScriptType,
coin: CoinInfo,
node,
multisig=None,
derive_blinding_pubkey=None,
) -> str:

pubkey = node.public_key()
if (
script_type == InputScriptType.SPENDADDRESS
or script_type == InputScriptType.SPENDMULTISIG
):
if multisig: # p2sh multisig
pubkey = node.public_key()
index = multisig_pubkey_index(multisig, pubkey)
if index is None:
raise AddressError(FailureType.ProcessError, "Public key not found")
Expand All @@ -51,7 +57,7 @@ def get_address(
raise AddressError(FailureType.ProcessError, "Multisig details required")

# p2pkh
address = node.address(coin.address_type)
address = address_pkh(pubkey, coin, derive_blinding_pubkey)
if coin.cashaddr_prefix is not None:
address = address_to_cashaddr(address, coin)
return address
Expand All @@ -67,7 +73,7 @@ def get_address(
return address_multisig_p2wsh(pubkeys, multisig.m, coin.bech32_prefix)

# native p2wpkh
return address_p2wpkh(node.public_key(), coin)
return address_p2wpkh(pubkey, coin)

elif (
script_type == InputScriptType.SPENDP2SHWITNESS
Expand All @@ -79,10 +85,12 @@ def get_address(
# p2wsh multisig nested in p2sh
if multisig is not None:
pubkeys = multisig_get_pubkeys(multisig)
return address_multisig_p2wsh_in_p2sh(pubkeys, multisig.m, coin)
return address_multisig_p2wsh_in_p2sh(
pubkeys, multisig.m, coin, derive_blinding_pubkey
)

# p2wpkh nested in p2sh
return address_p2wpkh_in_p2sh(node.public_key(), coin)
return address_p2wpkh_in_p2sh(pubkey, coin, derive_blinding_pubkey)

else:
raise AddressError(FailureType.ProcessError, "Invalid script type")
Expand All @@ -98,14 +106,16 @@ def address_multisig_p2sh(pubkeys: List[bytes], m: int, coin: CoinInfo):
return address_p2sh(redeem_script_hash, coin)


def address_multisig_p2wsh_in_p2sh(pubkeys: List[bytes], m: int, coin: CoinInfo):
def address_multisig_p2wsh_in_p2sh(
pubkeys: List[bytes], m: int, coin: CoinInfo, derive_blinding_pubkey=None
):
if coin.address_type_p2sh is None:
raise AddressError(
FailureType.ProcessError, "Multisig not enabled on this coin"
)
witness_script = output_script_multisig(pubkeys, m)
witness_script_hash = sha256(witness_script).digest()
return address_p2wsh_in_p2sh(witness_script_hash, coin)
return address_p2wsh_in_p2sh(witness_script_hash, coin, derive_blinding_pubkey)


def address_multisig_p2wsh(pubkeys: List[bytes], m: int, hrp: str):
Expand All @@ -118,27 +128,56 @@ def address_multisig_p2wsh(pubkeys: List[bytes], m: int, hrp: str):
return address_p2wsh(witness_script_hash, hrp)


def address_pkh(pubkey: bytes, coin: CoinInfo) -> str:
s = address_type.tobytes(coin.address_type) + coin.script_hash(pubkey)
def address_pkh(pubkey: bytes, coin: CoinInfo, derive_blinding_pubkey=None) -> str:
script_hash = coin.script_hash(pubkey)
address_type_prefix = address_type.tobytes(coin.address_type)
if not BITCOIN_ONLY and derive_blinding_pubkey:
script = output_script_p2pkh(script_hash)
blinding_pubkey = derive_blinding_pubkey(script=script)
s = (
address_type.tobytes(coin.confidential_assets["address_prefix"])
+ address_type_prefix
+ blinding_pubkey
+ script_hash
)
else:
s = address_type_prefix + script_hash
return base58.encode_check(bytes(s), coin.b58_hash)


def address_p2sh(redeem_script_hash: bytes, coin: CoinInfo) -> str:
s = address_type.tobytes(coin.address_type_p2sh) + redeem_script_hash
def address_p2sh(
redeem_script_hash: bytes, coin: CoinInfo, derive_blinding_pubkey=None
) -> str:
address_type_prefix = address_type.tobytes(coin.address_type_p2sh)
if not BITCOIN_ONLY and derive_blinding_pubkey:
script = output_script_p2sh(redeem_script_hash)
blinding_pubkey = derive_blinding_pubkey(script=script)
s = (
address_type.tobytes(coin.confidential_assets["address_prefix"])
+ address_type_prefix
+ blinding_pubkey
+ redeem_script_hash
)
else:
s = address_type_prefix + redeem_script_hash
return base58.encode_check(bytes(s), coin.b58_hash)


def address_p2wpkh_in_p2sh(pubkey: bytes, coin: CoinInfo) -> str:
def address_p2wpkh_in_p2sh(
pubkey: bytes, coin: CoinInfo, derive_blinding_pubkey=None
) -> str:
pubkey_hash = ecdsa_hash_pubkey(pubkey, coin)
redeem_script = output_script_native_p2wpkh_or_p2wsh(pubkey_hash)
redeem_script_hash = coin.script_hash(redeem_script)
return address_p2sh(redeem_script_hash, coin)
return address_p2sh(redeem_script_hash, coin, derive_blinding_pubkey)


def address_p2wsh_in_p2sh(witness_script_hash: bytes, coin: CoinInfo) -> str:
def address_p2wsh_in_p2sh(
witness_script_hash: bytes, coin: CoinInfo, derive_blinding_pubkey=None
) -> str:
redeem_script = output_script_native_p2wpkh_or_p2wsh(witness_script_hash)
redeem_script_hash = coin.script_hash(redeem_script)
return address_p2sh(redeem_script_hash, coin)
return address_p2sh(redeem_script_hash, coin, derive_blinding_pubkey)


def address_p2wpkh(pubkey: bytes, coin: CoinInfo) -> str:
Expand Down
3 changes: 3 additions & 0 deletions core/src/trezor/messages/GetAddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ def __init__(
show_display: bool = None,
multisig: MultisigRedeemScriptType = None,
script_type: EnumTypeInputScriptType = None,
confidential: bool = None,
) -> None:
self.address_n = address_n if address_n is not None else []
self.coin_name = coin_name
self.show_display = show_display
self.multisig = multisig
self.script_type = script_type
self.confidential = confidential

@classmethod
def get_fields(cls) -> Dict:
Expand All @@ -38,4 +40,5 @@ def get_fields(cls) -> Dict:
3: ('show_display', p.BoolType, 0),
4: ('multisig', MultisigRedeemScriptType, 0),
5: ('script_type', p.EnumType("InputScriptType", (0, 1, 2, 3, 4)), 0), # default=SPENDADDRESS
6: ('confidential', p.BoolType, 0),
}
2 changes: 2 additions & 0 deletions python/src/trezorlib/btc.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def get_address(
show_display=False,
multisig=None,
script_type=messages.InputScriptType.SPENDADDRESS,
confidential=False,
):
return client.call(
messages.GetAddress(
Expand All @@ -54,6 +55,7 @@ def get_address(
show_display=show_display,
multisig=multisig,
script_type=script_type,
confidential=confidential,
)
)

Expand Down
10 changes: 8 additions & 2 deletions python/src/trezorlib/cli/btc.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,19 @@ def cli():
@click.option("-n", "--address", required=True, help="BIP-32 path")
@click.option("-t", "--script-type", type=ChoiceType(INPUT_SCRIPTS), default="address")
@click.option("-d", "--show-display", is_flag=True)
@click.option("--confidential", type=bool)
@click.pass_obj
def get_address(connect, coin, address, script_type, show_display):
def get_address(connect, coin, address, script_type, show_display, confidential):
"""Get address for specified path."""
coin = coin or DEFAULT_COIN
address_n = tools.parse_path(address)
return btc.get_address(
connect(), coin, address_n, show_display, script_type=script_type
connect(),
coin,
address_n,
show_display,
script_type=script_type,
confidential=confidential,
)


Expand Down
3 changes: 3 additions & 0 deletions python/src/trezorlib/messages/GetAddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ def __init__(
show_display: bool = None,
multisig: MultisigRedeemScriptType = None,
script_type: EnumTypeInputScriptType = None,
confidential: bool = None,
) -> None:
self.address_n = address_n if address_n is not None else []
self.coin_name = coin_name
self.show_display = show_display
self.multisig = multisig
self.script_type = script_type
self.confidential = confidential

@classmethod
def get_fields(cls) -> Dict:
Expand All @@ -38,4 +40,5 @@ def get_fields(cls) -> Dict:
3: ('show_display', p.BoolType, 0),
4: ('multisig', MultisigRedeemScriptType, 0),
5: ('script_type', p.EnumType("InputScriptType", (0, 1, 2, 3, 4)), 0), # default=SPENDADDRESS
6: ('confidential', p.BoolType, 0),
}
21 changes: 19 additions & 2 deletions tests/device_tests/test_msg_getaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,27 @@ def test_grs(self, client):
)

@pytest.mark.altcoin
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
def test_elements(self, client):
assert (
btc.get_address(client, "Elements", parse_path("m/44'/1'/0'/0/0"))
== "2dpWh6jbhAowNsQ5agtFzi7j6nKscj6UnEr"
btc.get_address(
client,
"Elements",
parse_path("m/44'/1'/0'/0/0"),
script_type=proto.InputScriptType.SPENDADDRESS,
confidential=True,
)
== "CTExueKM8363z7gaDiD22BKuYCcj8peAtgDTndcdUFn4s47jgBHAsTk1WrpL3GJkVJzuRdnvU56ma3MQ"
)
assert (
btc.get_address(
client,
"Elements",
parse_path("m/44'/1'/0'/0/0"),
script_type=proto.InputScriptType.SPENDADDRESS,
confidential=False,
)
== "2dekDMTvVnZjAZVFRrs8mRAhyXKHGptx5pd"
)

@pytest.mark.multisig
Expand Down
17 changes: 15 additions & 2 deletions tests/device_tests/test_msg_getaddress_segwit.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,27 @@ def test_show_segwit_altcoin(self, client):
)
== "2N4Q5FhU2497BryFfUgbqkAJE87aKDv3V3e"
)

@pytest.mark.altcoin
@pytest.mark.setup_client(mnemonic=" ".join(["all"] * 12))
def test_elements(self, client):
assert (
btc.get_address(
client,
"Elements",
parse_path("m/49'/1'/0'/0/0"),
script_type=proto.InputScriptType.SPENDP2SHWITNESS,
confidential=True,
)
== "AzpuzTFTuET7YqJ9U8fBuRTf5xvsLmudLg3uvycfP1aTFpKXUYuUs3kz98boyzDSe5n1MevURZdB4pR5"
)
assert (
btc.get_address(
client,
"Elements",
parse_path("m/49'/1'/0'/0/0"),
False,
None,
script_type=proto.InputScriptType.SPENDP2SHWITNESS,
confidential=False,
)
== "XNW67ZQA9K3AuXPBWvJH4zN2y5QBDTwy2Z"
)
Expand Down
4 changes: 4 additions & 0 deletions tests/device_tests/test_msg_getaddress_segwit_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ def test_show_segwit_altcoin(self, client):
)
== "grs1qw4teyraux2s77nhjdwh9ar8rl9dt7zww8r6lne"
)

@pytest.mark.altcoin
@pytest.mark.setup_client(mnemonic=" ".join(["all"] * 12))
def test_elements(self, client):
assert (
btc.get_address(
client,
Expand Down

0 comments on commit 682bdad

Please sign in to comment.