refactor(core/stellar): prepare for contract creation support.
What changed, and why it matters
This commit is a routine internal code cleanup in the Stellar cryptocurrency support of Trezor firmware. It renames and moves helper functions, extracts reusable pieces, and adds tests. There is no change to user-visible behavior or security boundary, and no security issue is apparent.
No action required. Treat as normal refactoring.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit refactors Stellar/Soroban code: it introduces a shared HashIDPreimage header writer (write_hash_id_preimage_header), moves contract-address derivation from tokens.py to helpers.py as contract_address_from_asset, extracts _confirm_args and _same_xdr helpers, and updates tests. The logic for deriving SAC contract addresses and signing Soroban authorization payloads remains functionally identical; only code organization and naming changed. No new attack surface, missing validation, or cryptographic change is introduced.
Changed components
core/src/apps/stellar/helpers.pycore/src/apps/stellar/tokens.pycore/src/apps/stellar/writers.pycore/src/apps/stellar/sign_soroban_authorization.pycore/src/apps/stellar/layout.pycore/src/apps/stellar/operations/layout.pycore/tests/test_apps.stellar.address.pycore/tests/test_apps.stellar.tokens.pyInspect captured patch +144 / −86
### core/src/apps/stellar/consts.py
@@ -80,6 +80,10 @@
FLAG_AUTH_IMMUTABLE = const(4)
FLAGS_MAX_SIZE = const(7)
+# The HashIDPreimage variant contract IDs are derived from (CAP-46-02)
+# https://github.com/stellar/stellar-xdr/blob/v28.0/Stellar-ledger-entries.x#L666
+ENVELOPE_TYPE_CONTRACT_ID = const(8)
+
# SCSymbol is a string with a maximum length of 32
# https://github.com/stellar/stellar-xdr/blob/v26.0/Stellar-contract.x#L211
SCSYMBOL_MAX_SIZE = const(32)
### core/src/apps/stellar/helpers.py
@@ -6,6 +6,8 @@
if TYPE_CHECKING:
from buffer_types import AnyBytes
+ from trezor.messages import StellarAsset
+
# Stellar strkey version bytes
# See: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md
STRKEY_ED25519_PUBLIC_KEY = const(6) # G...
@@ -81,6 +83,22 @@ def decode_strkey(strkey: str) -> tuple[int, bytes]:
return version, data
+def contract_address_from_asset(network_id: AnyBytes, asset: StellarAsset) -> str:
+ """Derive the address (C...) of an asset's Stellar Asset Contract (SAC)."""
+ from trezor.crypto.hashlib import sha256
+
+ from .consts import ENVELOPE_TYPE_CONTRACT_ID
+ from .writers import (
+ write_contract_id_preimage_from_asset,
+ write_hash_id_preimage_header,
+ )
+
+ w = bytearray()
+ write_hash_id_preimage_header(w, ENVELOPE_TYPE_CONTRACT_ID, network_id)
+ write_contract_id_preimage_from_asset(w, asset)
+ return encode_strkey(STRKEY_CONTRACT, sha256(w).digest())
+
+
def _crc16_checksum(data: AnyBytes) -> bytes:
"""Returns the CRC-16 checksum of bytearray bytes
### core/src/apps/stellar/layout.py
@@ -288,11 +288,20 @@ async def _confirm_invoke_contract_args(
args.function_name,
description=TR.words__function if authorization_title else None,
)
- if not args.args:
+ await _confirm_args(args.args, br_name_prefix, authorization_title)
+
+
+async def _confirm_args(
+ args: list[StellarSCVal],
+ br_name_prefix: str,
+ authorization_title: str | None,
+) -> None:
+ """Confirm the arguments of a call, if any, one formatted value each."""
+ if not args:
return
props = [
- (f"{i + 1} / {len(args.args)}", _format_sc_val(arg), True)
- for i, arg in enumerate(args.args)
+ (f"{i + 1} / {len(args)}", _format_sc_val(arg), True)
+ for i, arg in enumerate(args)
]
await layouts.confirm_properties(
f"{br_name_prefix}_args",
### core/src/apps/stellar/operations/layout.py
@@ -15,6 +15,8 @@
if TYPE_CHECKING:
from buffer_types import AnyBytes, StrOrBytes
+ from collections.abc import Callable
+ from typing import TypeVar
from trezor.messages import (
StellarAccountMergeOp,
@@ -37,6 +39,9 @@
StellarSorobanAuthorizationEntry,
)
from trezor.ui.layouts import PropertyType
+ from trezor.utils import Writer
+
+ T = TypeVar("T")
async def confirm_source_account(source_account: str) -> None:
@@ -432,6 +437,11 @@ async def confirm_asset_issuer(asset: StellarAsset) -> None:
def _is_root_auth_entry(
auth_entry: StellarSorobanAuthorizationEntry, invoked_fn: StellarHostFunction
) -> bool:
+ """Whether the entry's root invocation is the invoked host function itself.
+
+ Such an entry only authorizes what the user has already confirmed, so its
+ root is not shown again. The two are compared in their XDR form.
+ """
from trezor.enums import (
StellarHostFunctionType,
StellarSorobanAuthorizedFunctionType,
@@ -447,17 +457,22 @@ def _is_root_auth_entry(
and invoked_fn.type
== StellarHostFunctionType.HOST_FUNCTION_TYPE_INVOKE_CONTRACT
):
- if auth_fn.contract_fn is None or invoked_fn.invoke_contract is None:
- return False
-
- b1 = bytearray()
- write_invoke_contract_args(b1, auth_fn.contract_fn)
- b2 = bytearray()
- write_invoke_contract_args(b2, invoked_fn.invoke_contract)
+ return _same_xdr(
+ write_invoke_contract_args,
+ auth_fn.contract_fn,
+ invoked_fn.invoke_contract,
+ )
+ return False
- return b1 == b2
- return False
+def _same_xdr(write: Callable[[Writer, T], None], a: T | None, b: T | None) -> bool:
+ if a is None or b is None:
+ return False
+ b1 = bytearray()
+ write(b1, a)
+ b2 = bytearray()
+ write(b2, b)
+ return b1 == b2
async def confirm_invoke_host_function_op(
### core/src/apps/stellar/sign_soroban_authorization.py
@@ -49,8 +49,7 @@ async def sign_soroban_authorization(
network_id = sha256(msg.network_passphrase.encode()).digest()
w = bytearray()
- writers.write_uint32(w, msg.envelope_type)
- writers.write_bytes_fixed(w, network_id, 32)
+ writers.write_hash_id_preimage_header(w, msg.envelope_type, network_id)
writers.write_int64(w, auth.nonce)
writers.write_uint32(w, auth.signature_expiration_ledger)
writers.write_sc_address(w, auth.address)
### core/src/apps/stellar/tokens.py
@@ -5,8 +5,7 @@
from trezor.wire import DataError
from .consts import AMOUNT_DECIMALS, NETWORK_PASSPHRASE_PUBLIC
-from .helpers import STRKEY_CONTRACT, encode_strkey
-from .writers import write_asset, write_bytes_fixed, write_uint32
+from .helpers import contract_address_from_asset
if TYPE_CHECKING:
from buffer_types import AnyBytes
@@ -65,19 +64,6 @@ def format(self, amount: int) -> str:
NATIVE_TOKEN = StellarToken("XLM", AMOUNT_DECIMALS, None)
-def sac_address_from_asset(network_id: AnyBytes, asset: StellarAsset) -> str:
- """Derive the address of the Stellar Asset Contract (SAC) of an asset (C...).
-
- See https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-02.md#contract-identifier-preimage-type
- """
- w = bytearray()
- write_uint32(w, 8) # ENVELOPE_TYPE_CONTRACT_ID
- write_bytes_fixed(w, network_id, 32)
- write_uint32(w, 1) # CONTRACT_ID_PREIMAGE_FROM_ASSET
- write_asset(w, asset)
- return encode_strkey(STRKEY_CONTRACT, sha256(w).digest())
-
-
def resolve_sep41_token(
args: StellarInvokeContractArgs, network_id: AnyBytes
) -> StellarToken | None:
@@ -96,7 +82,7 @@ def resolve_sep41_token(
asset = args.asset_hint
if asset is not None:
try:
- if sac_address_from_asset(network_id, asset) == contract:
+ if contract_address_from_asset(network_id, asset) == contract:
return StellarToken.from_asset(asset)
except DataError:
pass
### core/src/apps/stellar/writers.py
@@ -12,7 +12,7 @@
write_uint64 = writers.write_uint64_be
if TYPE_CHECKING:
- from buffer_types import StrOrBytes
+ from buffer_types import AnyBytes, StrOrBytes
from collections.abc import Callable
from typing import TypeVar
@@ -138,6 +138,25 @@ def write_invoke_contract_args(w: Writer, msg: StellarInvokeContractArgs) -> Non
write_vec(w, msg.args, write_sc_val)
+def write_hash_id_preimage_header(
+ w: Writer, envelope_type: int, network_id: AnyBytes
+) -> None:
+ """Write what every HashIDPreimage variant starts with: its envelope type
+ and the ID of the network it is bound to."""
+ write_uint32(w, envelope_type)
+ write_bytes_fixed(w, network_id, 32)
+
+
+def write_contract_id_preimage_from_asset(w: Writer, asset: StellarAsset) -> None:
+ """Write the CONTRACT_ID_PREIMAGE_FROM_ASSET variant of ContractIDPreimage.
+
+ Contracts are not created from it on the device; it only serves to derive
+ the address of an asset's Stellar Asset Contract.
+ """
+ write_uint32(w, 1) # CONTRACT_ID_PREIMAGE_FROM_ASSET
+ write_asset(w, asset)
+
+
def write_sc_address(w: Writer, addr: str) -> None:
from . import helpers
### core/tests/test_apps.stellar.address.py
@@ -4,13 +4,22 @@
from trezor.wire import DataError
if not utils.BITCOIN_ONLY:
+ from trezor.crypto.hashlib import sha256
+ from trezor.enums import StellarAssetType
+ from trezor.messages import StellarAsset
+
+ from apps.stellar.consts import (
+ NETWORK_PASSPHRASE_PUBLIC,
+ NETWORK_PASSPHRASE_TESTNET,
+ )
from apps.stellar.helpers import (
STRKEY_CLAIMABLE_BALANCE,
STRKEY_CONTRACT,
STRKEY_ED25519_PUBLIC_KEY,
STRKEY_LIQUIDITY_POOL,
STRKEY_MUXED_ACCOUNT,
address_from_public_key,
+ contract_address_from_asset,
decode_strkey,
encode_strkey,
public_key_from_address,
@@ -189,5 +198,59 @@ def test_decode_strkey_invalid_payload_size(self):
decode_strkey(strkey)
+@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
+class TestStellarContractAddress(unittest.TestCase):
+ # Expected addresses cross-checked against stellar_sdk's
+ # Asset.contract_id(); the PUBLIC USDC one is Circle's well-known SAC.
+ def test_contract_address_from_asset(self):
+ native = StellarAsset(type=StellarAssetType.NATIVE)
+ usdc = StellarAsset( # ALPHANUM4
+ type=StellarAssetType.ALPHANUM4,
+ code="USDC",
+ issuer="GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
+ )
+ ustry = StellarAsset( # ALPHANUM12
+ type=StellarAssetType.ALPHANUM12,
+ code="USTRY",
+ issuer="GCRYUGD5NVARGXT56XEZI5CIFCQETYHAPQQTHO2O3IQZTHDH4LATMYWC",
+ )
+ # the same asset resolves to a different contract on each network
+ VECTORS = (
+ (
+ NETWORK_PASSPHRASE_PUBLIC,
+ native,
+ "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA",
+ ),
+ (
+ NETWORK_PASSPHRASE_PUBLIC,
+ usdc,
+ "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75",
+ ),
+ (
+ NETWORK_PASSPHRASE_PUBLIC,
+ ustry,
+ "CBLV4ATSIWU67CFSQU2NVRKINQIKUZ2ODSZBUJTJ43VJVRSBTZYOPNUR",
+ ),
+ (
+ NETWORK_PASSPHRASE_TESTNET,
+ native,
+ "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
+ ),
+ (
+ NETWORK_PASSPHRASE_TESTNET,
+ usdc,
+ "CA2E53VHFZ6YSWQIEIPBXJQGT6VW3VKWWZO555XKRQXYJ63GEBJJGHY7",
+ ),
+ (
+ NETWORK_PASSPHRASE_TESTNET,
+ ustry,
+ "CBEHZAPSMUJXT6R4X4LSQYEOOSNBNUQISUTHJNDZKMPSZQEKJC753HR3",
+ ),
+ )
+ for passphrase, asset, expected in VECTORS:
+ network_id = sha256(passphrase.encode()).digest()
+ self.assertEqual(contract_address_from_asset(network_id, asset), expected)
+
+
if __name__ == "__main__":
unittest.main()
### core/tests/test_apps.stellar.tokens.py
@@ -16,64 +16,9 @@
PUBLIC_TOKENS,
StellarToken,
resolve_sep41_token,
- sac_address_from_asset,
)
-@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
-class TestStellarTokens(unittest.TestCase):
- # Expected addresses cross-checked against stellar_sdk's
- # Asset.contract_id(); the PUBLIC USDC one is Circle's well-known SAC.
- def test_sac_address_from_asset(self):
- native = StellarAsset(type=StellarAssetType.NATIVE)
- usdc = StellarAsset( # ALPHANUM4
- type=StellarAssetType.ALPHANUM4,
- code="USDC",
- issuer="GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
- )
- ustry = StellarAsset( # ALPHANUM12
- type=StellarAssetType.ALPHANUM12,
- code="USTRY",
- issuer="GCRYUGD5NVARGXT56XEZI5CIFCQETYHAPQQTHO2O3IQZTHDH4LATMYWC",
- )
- # the same asset resolves to a different contract on each network
- VECTORS = (
- (
- NETWORK_PASSPHRASE_PUBLIC,
- native,
- "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA",
- ),
- (
- NETWORK_PASSPHRASE_PUBLIC,
- usdc,
- "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75",
- ),
- (
- NETWORK_PASSPHRASE_PUBLIC,
- ustry,
- "CBLV4ATSIWU67CFSQU2NVRKINQIKUZ2ODSZBUJTJ43VJVRSBTZYOPNUR",
- ),
- (
- NETWORK_PASSPHRASE_TESTNET,
- native,
- "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
- ),
- (
- NETWORK_PASSPHRASE_TESTNET,
- usdc,
- "CA2E53VHFZ6YSWQIEIPBXJQGT6VW3VKWWZO555XKRQXYJ63GEBJJGHY7",
- ),
- (
- NETWORK_PASSPHRASE_TESTNET,
- ustry,
- "CBEHZAPSMUJXT6R4X4LSQYEOOSNBNUQISUTHJNDZKMPSZQEKJC753HR3",
- ),
- )
- for passphrase, asset, expected in VECTORS:
- network_id = sha256(passphrase.encode()).digest()
- self.assertEqual(sac_address_from_asset(network_id, asset), expected)
-
-
_SOLVBTC = "CBIJBDNZNF4X35BJ4FFZWCDBSCKOP5NB4PLG4SNENRMLAPYG4P5FM6VN"
# an ordinary contract, not a token known to the firmware
_UNKNOWN_CONTRACT = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"Why this scored 14/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.