diff --git a/deps/device-protocol b/deps/device-protocol
index b07589ff3..acd10830f 160000
--- a/deps/device-protocol
+++ b/deps/device-protocol
@@ -1 +1 @@
-Subproject commit b07589ff386265f894736a490fc163d46f9479b7
+Subproject commit acd10830f606751105b75b34c0bb60b15935ec8a
diff --git a/deps/python-keepkey b/deps/python-keepkey
index e89587701..f1dd2b684 160000
--- a/deps/python-keepkey
+++ b/deps/python-keepkey
@@ -1 +1 @@
-Subproject commit e8958770152a85af4c2a03f9e93b5d98e8dff44b
+Subproject commit f1dd2b6847346abe8ea2985b22d688de4911643f
diff --git a/include/keepkey/firmware/tron.h b/include/keepkey/firmware/tron.h
index 22abb5c26..cb6b79f1a 100644
--- a/include/keepkey/firmware/tron.h
+++ b/include/keepkey/firmware/tron.h
@@ -27,32 +27,89 @@
// TRON address length (Base58Check, typically 34 chars starting with 'T')
#define TRON_ADDRESS_MAX_LEN 64
+// TRON address raw size (1 prefix + 20 hash)
+#define TRON_ADDRESS_SIZE 21
+
+// TRON max encoded address size
+#define MAX_TRON_ADDR_SIZE 35
+
+// TRON mainnet address prefix
+#define TRON_MAINNET_PREFIX 0x41
+
// TRON decimals (1 TRX = 1,000,000 SUN)
#define TRON_DECIMALS 6
+// TRON signature size (r + s + recovery_id)
+#define TRON_SIGNATURE_SIZE 65
+
+// TRC-20 transfer(address,uint256) function selector
+#define TRC20_TRANSFER_SELECTOR 0xa9059cbb
+
/**
* Generate TRON address from secp256k1 public key
- * @param public_key secp256k1 public key (33 bytes compressed)
- * @param address Output buffer for Base58Check encoded address
- * @param address_len Length of output buffer
- * @return true on success, false on failure
*/
bool tron_getAddress(const uint8_t public_key[33], char *address,
size_t address_len);
+/**
+ * Decode a Base58Check TRON address to raw 21-byte form
+ * @param address Base58Check encoded "T..." address
+ * @param raw_address Output buffer (21 bytes: 0x41 prefix + 20 hash)
+ * @return true on success, false if decode fails or prefix is wrong
+ */
+bool tron_decodeAddress(const char *address,
+ uint8_t raw_address[TRON_ADDRESS_SIZE]);
+
+/**
+ * Validate a Base58Check TRON address
+ */
+bool tron_validateAddress(const char *address);
+
/**
* Format TRON amount (SUN) for display
- * @param buf Output buffer
- * @param len Length of output buffer
- * @param amount Amount in SUN (1 TRX = 1,000,000 SUN)
*/
void tron_formatAmount(char *buf, size_t len, uint64_t amount);
/**
- * Sign a TRON transaction
- * @param node HD node containing private key
- * @param msg TronSignTx request message
- * @param resp TronSignedTx response message (will be filled with signature)
+ * Format a token amount with given decimals for display
+ * @param buf Output buffer
+ * @param buf_len Buffer size
+ * @param amount_be 32-byte big-endian amount
+ * @param decimals Token decimal places
+ * @param symbol Token symbol (e.g., "USDT")
+ */
+void tron_formatTokenAmount(char *buf, size_t buf_len,
+ const uint8_t amount_be[32],
+ uint8_t decimals, const char *symbol);
+
+/**
+ * Decode TRC-20 transfer(address,uint256) ABI call data
+ * @param data ABI-encoded call data (>= 68 bytes)
+ * @param data_len Length of call data
+ * @param to_raw Output: 21-byte TRON address (0x41 + 20-byte EVM addr)
+ * @param amount_bytes Output: 32-byte big-endian amount
+ * @return true if data matches transfer(address,uint256) selector
+ */
+bool tron_decodeTRC20Transfer(const uint8_t *data, size_t data_len,
+ uint8_t to_raw[TRON_ADDRESS_SIZE],
+ uint8_t amount_bytes[32]);
+
+/**
+ * Reconstruct Transaction.raw protobuf from structured fields
+ * @param msg TronSignTx with structured fields
+ * @param owner_raw 21-byte raw address of the signer
+ * @param out Output buffer for serialized protobuf
+ * @param out_len In: max capacity; Out: bytes written
+ * @param max_len Maximum buffer size
+ * @return true on success
+ */
+bool tron_serializeRawTransaction(const TronSignTx *msg,
+ const uint8_t *owner_raw,
+ uint8_t *out, size_t *out_len,
+ size_t max_len);
+
+/**
+ * Sign a TRON transaction (supports both structured and legacy modes)
*/
bool tron_signTx(const HDNode *node, const TronSignTx *msg,
TronSignedTx *resp);
diff --git a/include/keepkey/transport/messages-ton.options b/include/keepkey/transport/messages-ton.options
index 18c064289..bec6d9857 100644
--- a/include/keepkey/transport/messages-ton.options
+++ b/include/keepkey/transport/messages-ton.options
@@ -9,4 +9,6 @@ TonSignTx.coin_name max_size:21
TonSignTx.raw_tx max_size:1024
TonSignTx.to_address max_size:50
+TonAddress.address max_size:50
+TonAddress.raw_address max_size:70
TonSignedTx.signature max_size:64
diff --git a/include/keepkey/transport/messages-tron.options b/include/keepkey/transport/messages-tron.options
index 80215e8cf..05aef1433 100644
--- a/include/keepkey/transport/messages-tron.options
+++ b/include/keepkey/transport/messages-tron.options
@@ -3,12 +3,27 @@ TronGetAddress.coin_name max_size:21
TronAddress.address max_size:35
+TronTransferContract.to_address max_size:35
+
+TronTriggerSmartContract.contract_address max_size:35
+TronTriggerSmartContract.data max_size:512
+
TronSignTx.address_n max_count:8
TronSignTx.coin_name max_size:21
-TronSignTx.raw_data max_size:2048
-TronSignTx.ref_block_bytes max_size:4
-TronSignTx.ref_block_hash max_size:32
+TronSignTx.raw_data max_size:1024
+TronSignTx.ref_block_bytes max_size:2
+TronSignTx.ref_block_hash max_size:8
TronSignTx.contract_type max_size:64
TronSignTx.to_address max_size:35
+TronSignTx.data max_size:256
+TronSignTx.address_n max_count:8
+TronSignTx.coin_name max_size:21
+TronSignTx.raw_data max_size:1024
+TronSignTx.ref_block_bytes max_size:2
+TronSignTx.ref_block_hash max_size:8
+TronSignTx.contract_type max_size:50
+TronSignTx.to_address max_size:35
+TronAddress.address max_size:35
TronSignedTx.signature max_size:65
+TronSignedTx.serialized_tx max_size:1024
diff --git a/lib/firmware/fsm_msg_tron.h b/lib/firmware/fsm_msg_tron.h
index beea2f769..48d2b6452 100644
--- a/lib/firmware/fsm_msg_tron.h
+++ b/lib/firmware/fsm_msg_tron.h
@@ -17,6 +17,8 @@
* along with this library. If not, see .
*/
+#include "tron_tokens.h"
+
void fsm_msgTronGetAddress(const TronGetAddress *msg) {
RESP_INIT(TronAddress);
@@ -34,13 +36,11 @@ void fsm_msgTronGetAddress(const TronGetAddress *msg) {
return;
}
- // Derive node using secp256k1 curve
HDNode *node = fsm_getDerivedNode(SECP256K1_NAME, msg->address_n,
msg->address_n_count, NULL);
if (!node) return;
hdnode_fill_public_key(node);
- // Get TRON address from public key (Base58Check with prefix 'T')
char address[MAX_ADDR_SIZE];
if (!tron_getAddress(node->public_key, address, sizeof(address))) {
memzero(node, sizeof(*node));
@@ -52,7 +52,6 @@ void fsm_msgTronGetAddress(const TronGetAddress *msg) {
resp->has_address = true;
strlcpy(resp->address, address, sizeof(resp->address));
- // Show address on display if requested
if (msg->has_show_display && msg->show_display) {
char node_str[NODE_STRING_LENGTH];
if (!bip32_path_to_string(node_str, sizeof(node_str), msg->address_n,
@@ -91,47 +90,250 @@ void fsm_msgTronSignTx(TronSignTx *msg) {
return;
}
-
- // Derive node using secp256k1 curve
HDNode *node = fsm_getDerivedNode(SECP256K1_NAME, msg->address_n,
msg->address_n_count, NULL);
if (!node) return;
hdnode_fill_public_key(node);
- if (!msg->has_raw_data || msg->raw_data.size == 0) {
+ bool is_structured = msg->has_transfer || msg->has_trigger_smart;
+ bool is_legacy = msg->has_raw_data && msg->raw_data.size > 0;
+
+ if (!is_structured && !is_legacy) {
memzero(node, sizeof(*node));
- fsm_sendFailure(FailureType_Failure_Other,
- _("Missing transaction data"));
+ fsm_sendFailure(FailureType_Failure_SyntaxError,
+ _("Must provide transfer, trigger_smart, or raw_data"));
+ layoutHome();
+ return;
+ }
+
+ /* Reject if both contract types are present — exactly one required */
+ if (msg->has_transfer && msg->has_trigger_smart) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_SyntaxError,
+ _("Cannot set both transfer and trigger_smart"));
layoutHome();
return;
}
- bool needs_confirm = true;
+ /* ---- Structured reconstruct-then-sign path ---- */
+ if (is_structured) {
+ /* Validate required header fields */
+ if (!msg->has_ref_block_bytes || msg->ref_block_bytes.size != 2) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_SyntaxError,
+ _("ref_block_bytes must be exactly 2 bytes"));
+ layoutHome();
+ return;
+ }
+ if (!msg->has_ref_block_hash || msg->ref_block_hash.size != 8) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_SyntaxError,
+ _("ref_block_hash must be exactly 8 bytes"));
+ layoutHome();
+ return;
+ }
+ if (!msg->has_expiration) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_SyntaxError,
+ _("expiration is required"));
+ layoutHome();
+ return;
+ }
+
+ /* Show memo if present */
+ if (msg->has_data && msg->data.size > 0) {
+ char memo_hex[65];
+ size_t show = msg->data.size < 32 ? msg->data.size : 32;
+ for (size_t i = 0; i < show; i++) {
+ snprintf(memo_hex + i * 2, 3, "%02x", msg->data.bytes[i]);
+ }
+ if (msg->data.size > 32) {
+ strlcat(memo_hex, "...", sizeof(memo_hex));
+ }
+ if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput,
+ "Memo", "Transaction memo:\n%s", memo_hex)) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_ActionCancelled,
+ _("Signing cancelled"));
+ layoutHome();
+ return;
+ }
+ }
+
+ /* ---- TransferContract path ---- */
+ if (msg->has_transfer) {
+ /* Validate to_address BEFORE display */
+ if (!tron_validateAddress(msg->transfer.to_address)) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_SyntaxError,
+ _("Invalid to_address in transfer"));
+ layoutHome();
+ return;
+ }
+
+ char amount_str[64];
+ tron_formatAmount(amount_str, sizeof(amount_str), msg->transfer.amount);
+
+ if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput,
+ "Send TRX", "Send %s to\n%s?",
+ amount_str, msg->transfer.to_address)) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_ActionCancelled,
+ _("Signing cancelled"));
+ layoutHome();
+ return;
+ }
+ }
+
+ /* ---- TriggerSmartContract path ---- */
+ if (msg->has_trigger_smart) {
+ /* Validate contract_address BEFORE any display */
+ if (!tron_validateAddress(msg->trigger_smart.contract_address)) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_SyntaxError,
+ _("Invalid contract_address"));
+ layoutHome();
+ return;
+ }
+
+ /* Attempt TRC-20 transfer(address,uint256) decode */
+ uint8_t trc20_to[TRON_ADDRESS_SIZE];
+ uint8_t trc20_amount[32];
+ bool is_trc20 = msg->trigger_smart.has_data &&
+ msg->trigger_smart.data.size >= 68 &&
+ tron_decodeTRC20Transfer(msg->trigger_smart.data.bytes,
+ msg->trigger_smart.data.size,
+ trc20_to, trc20_amount);
- // Display transaction details if available
- if (needs_confirm && msg->has_to_address && msg->has_amount) {
- char amount_str[32];
- tron_formatAmount(amount_str, sizeof(amount_str), msg->amount);
+ if (is_trc20) {
+ /* Look up known token by contract address */
+ const TronToken *token =
+ tron_token_by_address(msg->trigger_smart.contract_address);
+
+ char to_addr[MAX_TRON_ADDR_SIZE];
+ if (!base58_encode_check(trc20_to, 21, HASHER_SHA2D, to_addr,
+ sizeof(to_addr))) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_Other,
+ _("Failed to encode recipient address"));
+ layoutHome();
+ return;
+ }
+
+ if (token) {
+ char amt_str[64];
+ tron_formatTokenAmount(amt_str, sizeof(amt_str), trc20_amount,
+ token->decimals, token->symbol);
+ if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput,
+ "Send Token", "Send %s to\n%s?",
+ amt_str, to_addr)) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_ActionCancelled,
+ _("Signing cancelled"));
+ layoutHome();
+ return;
+ }
+ } else {
+ /* Unknown TRC-20 token */
+ if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput,
+ "Unknown Token",
+ "Transfer unknown token at\n%s\nto %s?",
+ msg->trigger_smart.contract_address, to_addr)) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_ActionCancelled,
+ _("Signing cancelled"));
+ layoutHome();
+ return;
+ }
+ }
+ } else {
+ /* Not a TRC-20 transfer — generic smart contract call */
+ if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput,
+ "Contract Call",
+ "Call contract\n%s?\nCannot verify call data.",
+ msg->trigger_smart.contract_address)) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_ActionCancelled,
+ _("Signing cancelled"));
+ layoutHome();
+ return;
+ }
+ }
+
+ /* Show call_value if sending TRX along with the smart contract call */
+ if (msg->trigger_smart.has_call_value &&
+ msg->trigger_smart.call_value > 0) {
+ char call_str[64];
+ tron_formatAmount(call_str, sizeof(call_str),
+ msg->trigger_smart.call_value);
+ if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput,
+ "Call Value",
+ "Also sending %s with call?", call_str)) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_ActionCancelled,
+ _("Signing cancelled"));
+ layoutHome();
+ return;
+ }
+ }
+ }
+
+ /* Show fee limit if present */
+ if (msg->has_fee_limit && msg->fee_limit > 0) {
+ char fee_str[64];
+ tron_formatAmount(fee_str, sizeof(fee_str), msg->fee_limit);
+ if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput,
+ "Fee Limit", "Maximum fee:\n%s?", fee_str)) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_ActionCancelled,
+ _("Signing cancelled"));
+ layoutHome();
+ return;
+ }
+ }
- if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput,
- "Send", "Send %s TRX to %s?",
- amount_str, msg->to_address)) {
+ /* Final confirmation */
+ if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Sign",
+ "Sign this TRON transaction?")) {
memzero(node, sizeof(*node));
- fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled");
+ fsm_sendFailure(FailureType_Failure_ActionCancelled,
+ _("Signing cancelled"));
layoutHome();
return;
}
}
- if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Transaction",
- "Really sign this TRON transaction?")) {
- memzero(node, sizeof(*node));
- fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled");
- layoutHome();
- return;
+ /* ---- Legacy blind-sign path ---- */
+ if (!is_structured) {
+ /* Blind-sign warning: user must understand this is unverified */
+ if (!confirm(ButtonRequestType_ButtonRequest_SignTx,
+ "Blind Sign",
+ "Sign unverified TRON\ntransaction?\n"
+ "Data cannot be verified\non-device.")) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_ActionCancelled,
+ _("Signing cancelled"));
+ layoutHome();
+ return;
+ }
+
+ /* Show optional unverified hints from host */
+ if (msg->has_to_address && msg->has_amount) {
+ char amount_str[64];
+ tron_formatAmount(amount_str, sizeof(amount_str), msg->amount);
+ if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput,
+ "Unverified", "Send %s to\n%s?\n(UNVERIFIED)",
+ amount_str, msg->to_address)) {
+ memzero(node, sizeof(*node));
+ fsm_sendFailure(FailureType_Failure_ActionCancelled,
+ _("Signing cancelled"));
+ layoutHome();
+ return;
+ }
+ }
}
- // Sign the transaction with secp256k1
+ /* Sign the transaction */
if (!tron_signTx(node, msg, resp)) {
memzero(node, sizeof(*node));
fsm_sendFailure(FailureType_Failure_Other, _("TRON signing failed"));
diff --git a/lib/firmware/tron.c b/lib/firmware/tron.c
index cbd5b0b4b..5a80973e1 100644
--- a/lib/firmware/tron.c
+++ b/lib/firmware/tron.c
@@ -24,17 +24,66 @@
#include "trezor/crypto/ecdsa.h"
#include "trezor/crypto/memzero.h"
#include "trezor/crypto/secp256k1.h"
+#include "trezor/crypto/sha2.h"
#include "trezor/crypto/sha3.h"
+#include
#include
-#define TRON_ADDRESS_PREFIX 0x41 // Mainnet addresses start with 'T'
+/* ------------------------------------------------------------------ */
+/* Bounded protobuf encoding helpers */
+/* ------------------------------------------------------------------ */
+
+/* How many bytes a varint takes. */
+static size_t pb_varint_size(uint64_t v) {
+ size_t n = 1;
+ while (v >= 0x80) { v >>= 7; n++; }
+ return n;
+}
+
+/* Encode a varint, checking remaining capacity. */
+static bool pb_encode_varint_safe(uint8_t *buf, size_t *pos, size_t cap,
+ uint64_t v) {
+ size_t need = pb_varint_size(v);
+ if (*pos + need > cap) return false;
+ while (v >= 0x80) {
+ buf[(*pos)++] = (uint8_t)(v & 0x7F) | 0x80;
+ v >>= 7;
+ }
+ buf[(*pos)++] = (uint8_t)v;
+ return true;
+}
+
+/* Write a protobuf tag (field_num << 3 | wire_type). */
+static bool pb_write_tag_safe(uint8_t *buf, size_t *pos, size_t cap,
+ unsigned field, unsigned wire) {
+ return pb_encode_varint_safe(buf, pos, cap,
+ ((uint64_t)field << 3) | wire);
+}
+
+/* Write a length-delimited field (wire type 2). */
+static bool pb_write_bytes_safe(uint8_t *buf, size_t *pos, size_t cap,
+ unsigned field,
+ const uint8_t *data, size_t data_len) {
+ if (!pb_write_tag_safe(buf, pos, cap, field, 2)) return false;
+ if (!pb_encode_varint_safe(buf, pos, cap, data_len)) return false;
+ if (*pos + data_len > cap) return false;
+ memcpy(buf + *pos, data, data_len);
+ *pos += data_len;
+ return true;
+}
+
+/* Write a varint field (wire type 0). */
+static bool pb_write_varint_field_safe(uint8_t *buf, size_t *pos, size_t cap,
+ unsigned field, uint64_t val) {
+ if (!pb_write_tag_safe(buf, pos, cap, field, 0)) return false;
+ return pb_encode_varint_safe(buf, pos, cap, val);
+}
+
+/* ------------------------------------------------------------------ */
+/* Address functions */
+/* ------------------------------------------------------------------ */
-/**
- * Generate TRON address from secp256k1 public key
- * TRON uses Keccak256(uncompressed_pubkey) and takes last 20 bytes,
- * then prepends 0x41 and Base58Check encodes it
- */
bool tron_getAddress(const uint8_t public_key[33], char *address,
size_t address_len) {
if (address_len < TRON_ADDRESS_MAX_LEN) {
@@ -45,88 +94,353 @@ bool tron_getAddress(const uint8_t public_key[33], char *address,
uint8_t hash[32];
uint8_t addr_bytes[21];
- // Uncompress the public key
if (!ecdsa_uncompress_pubkey(&secp256k1, public_key, uncompressed_pubkey)) {
return false;
}
- // Keccak256 hash of uncompressed public key (skip first 0x04 byte)
keccak_256(uncompressed_pubkey + 1, 64, hash);
- // Take last 20 bytes of hash and prepend TRON prefix byte
- addr_bytes[0] = TRON_ADDRESS_PREFIX;
+ addr_bytes[0] = TRON_MAINNET_PREFIX;
memcpy(addr_bytes + 1, hash + 12, 20);
- // Base58Check encode with double SHA256
if (!base58_encode_check(addr_bytes, 21, HASHER_SHA2D, address,
address_len)) {
+ memzero(uncompressed_pubkey, sizeof(uncompressed_pubkey));
+ memzero(hash, sizeof(hash));
return false;
}
- // Clean up sensitive data
memzero(uncompressed_pubkey, sizeof(uncompressed_pubkey));
memzero(hash, sizeof(hash));
+ return true;
+}
+bool tron_decodeAddress(const char *address,
+ uint8_t raw_address[TRON_ADDRESS_SIZE]) {
+ if (!address || strlen(address) < 25 || strlen(address) > 36) {
+ return false;
+ }
+ uint8_t decoded[25]; /* 21-byte payload + 4-byte checksum */
+ int len = base58_decode_check(address, HASHER_SHA2D, decoded,
+ sizeof(decoded));
+ if (len != 21) return false;
+ if (decoded[0] != TRON_MAINNET_PREFIX) return false;
+ memcpy(raw_address, decoded, 21);
return true;
}
-/**
- * Format TRON amount (SUN) for display
- * 1 TRX = 1,000,000 SUN
- */
+bool tron_validateAddress(const char *address) {
+ uint8_t raw[TRON_ADDRESS_SIZE];
+ return tron_decodeAddress(address, raw);
+}
+
+/* ------------------------------------------------------------------ */
+/* Formatting */
+/* ------------------------------------------------------------------ */
+
void tron_formatAmount(char *buf, size_t len, uint64_t amount) {
bignum256 val;
bn_read_uint64(amount, &val);
bn_format(&val, NULL, " TRX", TRON_DECIMALS, 0, false, buf, len);
}
-/**
- * Sign a TRON transaction with secp256k1
- */
-bool tron_signTx(const HDNode *node, const TronSignTx *msg,
- TronSignedTx *resp) {
- if (!node || !msg || !resp) {
+void tron_formatTokenAmount(char *buf, size_t buf_len,
+ const uint8_t amount_be[32],
+ uint8_t decimals, const char *symbol) {
+ /* Check if amount fits in uint64 (first 24 bytes must be zero). */
+ bool fits_u64 = true;
+ for (int i = 0; i < 24; i++) {
+ if (amount_be[i] != 0) { fits_u64 = false; break; }
+ }
+
+ if (fits_u64) {
+ uint64_t val64 = 0;
+ for (int i = 24; i < 32; i++) {
+ val64 = (val64 << 8) | amount_be[i];
+ }
+ bignum256 bn;
+ bn_read_uint64(val64, &bn);
+ char suffix[20];
+ snprintf(suffix, sizeof(suffix), " %s", symbol);
+ bn_format(&bn, NULL, suffix, decimals, 0, false, buf, buf_len);
+ } else {
+ snprintf(buf, buf_len, "(raw) %s", symbol);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* TRC-20 ABI decoding */
+/* ------------------------------------------------------------------ */
+
+bool tron_decodeTRC20Transfer(const uint8_t *data, size_t data_len,
+ uint8_t to_raw[TRON_ADDRESS_SIZE],
+ uint8_t amount_bytes[32]) {
+ if (data_len < 68) return false;
+
+ /* Check 4-byte function selector: transfer(address,uint256) = 0xa9059cbb */
+ if (data[0] != 0xa9 || data[1] != 0x05 ||
+ data[2] != 0x9c || data[3] != 0xbb) {
return false;
}
- // Verify we have raw transaction data
- if (!msg->has_raw_data || msg->raw_data.size == 0) {
+ /* Bytes 4..35: ABI-encoded address (12 zero bytes + 20-byte EVM address).
+ * TRON ABI uses standard Solidity encoding — no 0x41 prefix in the ABI.
+ * We verify the 12 leading bytes are zero, then prepend 0x41 ourselves. */
+ for (int i = 4; i < 16; i++) {
+ if (data[i] != 0) return false;
+ }
+ to_raw[0] = TRON_MAINNET_PREFIX;
+ memcpy(to_raw + 1, data + 16, 20);
+
+ /* Bytes 36..67: uint256 amount (big-endian) */
+ memcpy(amount_bytes, data + 36, 32);
+ return true;
+}
+
+/* ------------------------------------------------------------------ */
+/* Protobuf serialization (reconstruct-then-sign) */
+/* ------------------------------------------------------------------ */
+
+/* Serialize a TransferContract into a contract entry (field 11 of raw). */
+static bool tron_serializeTransferContract(uint8_t *buf, size_t *pos,
+ size_t cap,
+ const TronTransferContract *tc,
+ const uint8_t *owner_raw) {
+ /* Build inner TransferContract protobuf:
+ * field 1 (bytes): owner_address (21 bytes)
+ * field 2 (bytes): to_address (21 bytes)
+ * field 3 (varint): amount
+ */
+ uint8_t inner[128];
+ size_t ip = 0;
+
+ /* owner_address */
+ if (!pb_write_bytes_safe(inner, &ip, sizeof(inner), 1, owner_raw, 21))
+ return false;
+
+ /* to_address — decode from Base58 */
+ uint8_t to_raw[TRON_ADDRESS_SIZE];
+ if (!tron_decodeAddress(tc->to_address, to_raw)) return false;
+ if (!pb_write_bytes_safe(inner, &ip, sizeof(inner), 2, to_raw, 21))
+ return false;
+
+ /* amount */
+ if (!pb_write_varint_field_safe(inner, &ip, sizeof(inner), 3, tc->amount))
+ return false;
+
+ /* Build contract wrapper:
+ * field 1 (varint): type = 1 (TransferContract)
+ * field 2 (bytes): google.protobuf.Any {
+ * field 1 (bytes): type_url
+ * field 2 (bytes): value = inner
+ * }
+ */
+ static const char type_url[] =
+ "type.googleapis.com/protocol.TransferContract";
+ uint8_t any[256];
+ size_t ap = 0;
+ if (!pb_write_bytes_safe(any, &ap, sizeof(any), 1,
+ (const uint8_t *)type_url, strlen(type_url)))
+ return false;
+ if (!pb_write_bytes_safe(any, &ap, sizeof(any), 2, inner, ip))
+ return false;
+
+ uint8_t contract[300];
+ size_t cp = 0;
+ if (!pb_write_varint_field_safe(contract, &cp, sizeof(contract), 1, 1))
+ return false;
+ if (!pb_write_bytes_safe(contract, &cp, sizeof(contract), 2, any, ap))
+ return false;
+
+ /* Write as field 11 of the outer transaction.raw */
+ return pb_write_bytes_safe(buf, pos, cap, 11, contract, cp);
+}
+
+/* Serialize a TriggerSmartContract into a contract entry. */
+static bool tron_serializeTriggerSmartContract(
+ uint8_t *buf, size_t *pos, size_t cap,
+ const TronTriggerSmartContract *tsc, const uint8_t *owner_raw) {
+ /* Inner TriggerSmartContract:
+ * field 1 (bytes): owner_address (21)
+ * field 2 (bytes): contract_address (21)
+ * field 3 (bytes): data (ABI call data)
+ * field 4 (varint): call_value
+ */
+ uint8_t inner[700];
+ size_t ip = 0;
+
+ if (!pb_write_bytes_safe(inner, &ip, sizeof(inner), 1, owner_raw, 21))
+ return false;
+
+ uint8_t contract_raw[TRON_ADDRESS_SIZE];
+ if (!tron_decodeAddress(tsc->contract_address, contract_raw)) return false;
+ if (!pb_write_bytes_safe(inner, &ip, sizeof(inner), 2, contract_raw, 21))
return false;
+
+ if (tsc->has_data && tsc->data.size > 0) {
+ if (!pb_write_bytes_safe(inner, &ip, sizeof(inner), 3,
+ tsc->data.bytes, tsc->data.size))
+ return false;
}
- // Get the curve for secp256k1
- const curve_info *curve = get_curve_by_name(SECP256K1_NAME);
- if (!curve) {
+ if (tsc->has_call_value && tsc->call_value > 0) {
+ if (!pb_write_varint_field_safe(inner, &ip, sizeof(inner), 4,
+ tsc->call_value))
+ return false;
+ }
+
+ /* Wrap in Any with type_url for TriggerSmartContract */
+ static const char type_url[] =
+ "type.googleapis.com/protocol.TriggerSmartContract";
+ uint8_t any[768];
+ size_t ap = 0;
+ if (!pb_write_bytes_safe(any, &ap, sizeof(any), 1,
+ (const uint8_t *)type_url, strlen(type_url)))
+ return false;
+ if (!pb_write_bytes_safe(any, &ap, sizeof(any), 2, inner, ip))
+ return false;
+
+ /* contract type = 31 for TriggerSmartContract */
+ uint8_t contract[900];
+ size_t cpp = 0;
+ if (!pb_write_varint_field_safe(contract, &cpp, sizeof(contract), 1, 31))
+ return false;
+ if (!pb_write_bytes_safe(contract, &cpp, sizeof(contract), 2, any, ap))
+ return false;
+
+ return pb_write_bytes_safe(buf, pos, cap, 11, contract, cpp);
+}
+
+bool tron_serializeRawTransaction(const TronSignTx *msg,
+ const uint8_t *owner_raw,
+ uint8_t *out, size_t *out_len,
+ size_t max_len) {
+ size_t pos = 0;
+
+ /* Field 1: ref_block_bytes (2 bytes required) */
+ if (!msg->has_ref_block_bytes || msg->ref_block_bytes.size != 2)
+ return false;
+ if (!pb_write_bytes_safe(out, &pos, max_len, 1,
+ msg->ref_block_bytes.bytes, 2))
+ return false;
+
+ /* Field 4: ref_block_hash (8 bytes required) */
+ if (!msg->has_ref_block_hash || msg->ref_block_hash.size != 8)
+ return false;
+ if (!pb_write_bytes_safe(out, &pos, max_len, 4,
+ msg->ref_block_hash.bytes, 8))
+ return false;
+
+ /* Field 8: expiration */
+ if (!msg->has_expiration) return false;
+ if (!pb_write_varint_field_safe(out, &pos, max_len, 8, msg->expiration))
return false;
+
+ /* Field 10: data/memo (optional, max 256 bytes) */
+ if (msg->has_data && msg->data.size > 0) {
+ if (msg->data.size > 256) return false;
+ if (!pb_write_bytes_safe(out, &pos, max_len, 10,
+ msg->data.bytes, msg->data.size))
+ return false;
+ }
+
+ /* Field 11: contract (exactly one) */
+ if (msg->has_transfer) {
+ if (!tron_serializeTransferContract(out, &pos, max_len,
+ &msg->transfer, owner_raw))
+ return false;
+ } else if (msg->has_trigger_smart) {
+ if (!tron_serializeTriggerSmartContract(out, &pos, max_len,
+ &msg->trigger_smart, owner_raw))
+ return false;
+ } else {
+ return false; /* Must have exactly one contract */
+ }
+
+ /* Field 14: timestamp (optional) */
+ if (msg->has_timestamp) {
+ if (!pb_write_varint_field_safe(out, &pos, max_len, 14, msg->timestamp))
+ return false;
+ }
+
+ /* Field 18: fee_limit (optional) */
+ if (msg->has_fee_limit && msg->fee_limit > 0) {
+ if (!pb_write_varint_field_safe(out, &pos, max_len, 18, msg->fee_limit))
+ return false;
}
- // Hash the transaction with SHA256
+ *out_len = pos;
+ return true;
+}
+
+/* ------------------------------------------------------------------ */
+/* Transaction signing */
+/* ------------------------------------------------------------------ */
+
+bool tron_signTx(const HDNode *node, const TronSignTx *msg,
+ TronSignedTx *resp) {
+ if (!node || !msg || !resp) return false;
+
+ const curve_info *curve = get_curve_by_name(SECP256K1_NAME);
+ if (!curve) return false;
+
uint8_t hash[32];
- sha256_Raw(msg->raw_data.bytes, msg->raw_data.size, hash);
+ bool is_structured = msg->has_transfer || msg->has_trigger_smart;
+
+ if (is_structured) {
+ /* Derive owner address from public key */
+ uint8_t owner_raw[TRON_ADDRESS_SIZE];
+ {
+ uint8_t uncompressed[65];
+ uint8_t keccak[32];
+ if (!ecdsa_uncompress_pubkey(&secp256k1, node->public_key, uncompressed))
+ return false;
+ keccak_256(uncompressed + 1, 64, keccak);
+ owner_raw[0] = TRON_MAINNET_PREFIX;
+ memcpy(owner_raw + 1, keccak + 12, 20);
+ memzero(uncompressed, sizeof(uncompressed));
+ memzero(keccak, sizeof(keccak));
+ }
+
+ /* Reconstruct the raw transaction */
+ uint8_t raw_buf[1024];
+ size_t raw_len = sizeof(raw_buf);
+ if (!tron_serializeRawTransaction(msg, owner_raw, raw_buf, &raw_len,
+ sizeof(raw_buf))) {
+ memzero(owner_raw, sizeof(owner_raw));
+ return false;
+ }
+ memzero(owner_raw, sizeof(owner_raw));
+
+ /* Return the serialized bytes for host verification */
+ resp->has_serialized_tx = true;
+ resp->serialized_tx.size = raw_len;
+ memcpy(resp->serialized_tx.bytes, raw_buf, raw_len);
- // Sign with secp256k1 (recoverable signature: 65 bytes including recovery
- // ID)
+ sha256_Raw(raw_buf, raw_len, hash);
+ memzero(raw_buf, sizeof(raw_buf));
+ } else if (msg->has_raw_data && msg->raw_data.size > 0) {
+ /* Legacy: hash the client-provided raw_data */
+ sha256_Raw(msg->raw_data.bytes, msg->raw_data.size, hash);
+ } else {
+ return false;
+ }
+
+ /* Sign with secp256k1 */
uint8_t sig[65];
uint8_t pby;
-
if (ecdsa_sign_digest(&secp256k1, node->private_key, hash, sig, &pby,
NULL) != 0) {
memzero(hash, sizeof(hash));
return false;
}
-
- // Convert to recoverable signature format (r + s + recovery_id)
- // The recovery ID allows recovering the public key from the signature
sig[64] = pby;
- // Copy signature to response (65 bytes)
resp->has_signature = true;
resp->signature.size = 65;
memcpy(resp->signature.bytes, sig, 65);
- // Clean up sensitive data
memzero(hash, sizeof(hash));
memzero(sig, sizeof(sig));
-
return true;
}
diff --git a/lib/firmware/tron_tokens.h b/lib/firmware/tron_tokens.h
new file mode 100644
index 000000000..384bcfbc5
--- /dev/null
+++ b/lib/firmware/tron_tokens.h
@@ -0,0 +1,45 @@
+/*
+ * Hardcoded TRC-20 token definitions for on-device display.
+ * Only high-volume tokens are included; unknown tokens show as raw amounts.
+ */
+
+#ifndef KEEPKEY_FIRMWARE_TRON_TOKENS_H
+#define KEEPKEY_FIRMWARE_TRON_TOKENS_H
+
+#include
+#include
+
+typedef struct {
+ const char *address; // Base58Check contract address
+ const char *symbol; // Token ticker
+ uint8_t decimals; // Token decimal places
+} TronToken;
+
+static const TronToken tron_tokens[] = {
+ {"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "USDT", 6},
+ {"TPYmHEhy5n8TCEfYGqW2rPxsghSfzghPDn", "USDD", 18},
+ {"TSSMHYeV2uE9qYH95DqyoCuNCzEL1NvU3S", "SUN", 18},
+ {"TCFLL5dx5ZJdKnWuesXxi1VPwjLVmWZZy9", "JST", 18},
+ {"TAFjULxiVgT4qWk6UZwjqwZXTSaGaqnVp4", "BTT", 18},
+ {"TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7", "WIN", 6},
+ {"TXpw8XeWYeTUd4quDskoUqeQPowRh4jY65", "WBTC", 8},
+ {"THb4CqiFdwNHsWsQCs4JhzwjMWys4aqCbF", "ETH", 18},
+ {"TDPJwSHLPDqvDeX1JLkP2z1szHEhLLVBFq", "USD1", 6},
+ {"TUPM7K8REVzD2UdV4R5fe5M8XbnR2DdoJ6", "HTX", 18},
+ {"TUpMhErZL2fhh4sVNULAbNKLokS4GjC1F4", "TUSD", 18},
+ {"TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8", "USDC", 6},
+};
+
+#define TRON_TOKEN_COUNT (sizeof(tron_tokens) / sizeof(tron_tokens[0]))
+
+static inline const TronToken *tron_token_by_address(
+ const char *contract_address) {
+ for (size_t i = 0; i < TRON_TOKEN_COUNT; i++) {
+ if (strcmp(tron_tokens[i].address, contract_address) == 0) {
+ return &tron_tokens[i];
+ }
+ }
+ return NULL;
+}
+
+#endif
diff --git a/unittests/firmware/CMakeLists.txt b/unittests/firmware/CMakeLists.txt
index 221580e16..d07b07481 100644
--- a/unittests/firmware/CMakeLists.txt
+++ b/unittests/firmware/CMakeLists.txt
@@ -3,6 +3,7 @@ set(sources
recovery.cpp
storage.cpp
ton.cpp
+ tron.cpp
usb_rx.cpp
u2f.cpp)
diff --git a/unittests/firmware/tron.cpp b/unittests/firmware/tron.cpp
new file mode 100644
index 000000000..32a70ead6
--- /dev/null
+++ b/unittests/firmware/tron.cpp
@@ -0,0 +1,128 @@
+extern "C" {
+#include "keepkey/firmware/tron.h"
+#include "messages-tron.pb.h"
+#include
+}
+
+#include "gtest/gtest.h"
+
+/* ------------------------------------------------------------------ */
+/* Address encoding / decoding tests */
+/* ------------------------------------------------------------------ */
+
+TEST(Tron, DecodeValidAddress) {
+ // TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t (USDT contract)
+ uint8_t raw[TRON_ADDRESS_SIZE];
+ ASSERT_TRUE(tron_decodeAddress("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", raw));
+ EXPECT_EQ(raw[0], 0x41);
+}
+
+TEST(Tron, DecodeInvalidAddress) {
+ uint8_t raw[TRON_ADDRESS_SIZE];
+ // Too short
+ EXPECT_FALSE(tron_decodeAddress("T", raw));
+ // Bad checksum
+ EXPECT_FALSE(tron_decodeAddress("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6X", raw));
+ // NULL
+ EXPECT_FALSE(tron_decodeAddress(NULL, raw));
+}
+
+TEST(Tron, ValidateAddress) {
+ EXPECT_TRUE(tron_validateAddress("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"));
+ EXPECT_FALSE(tron_validateAddress("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6X"));
+ EXPECT_FALSE(tron_validateAddress(NULL));
+}
+
+/* ------------------------------------------------------------------ */
+/* TRC-20 ABI decoding tests */
+/* ------------------------------------------------------------------ */
+
+TEST(Tron, DecodeTRC20Transfer) {
+ // Real TRC-20 transfer(address,uint256) ABI encoding:
+ // selector: a9059cbb
+ // to: 000000000000000000000000 + 20-byte EVM address
+ // amount: 32-byte big-endian
+ uint8_t data[68];
+ memset(data, 0, sizeof(data));
+
+ // selector
+ data[0] = 0xa9; data[1] = 0x05; data[2] = 0x9c; data[3] = 0xbb;
+ // EVM address at bytes 16..35 (12 zero pad + 20 addr)
+ data[16] = 0xDE; data[17] = 0xAD; data[35] = 0xBE;
+ // Amount: 1000000 (0xF4240) at last 3 bytes
+ data[65] = 0x0F; data[66] = 0x42; data[67] = 0x40;
+
+ uint8_t to_raw[TRON_ADDRESS_SIZE];
+ uint8_t amount[32];
+ ASSERT_TRUE(tron_decodeTRC20Transfer(data, sizeof(data), to_raw, amount));
+
+ // Must prepend 0x41 TRON prefix
+ EXPECT_EQ(to_raw[0], 0x41);
+ EXPECT_EQ(to_raw[1], 0xDE);
+ EXPECT_EQ(to_raw[2], 0xAD);
+
+ // Amount check
+ EXPECT_EQ(amount[29], 0x0F);
+ EXPECT_EQ(amount[30], 0x42);
+ EXPECT_EQ(amount[31], 0x40);
+}
+
+TEST(Tron, DecodeTRC20WrongSelector) {
+ uint8_t data[68];
+ memset(data, 0, sizeof(data));
+ data[0] = 0x12; data[1] = 0x34; data[2] = 0x56; data[3] = 0x78;
+
+ uint8_t to_raw[TRON_ADDRESS_SIZE];
+ uint8_t amount[32];
+ EXPECT_FALSE(tron_decodeTRC20Transfer(data, sizeof(data), to_raw, amount));
+}
+
+TEST(Tron, DecodeTRC20TooShort) {
+ uint8_t data[60];
+ memset(data, 0, sizeof(data));
+ data[0] = 0xa9; data[1] = 0x05; data[2] = 0x9c; data[3] = 0xbb;
+
+ uint8_t to_raw[TRON_ADDRESS_SIZE];
+ uint8_t amount[32];
+ EXPECT_FALSE(tron_decodeTRC20Transfer(data, sizeof(data), to_raw, amount));
+}
+
+TEST(Tron, DecodeTRC20NonZeroPadding) {
+ // If the 12 leading pad bytes aren't all zero, reject
+ uint8_t data[68];
+ memset(data, 0, sizeof(data));
+ data[0] = 0xa9; data[1] = 0x05; data[2] = 0x9c; data[3] = 0xbb;
+ data[4] = 0x01; // non-zero in padding area
+
+ uint8_t to_raw[TRON_ADDRESS_SIZE];
+ uint8_t amount[32];
+ EXPECT_FALSE(tron_decodeTRC20Transfer(data, sizeof(data), to_raw, amount));
+}
+
+/* ------------------------------------------------------------------ */
+/* Formatting tests */
+/* ------------------------------------------------------------------ */
+
+TEST(Tron, FormatAmount) {
+ char buf[64];
+
+ tron_formatAmount(buf, sizeof(buf), 1000000); // 1 TRX
+ EXPECT_STREQ(buf, "1 TRX");
+
+ tron_formatAmount(buf, sizeof(buf), 500000); // 0.5 TRX
+ EXPECT_STREQ(buf, "0.5 TRX");
+
+ tron_formatAmount(buf, sizeof(buf), 0);
+ EXPECT_STREQ(buf, "0 TRX");
+}
+
+TEST(Tron, FormatTokenAmount) {
+ char buf[64];
+
+ // 1 USDT (6 decimals) = 1000000
+ uint8_t amount1[32];
+ memset(amount1, 0, 32);
+ amount1[29] = 0x0F; amount1[30] = 0x42; amount1[31] = 0x40; // 1000000
+ tron_formatTokenAmount(buf, sizeof(buf), amount1, 6, "USDT");
+ EXPECT_STREQ(buf, "1 USDT");
+}