Skip to content

Commit

Permalink
Add tests for ng configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
fbeutin-ledger committed Oct 17, 2023
1 parent 37f39bf commit 693ef99
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 58 deletions.
142 changes: 85 additions & 57 deletions test/python/apps/exchange_transaction_builder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from base64 import urlsafe_b64encode
from typing import Optional, Dict, Callable, Iterable
from typing import Optional, Dict, Callable, Iterable, Union
from enum import Enum, auto, IntEnum
from dataclasses import dataclass
from cryptography.hazmat.primitives.asymmetric import ec
Expand Down Expand Up @@ -44,52 +44,90 @@ class SubCommand(IntEnum):
NEW_SUBCOMMANDS = [SubCommand.SWAP_NG, SubCommand.SELL_NG, SubCommand.FUND_NG]
ALL_SUBCOMMANDS = [SubCommand.SWAP, SubCommand.SELL, SubCommand.FUND, SubCommand.SWAP_NG, SubCommand.SELL_NG, SubCommand.FUND_NG]

@dataclass(frozen=True)
@dataclass()
class SubCommandSpecs:
subcommand_id: SubCommand
partner_curve: ec.EllipticCurve
signature_computation: SignatureComputation
signature_encoding: SignatureEncoding
default_payload_encoding: PayloadEncoding
payload_encoding: PayloadEncoding
transaction_type: Callable
required_fields: Iterable[str]
transaction_id_field: str
payout_field: str
refund_field: Optional[str]

@property
def dot_prefix(self):
return int.to_bytes(1 if (self.signature_computation == SignatureComputation.DOT_PREFIXED_BASE_64_URL) else 0, 1, byteorder='big')
@property
def signature_encoding_prefix(self):
return int.to_bytes(1 if (self.signature_encoding == SignatureEncoding.PLAIN_R_S) else 0, 1, byteorder='big')
@property
def payload_encoding_prefix(self):
return int.to_bytes(1 if (self.payload_encoding == PayloadEncoding.BASE_64_URL) else 0, 1, byteorder='big')
@property
def is_ng(self):
return (self.subcommand_id == SubCommand.SWAP_NG or self.subcommand_id == SubCommand.SELL_NG or self.subcommand_id == SubCommand.FUND_NG)
@property
def size_of_transaction_length(self):
return (2 if self.is_ng else 1)

def check_conf(self, conf: Dict) -> bool:
return (all(i in conf for i in self.required_fields) and (len(conf) == len(self.required_fields)))

def format_transaction(self, transaction: bytes, prefix_transaction: bool) -> bytes:
if prefix_transaction == True:
def format_transaction(self, transaction: bytes) -> bytes:
if self.signature_computation == SignatureComputation.DOT_PREFIXED_BASE_64_URL:
return b"." + transaction
else:
return transaction

def encode_payload(self, raw_transaction: bytes, url_encode: bool) -> bytes:
if url_encode == True:
def encode_payload(self, raw_transaction: bytes) -> bytes:
if self.payload_encoding == PayloadEncoding.BASE_64_URL:
return urlsafe_b64encode(raw_transaction)
else:
return raw_transaction

def encode_signature(self, signature_to_encode: bytes, r_s_encode: bool) -> bytes:
if r_s_encode == True:
def encode_signature(self, signature_to_encode: bytes) -> bytes:
if self.signature_encoding == SignatureEncoding.PLAIN_R_S:
r, s = decode_dss_signature(signature_to_encode)
signature_to_encode = r.to_bytes(32, "big") + s.to_bytes(32, "big")

if self.is_ng:
signature_to_encode = self.dot_prefix + self.signature_encoding_prefix + signature_to_encode

return signature_to_encode

def _create_transaction(self, conf: Dict, transaction_id: bytes) -> bytes:
def create_transaction(self, conf: Dict, transaction_id: bytes) -> bytes:
# Alter a copy of conf to not modify the actual conf
c = conf.copy()
c[self.transaction_id_field] = transaction_id
raw_transaction = self.transaction_type(**c).SerializeToString()
return self.encode_payload(raw_transaction, (self.default_payload_encoding == PayloadEncoding.BASE_64_URL))


SWAP_NG_SPECS: SubCommandSpecs = SubCommandSpecs(
return self.encode_payload(raw_transaction)

def craft_pb(self, tx_infos: Dict, transaction_id: bytes) -> bytes:
assert self.check_conf(tx_infos)
return self.create_transaction(tx_infos, transaction_id)

def craft_transaction(self, transaction: bytes, fees: int) -> bytes:
fees_bytes = int_to_minimally_sized_bytes(fees)
payload = prefix_with_len_custom(transaction, self.size_of_transaction_length) + prefix_with_len(fees_bytes)
if self.is_ng:
payload = self.payload_encoding_prefix + payload
return payload

def encode_transaction_signature(self, signer: SigningAuthority, tx: bytes) -> bytes:
formated_transaction = self.format_transaction(tx)
signed_transaction = signer.sign(formated_transaction)
encoded_signature = self.encode_signature(signed_transaction)
return encoded_signature

SWAP_NG_SPECS = SubCommandSpecs(
subcommand_id = SubCommand.SWAP_NG,
partner_curve = ec.SECP256R1(),
signature_computation = SignatureComputation.DOT_PREFIXED_BASE_64_URL,
signature_encoding = SignatureEncoding.PLAIN_R_S,
default_payload_encoding = PayloadEncoding.BASE_64_URL,
payload_encoding = PayloadEncoding.BASE_64_URL,
transaction_type = NewTransactionResponse,
required_fields = ["payin_address", "payin_extra_id", "refund_address", "refund_extra_id",
"payout_address", "payout_extra_id", "currency_from", "currency_to",
Expand All @@ -99,11 +137,12 @@ def _create_transaction(self, conf: Dict, transaction_id: bytes) -> bytes:
refund_field = "currency_from",
)

SWAP_SPECS: SubCommandSpecs = SubCommandSpecs(
SWAP_SPECS = SubCommandSpecs(
subcommand_id = SubCommand.SWAP,
partner_curve = ec.SECP256K1(),
signature_computation = SignatureComputation.BINARY_ENCODED_PAYLOAD,
signature_encoding = SignatureEncoding.DER,
default_payload_encoding = PayloadEncoding.BYTES_ARRAY,
payload_encoding = PayloadEncoding.BYTES_ARRAY,
transaction_type = NewTransactionResponse,
required_fields = ["payin_address", "payin_extra_id", "refund_address", "refund_extra_id",
"payout_address", "payout_extra_id", "currency_from", "currency_to",
Expand All @@ -113,38 +152,51 @@ def _create_transaction(self, conf: Dict, transaction_id: bytes) -> bytes:
refund_field = "currency_from",
)

SELL_NG_SPECS: SubCommandSpecs = SubCommandSpecs(
SELL_NG_SPECS = SubCommandSpecs(
subcommand_id = SubCommand.SELL_NG,
partner_curve = ec.SECP256R1(),
signature_computation = SignatureComputation.DOT_PREFIXED_BASE_64_URL,
signature_encoding = SignatureEncoding.PLAIN_R_S,
default_payload_encoding = PayloadEncoding.BASE_64_URL,
payload_encoding = PayloadEncoding.BASE_64_URL,
transaction_type = NewSellResponse,
transaction_id_field = "device_transaction_id",
required_fields = ["trader_email", "in_currency", "in_amount", "in_address", "out_currency", "out_amount"],
payout_field = "in_currency",
refund_field = None,
)

# Legacy SELL specs happen to be the same as the unified specs
SELL_SPECS: SubCommandSpecs = SELL_NG_SPECS
SELL_SPECS = SubCommandSpecs(
subcommand_id = SubCommand.SELL,
partner_curve = ec.SECP256R1(),
signature_computation = SignatureComputation.DOT_PREFIXED_BASE_64_URL,
signature_encoding = SignatureEncoding.PLAIN_R_S,
payload_encoding = PayloadEncoding.BASE_64_URL,
transaction_type = NewSellResponse,
transaction_id_field = "device_transaction_id",
required_fields = ["trader_email", "in_currency", "in_amount", "in_address", "out_currency", "out_amount"],
payout_field = "in_currency",
refund_field = None,
)

FUND_NG_SPECS: SubCommandSpecs = SubCommandSpecs(
FUND_NG_SPECS = SubCommandSpecs(
subcommand_id = SubCommand.FUND_NG,
partner_curve = ec.SECP256R1(),
signature_computation = SignatureComputation.DOT_PREFIXED_BASE_64_URL,
signature_encoding = SignatureEncoding.PLAIN_R_S,
default_payload_encoding = PayloadEncoding.BASE_64_URL,
payload_encoding = PayloadEncoding.BASE_64_URL,
transaction_type = NewFundResponse,
required_fields = ["user_id", "account_name", "in_currency", "in_amount", "in_address"],
transaction_id_field = "device_transaction_id",
payout_field = "in_currency",
refund_field = None,
)

FUND_SPECS: SubCommandSpecs = SubCommandSpecs(
FUND_SPECS = SubCommandSpecs(
subcommand_id = SubCommand.FUND,
partner_curve = ec.SECP256R1(),
signature_computation = SignatureComputation.DOT_PREFIXED_BASE_64_URL,
signature_encoding = SignatureEncoding.DER,
default_payload_encoding = PayloadEncoding.BASE_64_URL,
payload_encoding = PayloadEncoding.BASE_64_URL,
transaction_type = NewFundResponse,
required_fields = ["user_id", "account_name", "in_currency", "in_amount", "in_address"],
transaction_id_field = "device_transaction_id",
Expand All @@ -161,38 +213,14 @@ def _create_transaction(self, conf: Dict, transaction_id: bytes) -> bytes:
SubCommand.FUND_NG: FUND_NG_SPECS,
}

def craft_pb(subcommand: SubCommand, tx_infos: Dict, transaction_id: bytes) -> bytes:
subcommand_specs = SUBCOMMAND_TO_SPECS[subcommand]
assert subcommand_specs.check_conf(tx_infos)
return subcommand_specs._create_transaction(tx_infos, transaction_id)

def encode_transaction_signature(subcommand: SubCommand, signer: SigningAuthority, tx: bytes) -> bytes:
subcommand_specs = SUBCOMMAND_TO_SPECS[subcommand]
prefix_transaction = True if (subcommand_specs.signature_computation == SignatureComputation.DOT_PREFIXED_BASE_64_URL) else False
formated_transaction = subcommand_specs.format_transaction(tx, prefix_transaction)
signed_transaction = signer.sign(formated_transaction)
r_s_encode = True if (subcommand_specs.signature_encoding == SignatureEncoding.PLAIN_R_S) else False
encoded_signature = subcommand_specs.encode_signature(signed_transaction, r_s_encode)

if subcommand == SubCommand.SWAP_NG or subcommand == SubCommand.SELL_NG or subcommand == SubCommand.FUND_NG:
dot_prefix = int.to_bytes(1 if prefix_transaction == True else False, 1, byteorder='big')
rs_encode = int.to_bytes(1 if r_s_encode == True else False, 1, byteorder='big')
encoded_signature = dot_prefix + rs_encode + encoded_signature
return encoded_signature

def craft_transaction(subcommand: SubCommand, transaction: bytes, fees: int) -> bytes:
subcommand_specs = SUBCOMMAND_TO_SPECS[subcommand]
fees_bytes = int_to_minimally_sized_bytes(fees)
prefix_length = 2 if (subcommand == SubCommand.SWAP_NG or subcommand == SubCommand.FUND_NG or subcommand == SubCommand.SELL_NG) else 1
payload = prefix_with_len_custom(transaction, prefix_length) + prefix_with_len(fees_bytes)
if subcommand == SubCommand.SWAP_NG or subcommand == SubCommand.FUND_NG or subcommand == SubCommand.SELL_NG:
payload = int.to_bytes(1, 1, byteorder='big') + payload
return payload

def craft_and_sign_tx(subcommand: SubCommand, tx_infos: Dict, transaction_id: bytes, fees: int, signer: SigningAuthority):
pb = craft_pb(subcommand, tx_infos, transaction_id)
tx = craft_transaction(subcommand, pb, fees)
signed_tx = encode_transaction_signature(subcommand, signer, pb)
def craft_and_sign_tx(subcommand: Union[SubCommand, SubCommandSpecs], tx_infos: Dict, transaction_id: bytes, fees: int, signer: SigningAuthority):
if isinstance(subcommand, SubCommand):
subcommand_specs = SUBCOMMAND_TO_SPECS[subcommand]
else:
subcommand_specs = subcommand
pb = subcommand_specs.craft_pb(tx_infos, transaction_id)
tx = subcommand_specs.craft_transaction(pb, fees)
signed_tx = subcommand_specs.encode_transaction_signature(signer, pb)
return tx, signed_tx

def extract_payout_ticker(subcommand: SubCommand, tx_infos: Dict) -> str:
Expand Down
92 changes: 92 additions & 0 deletions test/python/test_ng_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import pytest
import copy

from cryptography.hazmat.primitives.asymmetric import ec

from .apps.exchange import ExchangeClient, Rate, SubCommand
from .apps.exchange_transaction_builder import SWAP_NG_SPECS, SELL_NG_SPECS, FUND_NG_SPECS, get_partner_curve, get_credentials, craft_and_sign_tx, SignatureComputation, SignatureEncoding, PayloadEncoding
from .apps.signing_authority import SigningAuthority, LEDGER_SIGNER

# Some valid infos for TX. Content is irrelevant for the test

SWAP_TX_INFOS = {
"payin_address": b"0xd692Cb1346262F584D17B4B470954501f6715a82",
"payin_extra_id": b"",
"refund_address": b"0xDad77910DbDFdE764fC21FCD4E74D71bBACA6D8D",
"refund_extra_id": b"",
"payout_address": b"bc1qer57ma0fzhqys2cmydhuj9cprf9eg0nw922a8j",
"payout_extra_id": b"",
"currency_from": "ETH",
"currency_to": "BTC",
"amount_to_provider": bytes.fromhex("013fc3a717fb5000"),
"amount_to_wallet": b"\x0b\xeb\xc2\x00",
}
FUND_TX_INFOS = {
"user_id": "John Wick",
"account_name": "Remember Daisy",
"in_currency": "ETH",
"in_amount": b"\032\200\250]$T\000",
"in_address": "0x252fb4acbe0de4f0bd2409a5ed59a71e4ef1d2bc"
}
SELL_TX_INFOS = {
"trader_email": "[email protected]",
"out_currency": "USD",
"out_amount": {"coefficient": b"\x01", "exponent": 3},
"in_currency": "ETH",
"in_amount": b"\032\200\250]$T\000",
"in_address": "0x252fb4acbe0de4f0bd2409a5ed59a71e4ef1d2bc"
}
TX_INFOS = {
SubCommand.SWAP_NG: SWAP_TX_INFOS,
SubCommand.FUND_NG: FUND_TX_INFOS,
SubCommand.SELL_NG: SELL_TX_INFOS,
}
TEST_NAME_SUFFIX = {
SubCommand.SWAP_NG: "swap_ng",
SubCommand.FUND_NG: "fund_ng",
SubCommand.SELL_NG: "sell_ng",
}
FEES = 100

class TestNGConfiguration:

@pytest.mark.parametrize("specs_param", [SWAP_NG_SPECS, SELL_NG_SPECS, FUND_NG_SPECS], ids=["swap_ng", "sell_ng", "fund_ng"])
@pytest.mark.parametrize("payload_encoding", [PayloadEncoding.BASE_64_URL, PayloadEncoding.BYTES_ARRAY], ids=["base_56", "b_array"])
@pytest.mark.parametrize("signature_encoding", [SignatureEncoding.PLAIN_R_S, SignatureEncoding.DER], ids=["R,S", "DER"])
@pytest.mark.parametrize("signature_computation", [SignatureComputation.DOT_PREFIXED_BASE_64_URL, SignatureComputation.BINARY_ENCODED_PAYLOAD], ids=["dot_prefixed", "not_prefixed"])
def test_ng_tx_configuration(self, backend, specs_param, payload_encoding, signature_encoding, signature_computation):
specs = copy.deepcopy(specs_param)
specs.payload_encoding = payload_encoding
specs.signature_encoding = signature_encoding
specs.signature_computation = signature_computation

tx_infos = TX_INFOS[specs.subcommand_id]

ex = ExchangeClient(backend, Rate.FIXED, specs.subcommand_id)
partner = SigningAuthority(curve=get_partner_curve(specs.subcommand_id), name="Name")
transaction_id = ex.init_transaction().data
credentials = get_credentials(specs.subcommand_id, partner)
ex.set_partner_key(credentials)
ex.check_partner_key(LEDGER_SIGNER.sign(credentials))
tx, tx_signature = craft_and_sign_tx(specs, TX_INFOS[specs.subcommand_id], transaction_id, FEES, partner)
ex.process_transaction(tx)
ex.check_transaction_signature(tx_signature)


@pytest.mark.parametrize("specs_param", [SWAP_NG_SPECS, SELL_NG_SPECS, FUND_NG_SPECS], ids=["swap_ng", "sell_ng", "fund_ng"])
@pytest.mark.parametrize("partner_curve", [ec.SECP256R1(), ec.SECP256K1()], ids=["R1", "K1"])
def test_ng_curve_configuration(self, backend, specs_param, partner_curve):
specs = copy.deepcopy(specs_param)
specs.partner_curve = partner_curve

tx_infos = TX_INFOS[specs.subcommand_id]

ex = ExchangeClient(backend, Rate.FIXED, specs.subcommand_id)
partner = SigningAuthority(curve=get_partner_curve(specs.subcommand_id), name="Name")
transaction_id = ex.init_transaction().data
credentials = get_credentials(specs.subcommand_id, partner)
ex.set_partner_key(credentials)
ex.check_partner_key(LEDGER_SIGNER.sign(credentials))
tx, tx_signature = craft_and_sign_tx(specs, TX_INFOS[specs.subcommand_id], transaction_id, FEES, partner)
ex.process_transaction(tx)
ex.check_transaction_signature(tx_signature)
1 change: 0 additions & 1 deletion test/python/test_transaction_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,5 @@ def test_transaction_id(backend):

ex = ExchangeClient(backend, Rate.FIXED, SubCommand.FUND)
transaction_id = ex.init_transaction().data
print(transaction_id)
# Assert length
assert len(transaction_id) == 32

0 comments on commit 693ef99

Please sign in to comment.