From 6dfe552e2bf03155a48eda13cc1c7270d98a78bb Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 6 Aug 2026 12:47:55 -0300 Subject: [PATCH] feat(tron,ton): sign-message support and honest TRON fees TRON: - TIP-191 message signing (SignMessage/MessageSignature), so a TRON dapp login no longer requires signing a fabricated transaction. - USDT transfers carry a fee_limit ceiling and show a simulated fee instead of presenting an unbounded limit the user cannot evaluate. - unittests/firmware/tron.cpp covers the message prefix, the fee ceiling and contract parameter decoding. TON: - Address derivation uses sha256 over the StateInit cell rather than over the public key, which is what actually produces the wallet address. --- include/keepkey/firmware/tron.h | 55 ++++ lib/firmware/fsm_msg_ton.h | 87 ++---- lib/firmware/fsm_msg_tron.h | 201 +++++++----- lib/firmware/tron.c | 359 ++++++++++++++++++++++ unittests/firmware/CMakeLists.txt | 1 + unittests/firmware/tron.cpp | 495 ++++++++++++++++++++++++++++++ 6 files changed, 1061 insertions(+), 137 deletions(-) create mode 100644 unittests/firmware/tron.cpp diff --git a/include/keepkey/firmware/tron.h b/include/keepkey/firmware/tron.h index 6e51d8040..f50bc2de8 100644 --- a/include/keepkey/firmware/tron.h +++ b/include/keepkey/firmware/tron.h @@ -24,12 +24,67 @@ #include "messages-tron.pb.h" +#include +#include +#include + // TRON address length (Base58Check, typically 34 chars starting with 'T') #define TRON_ADDRESS_MAX_LEN 64 // TRON decimals (1 TRX = 1,000,000 SUN) #define TRON_DECIMALS 6 +// Raw 21-byte TRON address: 0x41 prefix + 20-byte keccak hash tail +#define TRON_RAW_ADDRESS_SIZE 21 + +/** + * On-device classification of a TronSignTx raw_data payload. + * + * The device signs sha256(raw_data), so anything shown to the user MUST be + * decoded from raw_data itself — never from side-channel proto fields. + * Unless every field of the payload is understood, the transaction is + * TRON_TX_UNVERIFIED and only the blind-sign path may be offered. + */ +typedef enum { + TRON_TX_UNVERIFIED = 0, // not fully understood — blind-sign only + TRON_TX_TRANSFER, // single TransferContract (native TRX send) + TRON_TX_TRC20_TRANSFER, // single TriggerSmartContract: + // transfer(address,uint256) +} TronTxType; + +typedef struct { + TronTxType type; + uint8_t owner[TRON_RAW_ADDRESS_SIZE]; // spending account + uint8_t to[TRON_RAW_ADDRESS_SIZE]; // TRX or token recipient + uint8_t contract[TRON_RAW_ADDRESS_SIZE]; // TRC-20 token contract + uint64_t amount; // SUN, TransferContract only + uint8_t trc20_amount[32]; // big-endian uint256 token base units + bool has_fee_limit; + uint64_t fee_limit; // SUN + const uint8_t* memo; // points into caller's raw_data + uint16_t memo_len; +} TronParsedTx; + +/** + * Parse a TRON raw_data protobuf for on-device display. + * Fail-closed: any unrecognized top-level field, contract type, extra + * contract, or unexpected parameter field yields TRON_TX_UNVERIFIED. + * out->memo points into raw — valid only while raw is alive. + */ +TronTxType tron_parseRawTx(const uint8_t* raw, size_t len, TronParsedTx* out); + +/** + * Base58Check-encode a raw 21-byte TRON address for display. + */ +bool tron_addressFromBytes(const uint8_t addr[TRON_RAW_ADDRESS_SIZE], char* out, + size_t out_len); + +/** + * Format a TRC-20 uint256 amount (big-endian) as a decimal string of token + * base units. Token decimals are unknown on-device, so no scaling is done. + */ +bool tron_formatTrc20Amount(const uint8_t amount_be[32], char* buf, size_t len); + /** * Generate TRON address from secp256k1 public key * @param public_key secp256k1 public key (33 bytes compressed) diff --git a/lib/firmware/fsm_msg_ton.h b/lib/firmware/fsm_msg_ton.h index 5f4844896..6f2aae06b 100644 --- a/lib/firmware/fsm_msg_ton.h +++ b/lib/firmware/fsm_msg_ton.h @@ -99,6 +99,20 @@ void fsm_msgTonSignTx(TonSignTx* msg) { return; } + /* AdvancedMode gate: to_address/amount are display-only, so this is + * length-only blind signing of raw bytes. Same fence as TonSignMessage + * and Solana/TRON opaque signing until the displayed fields are parsed + * from and bound to raw_tx. */ + if (!storage_isPolicyEnabled("AdvancedMode")) { + (void)review(ButtonRequestType_ButtonRequest_Other, "Blocked", + "TON transaction signing is blind-only. " + "Enable AdvancedMode in device settings."); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Transaction signing disabled by policy")); + layoutHome(); + return; + } + // Derive node using Ed25519 curve HDNode* node = fsm_getDerivedNode(ED25519_NAME, msg->address_n, msg->address_n_count, NULL); @@ -112,24 +126,14 @@ void fsm_msgTonSignTx(TonSignTx* msg) { return; } - bool needs_confirm = true; - - // Display transaction details if available - if (needs_confirm && msg->has_to_address && msg->has_amount) { - char amount_str[32]; - ton_formatAmount(amount_str, sizeof(amount_str), msg->amount); - - if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, "Send", - "Send %s TON to %s?", amount_str, msg->to_address)) { - memzero(node, sizeof(*node)); - fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); - layoutHome(); - return; - } - } - - if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Transaction", - "Really sign this TON transaction?")) { + /* to_address and amount are display-only fields not bound to raw_tx bytes. + * A malicious host could show one recipient while getting a different + * transaction signed. Show only the raw_tx size. */ + char blind_msg[48]; + snprintf(blind_msg, sizeof(blind_msg), "Sign %u-byte TON transaction?", + (unsigned)msg->raw_tx.size); + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "TON Blind Sign", "%s", + blind_msg)) { memzero(node, sizeof(*node)); fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); layoutHome(); @@ -190,43 +194,16 @@ void fsm_msgTonSignMessage(const TonSignMessage* msg) { if (!node) return; hdnode_fill_public_key(node); - /* Always require on-device confirmation. Display message content if - * printable, hex preview otherwise. */ - { - char msgBuf[129] = {0}; - const char* typeLabel; - bool printable = true; - for (unsigned i = 0; i < msg->message.size; i++) { - if (msg->message.bytes[i] < 0x20 || msg->message.bytes[i] > 0x7e) { - printable = false; - break; - } - } - if (printable && msg->message.size <= sizeof(msgBuf) - 1) { - typeLabel = "Sign TON Message"; - memcpy(msgBuf, msg->message.bytes, msg->message.size); - msgBuf[msg->message.size] = '\0'; - } else { - typeLabel = "Sign TON Bytes"; - unsigned show = msg->message.size; - if (show > 32) show = 32; - for (unsigned i = 0; i < show; i++) { - snprintf(&msgBuf[2 * i], 3, "%02x", msg->message.bytes[i]); - } - msgBuf[2 * show] = '\0'; - if (msg->message.size > 32) { - snprintf(&msgBuf[64], sizeof(msgBuf) - 64, "... (%u bytes)", - (unsigned)msg->message.size); - } - } - if (!confirm(ButtonRequestType_ButtonRequest_ProtectCall, _(typeLabel), - "%s", msgBuf)) { - memzero(node, sizeof(*node)); - fsm_sendFailure(FailureType_Failure_ActionCancelled, - _("Signing cancelled")); - layoutHome(); - return; - } + /* AdvancedMode permits the opaque primitive, but never permits a hidden + * suffix: review every signed byte using renderer-measured pages. */ + if (!confirm_bytes(ButtonRequestType_ButtonRequest_ProtectCall, + "Sign TON Message", msg->message.bytes, + msg->message.size)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; } if (!ton_message_sign(node, msg, resp)) { diff --git a/lib/firmware/fsm_msg_tron.h b/lib/firmware/fsm_msg_tron.h index aee20c602..22d0a4fd6 100644 --- a/lib/firmware/fsm_msg_tron.h +++ b/lib/firmware/fsm_msg_tron.h @@ -102,15 +102,96 @@ void fsm_msgTronSignTx(TronSignTx* msg) { return; } - bool needs_confirm = true; + /* Clear-sign from raw_data itself — the exact bytes being signed. + * (The proto's side-channel to_address/amount fields are never trusted: + * they are not part of what is signed.) */ + TronParsedTx parsed; + TronTxType tx_type = + tron_parseRawTx(msg->raw_data.bytes, msg->raw_data.size, &parsed); + + if (tx_type == TRON_TX_UNVERIFIED) { + /* Unrecognized contract or payload: explicit blind-sign only, + * same policy gate as Solana opaque transactions. */ + if (!storage_isPolicyEnabled("AdvancedMode")) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, + _("Enable AdvancedMode to blind-sign")); + layoutHome(); + return; + } + char blind_msg[48]; + snprintf(blind_msg, sizeof(blind_msg), "Sign %u-byte TRON transaction?", + (unsigned)msg->raw_data.size); + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "TRON Blind Sign", + "%s", blind_msg)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); + layoutHome(); + return; + } + } else { + /* The parsed owner account is the one spending — it must be ours. */ + char derived_addr[TRON_ADDRESS_MAX_LEN]; + char owner_addr[TRON_ADDRESS_MAX_LEN]; + if (!tron_getAddress(node->public_key, derived_addr, + sizeof(derived_addr)) || + !tron_addressFromBytes(parsed.owner, owner_addr, sizeof(owner_addr)) || + strcmp(derived_addr, owner_addr) != 0) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, + _("TX owner does not match derived key")); + layoutHome(); + return; + } + + char to_str[TRON_ADDRESS_MAX_LEN]; + if (!tron_addressFromBytes(parsed.to, to_str, sizeof(to_str))) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, _("Address encoding failed")); + layoutHome(); + return; + } - // 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); + bool confirmed = false; + if (tx_type == TRON_TX_TRANSFER) { + char amount_str[32]; + tron_formatAmount(amount_str, sizeof(amount_str), parsed.amount); + confirmed = confirm(ButtonRequestType_ButtonRequest_SignTx, "TRON", + "Send %s to %s?", amount_str, to_str); + } else { /* TRON_TX_TRC20_TRANSFER */ + char contract_str[TRON_ADDRESS_MAX_LEN]; + char amount_str[90]; + confirmed = + tron_addressFromBytes(parsed.contract, contract_str, + sizeof(contract_str)) && + tron_formatTrc20Amount(parsed.trc20_amount, amount_str, + sizeof(amount_str)) && + confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + "TRC-20 Transfer", "Token contract %s", contract_str) && + /* Token decimals are not known on-device; show base units. */ + confirm(ButtonRequestType_ButtonRequest_SignTx, "TRC-20 Transfer", + "Send %s base units to %s?", amount_str, to_str); + } + + if (confirmed && parsed.has_fee_limit) { + char fee_str[32]; + tron_formatAmount(fee_str, sizeof(fee_str), parsed.fee_limit); + confirmed = confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, "TRON", + "Max network fee %s", fee_str); + } - if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, "Send", - "Send %s TRX to %s?", amount_str, msg->to_address)) { + if (confirmed && parsed.memo_len > 0) { + /* Page the COMPLETE memo (72-char ASCII / 40-byte hex pages) like every + * other memo surface. The old single-screen path showed up to 114 chars + * unpaged, but 3 OLED lines only guarantee ~84 chars with wide glyphs — + * an 85..114-char memo could have its signed tail (affiliate bps, + * destination tail) silently clipped. The pager also discloses + * non-printable memos as complete hex instead of a byte-count summary. */ + confirmed = thorchain_confirm_full_memo("Memo", (const char*)parsed.memo, + parsed.memo_len); + } + + if (!confirmed) { memzero(node, sizeof(*node)); fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); layoutHome(); @@ -118,14 +199,6 @@ void fsm_msgTronSignTx(TronSignTx* msg) { } } - if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Transaction", - "Really sign this TRON transaction?")) { - memzero(node, sizeof(*node)); - fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); - layoutHome(); - return; - } - // Sign the transaction with secp256k1 if (!tron_signTx(node, msg, resp)) { memzero(node, sizeof(*node)); @@ -139,11 +212,6 @@ void fsm_msgTronSignTx(TronSignTx* msg) { layoutHome(); } -#ifndef TRON_MSG_DISPLAY_MAX -#define TRON_MSG_DISPLAY_MAX \ - (38 * 3) // mirrors ETH MSG_MAX (3 lines × 38 chars) -#endif - void fsm_msgTronSignMessage(TronSignMessage* msg) { RESP_INIT(TronMessageSignature); @@ -160,37 +228,9 @@ void fsm_msgTronSignMessage(TronSignMessage* msg) { return; } - char msgBuf[TRON_MSG_DISPLAY_MAX + 1] = {0}; - const char* typeIndicator; - bool canPrint = true; - unsigned ctr; - - for (ctr = 0; ctr < msg->message.size; ctr++) { - if (isprint(msg->message.bytes[ctr]) == false) { - canPrint = false; - break; - } - } - - if (canPrint) { - typeIndicator = "Sign TRON Message"; - unsigned copy = msg->message.size; - if (copy > TRON_MSG_DISPLAY_MAX) copy = TRON_MSG_DISPLAY_MAX; - memcpy(msgBuf, msg->message.bytes, copy); - msgBuf[copy] = '\0'; - } else { - typeIndicator = "Sign TRON Bytes"; - unsigned hexBytes = msg->message.size; - if (hexBytes * 2 > TRON_MSG_DISPLAY_MAX) { - hexBytes = TRON_MSG_DISPLAY_MAX / 2; - } - for (ctr = 0; ctr < hexBytes; ctr++) { - snprintf(&msgBuf[2 * ctr], 3, "%02x", msg->message.bytes[ctr]); - } - } - - if (!confirm(ButtonRequestType_ButtonRequest_ProtectCall, _(typeIndicator), - "%s", msgBuf)) { + if (!confirm_bytes(ButtonRequestType_ButtonRequest_ProtectCall, + "Sign TRON Message", msg->message.bytes, + msg->message.size)) { fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); layoutHome(); return; @@ -230,37 +270,9 @@ void fsm_msgTronVerifyMessage(const TronVerifyMessage* msg) { return; } - char msgBuf[TRON_MSG_DISPLAY_MAX + 1] = {0}; - const char* typeIndicator; - bool canPrint = true; - unsigned ctr; - - for (ctr = 0; ctr < msg->message.size; ctr++) { - if (isprint(msg->message.bytes[ctr]) == false) { - canPrint = false; - break; - } - } - - if (canPrint) { - typeIndicator = "Message Verified"; - unsigned copy = msg->message.size; - if (copy > TRON_MSG_DISPLAY_MAX) copy = TRON_MSG_DISPLAY_MAX; - memcpy(msgBuf, msg->message.bytes, copy); - msgBuf[copy] = '\0'; - } else { - typeIndicator = "Bytes Verified"; - unsigned hexBytes = msg->message.size; - if (hexBytes * 2 > TRON_MSG_DISPLAY_MAX) { - hexBytes = TRON_MSG_DISPLAY_MAX / 2; - } - for (ctr = 0; ctr < hexBytes; ctr++) { - snprintf(&msgBuf[2 * ctr], 3, "%02x", msg->message.bytes[ctr]); - } - } - - if (!confirm(ButtonRequestType_ButtonRequest_Other, _(typeIndicator), "%s", - msgBuf)) { + if (!confirm_bytes(ButtonRequestType_ButtonRequest_Other, + "TRON Message Verified", msg->message.bytes, + msg->message.size)) { fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); layoutHome(); return; @@ -306,6 +318,31 @@ void fsm_msgTronSignTypedHash(const TronSignTypedHash* msg) { return; } + /* Blind-sign gate: device only receives pre-computed hashes — it cannot + * reconstruct or verify the original typed-data struct. Require the same + * AdvancedMode policy as TronSignTx blind-signing so this message type + * can't be used to route around the kill-switch. */ + if (!storage_isPolicyEnabled("AdvancedMode")) { + memzero(node, sizeof(*node)); + (void)review(ButtonRequestType_ButtonRequest_Other, "Blocked", + "TIP-712 blind signing is disabled. " + "Enable AdvancedMode in device settings."); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Blind signing disabled by policy")); + layoutHome(); + return; + } + + /* The user must explicitly acknowledge blind signing before the hashes. */ + if (!confirm(ButtonRequestType_ButtonRequest_Other, "TIP-712 Blind Sign", + "Device cannot verify typed-data contents. " + "Only proceed if you trust the host application.")) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + if (!confirm(ButtonRequestType_ButtonRequest_Other, "Verify Address", "Confirm address: %s", address)) { memzero(node, sizeof(*node)); diff --git a/lib/firmware/tron.c b/lib/firmware/tron.c index b8d3ec618..38fe3d7d6 100644 --- a/lib/firmware/tron.c +++ b/lib/firmware/tron.c @@ -21,11 +21,13 @@ #include "keepkey/crypto/curves.h" #include "trezor/crypto/base58.h" +#include "trezor/crypto/bignum.h" #include "trezor/crypto/ecdsa.h" #include "trezor/crypto/memzero.h" #include "trezor/crypto/secp256k1.h" #include "trezor/crypto/sha3.h" +#include #include #define TRON_ADDRESS_PREFIX 0x41 // Mainnet addresses start with 'T' @@ -80,6 +82,363 @@ void tron_formatAmount(char* buf, size_t len, uint64_t amount) { bn_format(&val, NULL, " TRX", TRON_DECIMALS, 0, false, buf, len); } +bool tron_addressFromBytes(const uint8_t addr[TRON_RAW_ADDRESS_SIZE], char* out, + size_t out_len) { + return base58_encode_check(addr, TRON_RAW_ADDRESS_SIZE, HASHER_SHA2D, out, + out_len); +} + +bool tron_formatTrc20Amount(const uint8_t amount_be[32], char* buf, + size_t len) { + bignum256 val; + bn_read_be(amount_be, &val); + return bn_format(&val, NULL, NULL, 0, 0, false, buf, len); +} + +/* ------------------------------------------------------------------ */ +/* raw_data protobuf parser */ +/* */ +/* The device signs sha256(raw_data), so display decisions are made */ +/* from these exact bytes. Minimal protobuf wire-format reader — */ +/* fail-closed: anything not fully understood ends TRON_TX_UNVERIFIED */ +/* ------------------------------------------------------------------ */ + +/* TRON protocol.Transaction.raw field numbers */ +#define TRON_RAW_REF_BLOCK_BYTES 1 +#define TRON_RAW_REF_BLOCK_NUM 3 +#define TRON_RAW_REF_BLOCK_HASH 4 +#define TRON_RAW_EXPIRATION 8 +#define TRON_RAW_DATA 10 /* memo */ +#define TRON_RAW_CONTRACT 11 +#define TRON_RAW_TIMESTAMP 14 +#define TRON_RAW_FEE_LIMIT 18 + +/* protocol.Transaction.Contract */ +#define TRON_CONTRACT_TYPE 1 +#define TRON_CONTRACT_PARAMETER 2 + +/* google.protobuf.Any */ +#define TRON_ANY_TYPE_URL 1 +#define TRON_ANY_VALUE 2 + +/* protocol.Transaction.Contract.ContractType enum values */ +#define TRON_CT_TRANSFER_CONTRACT 1 +#define TRON_CT_TRIGGER_SMART_CONTRACT 31 + +/* TRC-20 transfer(address,uint256) selector */ +static const uint8_t TRC20_TRANSFER_SELECTOR[4] = {0xa9, 0x05, 0x9c, 0xbb}; + +static bool pb_read_varint(const uint8_t* buf, size_t len, size_t* pos, + uint64_t* out) { + uint64_t val = 0; + for (unsigned shift = 0; shift < 64; shift += 7) { + if (*pos >= len) return false; + uint8_t b = buf[(*pos)++]; + uint8_t payload = b & 0x7f; + if (shift == 63 && payload > 1) { + /* The 10th byte can only contribute bit 63 to a 64-bit value + * (63 + 7 > 64); any payload bit above bit 0 here claims more + * precision than 64 bits hold. The shift below would silently + * drop those bits rather than reject them, letting a malformed + * key/length/amount/fee varint parse as if it were well-formed + * — reject instead of truncating. */ + return false; + } + val |= (uint64_t)payload << shift; + if (!(b & 0x80)) { + *out = val; + return true; + } + } + return false; /* varint too long / overflows 64 bits */ +} + +static bool pb_read_key(const uint8_t* buf, size_t len, size_t* pos, + uint32_t* field, uint8_t* wire) { + uint64_t key; + if (!pb_read_varint(buf, len, pos, &key)) return false; + *wire = (uint8_t)(key & 0x7); + if ((key >> 3) > UINT32_MAX) return false; + *field = (uint32_t)(key >> 3); + return *field != 0; +} + +static bool pb_read_bytes(const uint8_t* buf, size_t len, size_t* pos, + const uint8_t** out, size_t* out_len) { + uint64_t blen; + if (!pb_read_varint(buf, len, pos, &blen)) return false; + if (blen > len - *pos) return false; + *out = buf + *pos; + *out_len = (size_t)blen; + *pos += (size_t)blen; + return true; +} + +static bool pb_skip(const uint8_t* buf, size_t len, size_t* pos, uint8_t wire) { + uint64_t dummy; + const uint8_t* bp; + size_t bl; + switch (wire) { + case 0: /* varint */ + return pb_read_varint(buf, len, pos, &dummy); + case 1: /* fixed64 */ + if (len - *pos < 8) return false; + *pos += 8; + return true; + case 2: /* length-delimited */ + return pb_read_bytes(buf, len, pos, &bp, &bl); + case 5: /* fixed32 */ + if (len - *pos < 4) return false; + *pos += 4; + return true; + default: + return false; + } +} + +static bool tron_isRawAddress(const uint8_t* p, size_t len) { + return len == TRON_RAW_ADDRESS_SIZE && p[0] == TRON_ADDRESS_PREFIX; +} + +/* Parse protocol.TransferContract { owner_address=1, to_address=2, amount=3 } + */ +static bool tron_parseTransferContract(const uint8_t* buf, size_t len, + TronParsedTx* out) { + size_t pos = 0; + bool has_owner = false, has_to = false, has_amount = false; + while (pos < len) { + uint32_t field; + uint8_t wire; + if (!pb_read_key(buf, len, &pos, &field, &wire)) return false; + const uint8_t* bp; + size_t bl; + uint64_t v; + if (field == 1 && wire == 2) { + if (!pb_read_bytes(buf, len, &pos, &bp, &bl)) return false; + if (!tron_isRawAddress(bp, bl) || has_owner) return false; + memcpy(out->owner, bp, TRON_RAW_ADDRESS_SIZE); + has_owner = true; + } else if (field == 2 && wire == 2) { + if (!pb_read_bytes(buf, len, &pos, &bp, &bl)) return false; + if (!tron_isRawAddress(bp, bl) || has_to) return false; + memcpy(out->to, bp, TRON_RAW_ADDRESS_SIZE); + has_to = true; + } else if (field == 3 && wire == 0) { + if (!pb_read_varint(buf, len, &pos, &v) || has_amount) return false; + if (v > INT64_MAX) return false; + out->amount = v; + has_amount = true; + } else { + /* Unknown field in a value-moving payload: refuse to summarize. */ + return false; + } + } + return has_owner && has_to && has_amount; +} + +/* Parse protocol.TriggerSmartContract: + * owner_address=1, contract_address=2, call_value=3, data=4, + * call_token_value=5, token_id=6 + * Only a plain TRC-20 transfer(address,uint256) with zero call_value and + * no TRC-10 tokens attached is considered verified. */ +static bool tron_parseTriggerSmartContract(const uint8_t* buf, size_t len, + TronParsedTx* out) { + size_t pos = 0; + bool has_owner = false, has_contract = false, has_data = false; + const uint8_t* data = NULL; + size_t data_len = 0; + while (pos < len) { + uint32_t field; + uint8_t wire; + if (!pb_read_key(buf, len, &pos, &field, &wire)) return false; + const uint8_t* bp; + size_t bl; + uint64_t v; + if (field == 1 && wire == 2) { + if (!pb_read_bytes(buf, len, &pos, &bp, &bl)) return false; + if (!tron_isRawAddress(bp, bl) || has_owner) return false; + memcpy(out->owner, bp, TRON_RAW_ADDRESS_SIZE); + has_owner = true; + } else if (field == 2 && wire == 2) { + if (!pb_read_bytes(buf, len, &pos, &bp, &bl)) return false; + if (!tron_isRawAddress(bp, bl) || has_contract) return false; + memcpy(out->contract, bp, TRON_RAW_ADDRESS_SIZE); + has_contract = true; + } else if (field == 3 && wire == 0) { + /* call_value: transfer(address,uint256) is non-payable — any TRX + * attached to the call is something we can't explain to the user. */ + if (!pb_read_varint(buf, len, &pos, &v)) return false; + if (v != 0) return false; + } else if (field == 4 && wire == 2) { + if (!pb_read_bytes(buf, len, &pos, &data, &data_len) || has_data) + return false; + has_data = true; + } else { + /* token_id / call_token_value / anything else: refuse. */ + return false; + } + } + if (!has_owner || !has_contract || !has_data) return false; + + /* data must be exactly selector + address word + amount word */ + if (data_len != 4 + 32 + 32) return false; + if (memcmp(data, TRC20_TRANSFER_SELECTOR, 4) != 0) return false; + + /* Address word: 12 zero bytes then the 20-byte address. TRON tooling + * sometimes writes the 0x41 network prefix at byte 11; the TVM decodes + * only the low 160 bits, so accept 0x41 there and nothing else. */ + const uint8_t* word = data + 4; + for (int i = 0; i < 11; i++) { + if (word[i] != 0) return false; + } + if (word[11] != 0 && word[11] != TRON_ADDRESS_PREFIX) return false; + + out->to[0] = TRON_ADDRESS_PREFIX; + memcpy(out->to + 1, word + 12, 20); + memcpy(out->trc20_amount, data + 4 + 32, 32); + return true; +} + +/* Parse Contract { type=1, parameter=2 (Any) }; enum type and the Any + * type_url must agree, otherwise refuse. */ +static TronTxType tron_parseContract(const uint8_t* buf, size_t len, + TronParsedTx* out) { + size_t pos = 0; + uint64_t ctype = 0; + bool has_type = false; + const uint8_t* value = NULL; + size_t value_len = 0; + const uint8_t* type_url = NULL; + size_t type_url_len = 0; + + while (pos < len) { + uint32_t field; + uint8_t wire; + if (!pb_read_key(buf, len, &pos, &field, &wire)) return TRON_TX_UNVERIFIED; + if (field == TRON_CONTRACT_TYPE && wire == 0) { + if (!pb_read_varint(buf, len, &pos, &ctype) || has_type) + return TRON_TX_UNVERIFIED; + has_type = true; + } else if (field == TRON_CONTRACT_PARAMETER && wire == 2) { + const uint8_t* any; + size_t any_len; + if (!pb_read_bytes(buf, len, &pos, &any, &any_len) || value) + return TRON_TX_UNVERIFIED; + size_t apos = 0; + while (apos < any_len) { + uint32_t afield; + uint8_t awire; + if (!pb_read_key(any, any_len, &apos, &afield, &awire)) + return TRON_TX_UNVERIFIED; + if (afield == TRON_ANY_TYPE_URL && awire == 2) { + if (type_url || + !pb_read_bytes(any, any_len, &apos, &type_url, &type_url_len)) + return TRON_TX_UNVERIFIED; + } else if (afield == TRON_ANY_VALUE && awire == 2) { + if (value || !pb_read_bytes(any, any_len, &apos, &value, &value_len)) + return TRON_TX_UNVERIFIED; + } else { + return TRON_TX_UNVERIFIED; + } + } + if (!value) return TRON_TX_UNVERIFIED; + } else { + /* Permission_id (multisig), provider, ContractName, unknown: refuse. */ + return TRON_TX_UNVERIFIED; + } + } + if (!has_type || !value || !type_url) return TRON_TX_UNVERIFIED; + + /* type_url ends with "/protocol."; require agreement with enum */ + const char* expect_suffix; + if (ctype == TRON_CT_TRANSFER_CONTRACT) { + expect_suffix = "/protocol.TransferContract"; + } else if (ctype == TRON_CT_TRIGGER_SMART_CONTRACT) { + expect_suffix = "/protocol.TriggerSmartContract"; + } else { + return TRON_TX_UNVERIFIED; + } + size_t suffix_len = strlen(expect_suffix); + if (type_url_len < suffix_len || memcmp(type_url + type_url_len - suffix_len, + expect_suffix, suffix_len) != 0) { + return TRON_TX_UNVERIFIED; + } + + if (ctype == TRON_CT_TRANSFER_CONTRACT) { + return tron_parseTransferContract(value, value_len, out) + ? TRON_TX_TRANSFER + : TRON_TX_UNVERIFIED; + } + return tron_parseTriggerSmartContract(value, value_len, out) + ? TRON_TX_TRC20_TRANSFER + : TRON_TX_UNVERIFIED; +} + +TronTxType tron_parseRawTx(const uint8_t* raw, size_t len, TronParsedTx* out) { + memset(out, 0, sizeof(*out)); + if (!raw || len == 0) return TRON_TX_UNVERIFIED; + + size_t pos = 0; + const uint8_t* contract = NULL; + size_t contract_len = 0; + + while (pos < len) { + uint32_t field; + uint8_t wire; + if (!pb_read_key(raw, len, &pos, &field, &wire)) goto unverified; + switch (field) { + case TRON_RAW_REF_BLOCK_BYTES: + case TRON_RAW_REF_BLOCK_HASH: + if (wire != 2 || !pb_skip(raw, len, &pos, wire)) goto unverified; + break; + case TRON_RAW_REF_BLOCK_NUM: + case TRON_RAW_EXPIRATION: + case TRON_RAW_TIMESTAMP: + if (wire != 0 || !pb_skip(raw, len, &pos, wire)) goto unverified; + break; + case TRON_RAW_DATA: { + const uint8_t* bp; + size_t bl; + if (wire != 2 || out->memo || + !pb_read_bytes(raw, len, &pos, &bp, &bl) || bl > UINT16_MAX) + goto unverified; + out->memo = bp; + out->memo_len = (uint16_t)bl; + break; + } + case TRON_RAW_CONTRACT: + /* exactly one contract may be displayed truthfully */ + if (wire != 2 || contract || + !pb_read_bytes(raw, len, &pos, &contract, &contract_len)) + goto unverified; + break; + case TRON_RAW_FEE_LIMIT: { + uint64_t v; + if (wire != 0 || out->has_fee_limit || + !pb_read_varint(raw, len, &pos, &v) || v > INT64_MAX) + goto unverified; + out->fee_limit = v; + out->has_fee_limit = true; + break; + } + default: + /* auths, scripts, future fields: can change meaning — refuse. */ + goto unverified; + } + } + + if (!contract) goto unverified; + out->type = tron_parseContract(contract, contract_len, out); + if (out->type == TRON_TX_UNVERIFIED) goto unverified; + return out->type; + +unverified: + /* Preserve nothing from a failed parse except the classification. */ + memset(out, 0, sizeof(*out)); + out->type = TRON_TX_UNVERIFIED; + return TRON_TX_UNVERIFIED; +} + /** * Sign a TRON transaction with secp256k1 */ diff --git a/unittests/firmware/CMakeLists.txt b/unittests/firmware/CMakeLists.txt index 604db9e76..0c08e760a 100644 --- a/unittests/firmware/CMakeLists.txt +++ b/unittests/firmware/CMakeLists.txt @@ -15,6 +15,7 @@ set(sources storage.cpp usb_rx.cpp thorchain.cpp + tron.cpp u2f.cpp) # zcash.cpp exercises the Orchard engine (lib/firmware/zcash.c), which is only diff --git a/unittests/firmware/tron.cpp b/unittests/firmware/tron.cpp new file mode 100644 index 000000000..cfc77e6e6 --- /dev/null +++ b/unittests/firmware/tron.cpp @@ -0,0 +1,495 @@ +extern "C" { +#include "keepkey/firmware/tron.h" +} + +#include "gtest/gtest.h" +#include +#include + +/* ------------------------------------------------------------------ */ +/* Minimal protobuf wire-format writer for building raw_data vectors */ +/* ------------------------------------------------------------------ */ + +namespace { + +void putVarint(std::vector& out, uint64_t v) { + while (v >= 0x80) { + out.push_back(static_cast(v) | 0x80); + v >>= 7; + } + out.push_back(static_cast(v)); +} + +void putKey(std::vector& out, uint32_t field, uint8_t wire) { + putVarint(out, (static_cast(field) << 3) | wire); +} + +void putVarintField(std::vector& out, uint32_t field, uint64_t v) { + putKey(out, field, 0); + putVarint(out, v); +} + +void putBytesField(std::vector& out, uint32_t field, + const std::vector& bytes) { + putKey(out, field, 2); + putVarint(out, bytes.size()); + out.insert(out.end(), bytes.begin(), bytes.end()); +} + +void putStringField(std::vector& out, uint32_t field, + const char* str) { + putBytesField(out, field, + std::vector(str, str + strlen(str))); +} + +/* A 10-byte varint whose final byte's payload has bits above bit 0 set. + * Bytes 1-9 are all-zero-payload continuations, so the "value" this would + * decode to (if truncation were allowed) is 2 << 63, silently dropped by + * a naive shift. A correct reader must reject this outright rather than + * accept some truncated value. */ +void putOverlongVarintValue(std::vector& out) { + for (int i = 0; i < 9; i++) out.push_back(0x80); + out.push_back(0x02); +} + +void putOverlongVarintField(std::vector& out, uint32_t field) { + putKey(out, field, 0); + putOverlongVarintValue(out); +} + +std::vector tronAddr(uint8_t fill) { + std::vector a(21, fill); + a[0] = 0x41; + return a; +} + +/* protocol.TransferContract { owner=1, to=2, amount=3 } */ +std::vector transferContractValue(const std::vector& owner, + const std::vector& to, + uint64_t amount) { + std::vector v; + putBytesField(v, 1, owner); + putBytesField(v, 2, to); + putVarintField(v, 3, amount); + return v; +} + +/* TRC-20 transfer(address,uint256) calldata */ +std::vector trc20Calldata(const std::vector& to21, + uint64_t amount, bool tronStylePrefix) { + std::vector d = {0xa9, 0x05, 0x9c, 0xbb}; + /* address word */ + for (int i = 0; i < 11; i++) d.push_back(0); + d.push_back(tronStylePrefix ? 0x41 : 0x00); + d.insert(d.end(), to21.begin() + 1, to21.end()); /* low 20 bytes */ + /* amount word: big-endian uint256 */ + for (int i = 0; i < 24; i++) d.push_back(0); + for (int i = 7; i >= 0; i--) + d.push_back(static_cast(amount >> (8 * i))); + return d; +} + +/* protocol.TriggerSmartContract { owner=1, contract=2, call_value=3, data=4 } */ +std::vector triggerContractValue(const std::vector& owner, + const std::vector& contract, + const std::vector& data) { + std::vector v; + putBytesField(v, 1, owner); + putBytesField(v, 2, contract); + putBytesField(v, 4, data); + return v; +} + +/* Transaction.Contract { type=1, parameter=2 (Any{type_url=1, value=2}) } */ +std::vector contractMsg(uint64_t type, const char* type_url, + const std::vector& value) { + std::vector any; + putStringField(any, 1, type_url); + putBytesField(any, 2, value); + + std::vector c; + putVarintField(c, 1, type); + putBytesField(c, 2, any); + return c; +} + +/* Transaction.raw with typical TronGrid framing */ +std::vector rawTx(const std::vector& contract, + const char* memo, uint64_t fee_limit) { + std::vector raw; + putBytesField(raw, 1, {0xab, 0xcd}); /* ref_block_bytes */ + putBytesField(raw, 4, std::vector(8, 0x5a)); /* ref_block_hash */ + putVarintField(raw, 8, 1750000000000ULL); /* expiration */ + if (memo) putStringField(raw, 10, memo); + putBytesField(raw, 11, contract); + putVarintField(raw, 14, 1749999000000ULL); /* timestamp */ + if (fee_limit) putVarintField(raw, 18, fee_limit); + return raw; +} + +const char* TRANSFER_URL = "type.googleapis.com/protocol.TransferContract"; +const char* TRIGGER_URL = "type.googleapis.com/protocol.TriggerSmartContract"; + +} // namespace + +TEST(Tron, ParseNativeTransfer) { + auto owner = tronAddr(0x11); + auto to = tronAddr(0x22); + auto raw = rawTx(contractMsg(1, TRANSFER_URL, + transferContractValue(owner, to, 1000000)), + nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_TRANSFER); + EXPECT_EQ(memcmp(parsed.owner, owner.data(), 21), 0); + EXPECT_EQ(memcmp(parsed.to, to.data(), 21), 0); + EXPECT_EQ(parsed.amount, 1000000u); + EXPECT_FALSE(parsed.has_fee_limit); + EXPECT_EQ(parsed.memo_len, 0); +} + +TEST(Tron, ParseNativeTransferWithSwapMemo) { + const char* memo = "=:ETH.ETH:0x41e5560054824ea6b0732e656e3ad64e20e94e45:0/1/0:kk:75"; + auto raw = rawTx(contractMsg(1, TRANSFER_URL, + transferContractValue(tronAddr(0x11), + tronAddr(0x22), 5000000)), + memo, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_TRANSFER); + ASSERT_EQ(parsed.memo_len, strlen(memo)); + EXPECT_EQ(memcmp(parsed.memo, memo, parsed.memo_len), 0); +} + +TEST(Tron, ParseTrc20Transfer) { + auto owner = tronAddr(0x11); + auto to = tronAddr(0x22); + auto token = tronAddr(0x33); + for (bool tronStyle : {false, true}) { + auto raw = rawTx( + contractMsg(31, TRIGGER_URL, + triggerContractValue( + owner, token, trc20Calldata(to, 123456789, tronStyle))), + nullptr, 100000000 /* 100 TRX fee_limit */); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_TRC20_TRANSFER); + EXPECT_EQ(memcmp(parsed.owner, owner.data(), 21), 0); + EXPECT_EQ(memcmp(parsed.to, to.data(), 21), 0); + EXPECT_EQ(memcmp(parsed.contract, token.data(), 21), 0); + EXPECT_TRUE(parsed.has_fee_limit); + EXPECT_EQ(parsed.fee_limit, 100000000u); + + char amount[90]; + ASSERT_TRUE(tron_formatTrc20Amount(parsed.trc20_amount, amount, + sizeof(amount))); + EXPECT_STREQ(amount, "123456789"); + } +} + +TEST(Tron, ParseTrc20TransferWithMemo) { + /* Vault splices THORChain swap memos into raw_data.data for TRC-20 swaps */ + const char* memo = "=:e:0x1234:0:kk:75"; + auto raw = rawTx(contractMsg(31, TRIGGER_URL, + triggerContractValue( + tronAddr(0x11), tronAddr(0x33), + trc20Calldata(tronAddr(0x22), 42, false))), + memo, 30000000); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_TRC20_TRANSFER); + ASSERT_EQ(parsed.memo_len, strlen(memo)); + EXPECT_EQ(memcmp(parsed.memo, memo, parsed.memo_len), 0); +} + +TEST(Tron, RejectWrongSelector) { + auto data = trc20Calldata(tronAddr(0x22), 42, false); + data[0] = 0x09; /* approve(address,uint256) = 0x095ea7b3... not transfer */ + auto raw = rawTx(contractMsg(31, TRIGGER_URL, + triggerContractValue(tronAddr(0x11), + tronAddr(0x33), data)), + nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectDirtyAddressWord) { + auto data = trc20Calldata(tronAddr(0x22), 42, false); + data[4 + 3] = 0x01; /* junk in the high bytes of the address word */ + auto raw = rawTx(contractMsg(31, TRIGGER_URL, + triggerContractValue(tronAddr(0x11), + tronAddr(0x33), data)), + nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectCalldataLengthMismatch) { + auto data = trc20Calldata(tronAddr(0x22), 42, false); + data.push_back(0x00); /* trailing byte — could smuggle params */ + auto raw = rawTx(contractMsg(31, TRIGGER_URL, + triggerContractValue(tronAddr(0x11), + tronAddr(0x33), data)), + nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectNonzeroCallValue) { + auto value = triggerContractValue(tronAddr(0x11), tronAddr(0x33), + trc20Calldata(tronAddr(0x22), 42, false)); + std::vector withCallValue; + putBytesField(withCallValue, 1, tronAddr(0x11)); + putBytesField(withCallValue, 2, tronAddr(0x33)); + putVarintField(withCallValue, 3, 7 /* nonzero TRX attached */); + putBytesField(withCallValue, 4, trc20Calldata(tronAddr(0x22), 42, false)); + auto raw = rawTx(contractMsg(31, TRIGGER_URL, withCallValue), nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); + + /* zero call_value explicitly present is fine */ + std::vector zeroCallValue; + putBytesField(zeroCallValue, 1, tronAddr(0x11)); + putBytesField(zeroCallValue, 2, tronAddr(0x33)); + putVarintField(zeroCallValue, 3, 0); + putBytesField(zeroCallValue, 4, trc20Calldata(tronAddr(0x22), 42, false)); + raw = rawTx(contractMsg(31, TRIGGER_URL, zeroCallValue), nullptr, 0); + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_TRC20_TRANSFER); +} + +TEST(Tron, RejectTrc10Fields) { + std::vector v; + putBytesField(v, 1, tronAddr(0x11)); + putBytesField(v, 2, tronAddr(0x33)); + putBytesField(v, 4, trc20Calldata(tronAddr(0x22), 42, false)); + putVarintField(v, 5, 1000001); /* call_token_value / token_id territory */ + auto raw = rawTx(contractMsg(31, TRIGGER_URL, v), nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectMultipleContracts) { + auto contract = contractMsg( + 1, TRANSFER_URL, + transferContractValue(tronAddr(0x11), tronAddr(0x22), 1)); + std::vector raw; + putBytesField(raw, 11, contract); + putBytesField(raw, 11, contract); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectUnknownTopLevelField) { + auto raw = rawTx(contractMsg(1, TRANSFER_URL, + transferContractValue(tronAddr(0x11), + tronAddr(0x22), 1)), + nullptr, 0); + putBytesField(raw, 9, {0x01}); /* auths — permission delegation */ + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectExtraFieldInTransferContract) { + auto value = transferContractValue(tronAddr(0x11), tronAddr(0x22), 1); + putVarintField(value, 4, 99); + auto raw = rawTx(contractMsg(1, TRANSFER_URL, value), nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectPermissionId) { + std::vector any; + putStringField(any, 1, TRANSFER_URL); + putBytesField(any, 2, + transferContractValue(tronAddr(0x11), tronAddr(0x22), 1)); + std::vector c; + putVarintField(c, 1, 1); + putBytesField(c, 2, any); + putVarintField(c, 5, 2); /* Permission_id — multisig account slot */ + auto raw = rawTx(c, nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectDuplicateAnyFields) { + /* Two type_urls in the Any wrapper — last-wins ambiguity, refuse. */ + std::vector any; + putStringField(any, 1, TRIGGER_URL); + putStringField(any, 1, TRANSFER_URL); + putBytesField(any, 2, + transferContractValue(tronAddr(0x11), tronAddr(0x22), 1)); + std::vector c; + putVarintField(c, 1, 1); + putBytesField(c, 2, any); + auto raw = rawTx(c, nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); + + /* Two value fields likewise */ + std::vector any2; + putStringField(any2, 1, TRANSFER_URL); + putBytesField(any2, 2, + transferContractValue(tronAddr(0x11), tronAddr(0x22), 1)); + putBytesField(any2, 2, + transferContractValue(tronAddr(0x11), tronAddr(0x33), 2)); + std::vector c2; + putVarintField(c2, 1, 1); + putBytesField(c2, 2, any2); + raw = rawTx(c2, nullptr, 0); + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectTypeUrlEnumMismatch) { + /* enum says TransferContract, Any says TriggerSmartContract */ + auto raw = rawTx(contractMsg(1, TRIGGER_URL, + transferContractValue(tronAddr(0x11), + tronAddr(0x22), 1)), + nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectBadOwnerAddress) { + auto owner = tronAddr(0x11); + owner[0] = 0x42; /* wrong network prefix */ + auto raw = rawTx(contractMsg(1, TRANSFER_URL, + transferContractValue(owner, tronAddr(0x22), 1)), + nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectOverlongKeyVarint) { + /* The very first varint of raw_data is a field key. An overlong + * (overflowing) key varint must not be silently truncated into some + * other field number. */ + std::vector raw; + putOverlongVarintValue(raw); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectOverlongLengthVarint) { + /* A valid key (field 11, length-delimited) followed by an overlong + * length varint — must not be truncated into some in-bounds length. */ + std::vector raw; + putKey(raw, 11, 2); + putOverlongVarintValue(raw); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectOverlongAmountVarint) { + /* TransferContract.amount (field 3) encoded as an overlong varint. */ + std::vector value; + putBytesField(value, 1, tronAddr(0x11)); + putBytesField(value, 2, tronAddr(0x22)); + putOverlongVarintField(value, 3); + auto raw = rawTx(contractMsg(1, TRANSFER_URL, value), nullptr, 0); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectOverlongFeeLimitVarint) { + /* Top-level fee_limit (field 18) encoded as an overlong varint. */ + auto raw = rawTx(contractMsg(1, TRANSFER_URL, + transferContractValue(tronAddr(0x11), + tronAddr(0x22), 1)), + nullptr, 0); + putOverlongVarintField(raw, 18); + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size(), &parsed), + TRON_TX_UNVERIFIED); +} + +TEST(Tron, RejectTruncated) { + /* Build with the contract as the LAST field: any truncation then either + * cuts into a field (parse failure) or drops the contract entirely — + * both must be UNVERIFIED. (Truncation at a field boundary that only + * drops benign trailing fields like timestamp is legal protobuf and + * stays verified — that case is exercised by the parse tests above.) */ + std::vector raw; + putBytesField(raw, 1, {0xab, 0xcd}); + putVarintField(raw, 8, 1750000000000ULL); + putBytesField(raw, 11, + contractMsg(1, TRANSFER_URL, + transferContractValue(tronAddr(0x11), + tronAddr(0x22), 1000000))); + TronParsedTx sanity; + ASSERT_EQ(tron_parseRawTx(raw.data(), raw.size(), &sanity), + TRON_TX_TRANSFER); + + for (size_t cut = 1; cut < raw.size(); cut++) { + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(raw.data(), raw.size() - cut, &parsed), + TRON_TX_UNVERIFIED) + << "cut=" << cut; + } + + TronParsedTx parsed; + EXPECT_EQ(tron_parseRawTx(nullptr, 0, &parsed), TRON_TX_UNVERIFIED); +} + +TEST(Tron, FormatTrc20AmountUint256) { + uint8_t amount[32] = {0}; + amount[31] = 0x01; + char buf[90]; + ASSERT_TRUE(tron_formatTrc20Amount(amount, buf, sizeof(buf))); + EXPECT_STREQ(buf, "1"); + + /* 10^18 — an 18-decimals token unit */ + uint8_t big[32] = {0}; + const uint64_t e18 = 1000000000000000000ULL; + for (int i = 0; i < 8; i++) + big[24 + i] = static_cast(e18 >> (8 * (7 - i))); + ASSERT_TRUE(tron_formatTrc20Amount(big, buf, sizeof(buf))); + EXPECT_STREQ(buf, "1000000000000000000"); +} + +TEST(Tron, AddressFromBytes) { + /* Base58Check of 41 + 20 bytes must round-trip through the display helper */ + uint8_t addr[21]; + memset(addr, 0x11, sizeof(addr)); + addr[0] = 0x41; + char out[64]; + ASSERT_TRUE(tron_addressFromBytes(addr, out, sizeof(out))); + EXPECT_EQ(out[0], 'T'); /* mainnet addresses render as T... */ + EXPECT_GE(strlen(out), 33u); +}