cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
tron_tx.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: LGPL-3.0-or-later
3 * Copyright (c) 2026 Cryptnox SA
4 */
5
10
11#include "tron_tx.h"
12
13#include <stdio.h>
14#include <string.h>
15
16/* Protobuf wire tags used below (field number << 3 | wire type):
17 * TransferContract.owner_address : field 1, bytes -> 0x0a, len 0x15 (21)
18 * TransferContract.to_address : field 2, bytes -> 0x12, len 0x15 (21)
19 * TransferContract.amount : field 3, varint -> 0x18
20 * TriggerSmartContract.owner_address : field 1, bytes -> 0x0a, len 0x15
21 * TriggerSmartContract.contract_address : field 2, bytes -> 0x12, len 0x15
22 * TriggerSmartContract.data : field 4, bytes -> 0x22, len 0x44 (68)
23 * Transaction.raw_data : field 1, bytes -> 0x0a
24 * Transaction.raw_data.fee_limit : field 18, varint -> 0x90 0x01
25 * Transaction.signature : field 2, bytes -> 0x12, len 0x41 (65)
26 */
27
28static char nibble_hex(unsigned n)
29{
30 return (n < 10U) ? static_cast<char>('0' + n)
31 : static_cast<char>('a' + n - 10U);
32}
33
34static bool is_hex_digit(char c)
35{
36 return ((c >= '0') && (c <= '9')) ||
37 ((c >= 'a') && (c <= 'f')) ||
38 ((c >= 'A') && (c <= 'F'));
39}
40
41static char lower_ascii(char c)
42{
43 return ((c >= 'A') && (c <= 'Z')) ? static_cast<char>(c + ('a' - 'A')) : c;
44}
45
47static size_t hex_strlen(const char *s)
48{
49 size_t n = 0U;
50 for (; s[n] != '\0'; n++) {
51 if (!is_hex_digit(s[n])) { return 0U; }
52 }
53 return n;
54}
55
61static bool raw_hex_ok(const char *raw)
62{
63 if (raw == NULL) { return false; }
64 const size_t n = hex_strlen(raw);
65 return (n != 0U) && ((n % 2U) == 0U);
66}
67
69static bool addr_hex_ok(const char *addr)
70{
71 return (addr != NULL) &&
72 (hex_strlen(addr) == TRON_ADDR_HEX_LEN) &&
73 (addr[0] == '4') && (addr[1] == '1');
74}
75
76size_t tron_varint_hex(uint64_t v, char *out, size_t out_size)
77{
78 uint8_t b[10]; /* 64 bits / 7 bits per byte = 10 bytes max */
79 size_t n = 0U;
80
81 do {
82 uint8_t x = static_cast<uint8_t>(v & 0x7FU);
83 v >>= 7U;
84 if (v != 0U) { x |= 0x80U; }
85 b[n] = x;
86 n++;
87 } while ((v != 0U) && (n < sizeof(b)));
88
89 if ((out == NULL) || (out_size < ((n * 2U) + 1U))) { return 0U; }
90
91 for (size_t i = 0U; i < n; i++) {
92 out[i * 2U] = nibble_hex((b[i] >> 4U) & 0x0FU);
93 out[i * 2U + 1U] = nibble_hex(b[i] & 0x0FU);
94 }
95 out[n * 2U] = '\0';
96 return n * 2U;
97}
98
99/* ── A strict reader for the one protobuf shape a node may hand back ──
100 *
101 * The node serialises raw_data and the card signs sha256 of it, so every byte
102 * must be accounted for. Finding the expected bytes *somewhere* is not enough: a
103 * node could put them in the memo (raw_data.data) while the contract that
104 * executes pays somebody else. So raw_data is walked field by field and only
105 * this shape is accepted:
106 *
107 * raw { 1 ref_block_bytes, 3 ref_block_num, 4 ref_block_hash,
108 * 8 expiration, 11 contract (exactly one), 14 timestamp,
109 * 18 fee_limit (TRC-20 only: exactly the cap we asked for) }
110 * contract { 1 type, 2 parameter } no Permission_id, provider, name
111 * Any { 1 type_url, 2 value } value byte-exact
112 *
113 * No field may repeat. Anything else — data (10), auths (9), scripts (12), an
114 * unknown field, a fixed-width wire type, a length past the end — is refused. */
115
117typedef struct {
118 const char *hex;
119 size_t pos;
120 size_t end;
121} pb_t;
122
123static int hex_val(char c)
124{
125 c = lower_ascii(c);
126 if ((c >= '0') && (c <= '9')) { return c - '0'; }
127 if ((c >= 'a') && (c <= 'f')) { return (c - 'a') + 10; }
128 return -1;
129}
130
131static bool pb_byte(pb_t *p, uint8_t *b)
132{
133 if (p->pos >= p->end) { return false; }
134 const int hi = hex_val(p->hex[p->pos * 2U]);
135 const int lo = hex_val(p->hex[(p->pos * 2U) + 1U]);
136 if ((hi < 0) || (lo < 0)) { return false; }
137 *b = static_cast<uint8_t>((hi << 4) | lo);
138 p->pos++;
139 return true;
140}
141
142static bool pb_varint(pb_t *p, uint64_t *v)
143{
144 uint64_t x = 0U;
145 for (unsigned i = 0U; i < 10U; i++) {
146 uint8_t b;
147 if (!pb_byte(p, &b)) { return false; }
148 x |= static_cast<uint64_t>(b & 0x7FU) << (7U * i);
149 if ((b & 0x80U) == 0U) { *v = x; return true; }
150 }
151 return false; /* longer than any 64-bit varint */
152}
153
155typedef struct {
156 uint32_t num;
157 uint32_t wire; /* 0 varint or 2 length-delimited; nothing else passes */
158 uint64_t value; /* wire 0 */
159 pb_t sub; /* wire 2 */
160} pb_field_t;
161
162static bool pb_next(pb_t *p, pb_field_t *f)
163{
164 uint64_t key;
165 if (!pb_varint(p, &key)) { return false; }
166 f->num = static_cast<uint32_t>(key >> 3U);
167 f->wire = static_cast<uint32_t>(key & 7U);
168 if ((f->num == 0U) || (f->num > 31U)) { return false; }
169 if (f->wire == 0U) { return pb_varint(p, &f->value); }
170 if (f->wire != 2U) { return false; }
171 uint64_t len;
172 if (!pb_varint(p, &len) || (len > (p->end - p->pos))) { return false; }
173 f->sub.hex = p->hex;
174 f->sub.pos = p->pos;
175 f->sub.end = p->pos + static_cast<size_t>(len);
176 p->pos = f->sub.end;
177 return true;
178}
179
181static bool pb_once(const pb_field_t *f, uint32_t wire, uint32_t *seen)
182{
183 const uint32_t bit = 1UL << f->num;
184 if ((f->wire != wire) || ((*seen & bit) != 0U)) { return false; }
185 *seen |= bit;
186 return true;
187}
188
190static bool pb_is_hex(const pb_t *sub, const char *want)
191{
192 const size_t n = sub->end - sub->pos;
193 if (strlen(want) != (n * 2U)) { return false; }
194 const char *h = sub->hex + (sub->pos * 2U);
195 for (size_t i = 0U; i < (n * 2U); i++) {
196 if (lower_ascii(h[i]) != lower_ascii(want[i])) { return false; }
197 }
198 return true;
199}
200
202static bool pb_is_str(const pb_t *sub, const char *want)
203{
204 const size_t n = sub->end - sub->pos;
205 if (strlen(want) != n) { return false; }
206 pb_t c = *sub;
207 for (size_t i = 0U; i < n; i++) {
208 uint8_t b;
209 if (!pb_byte(&c, &b) || (b != static_cast<uint8_t>(want[i]))) {
210 return false;
211 }
212 }
213 return true;
214}
215
216#define TRON_TYPE_TRANSFER 1U /* ContractType.TransferContract */
217#define TRON_TYPE_TRIGGER 31U /* ContractType.TriggerSmartContract */
218#define TRON_URL_TRANSFER "type.googleapis.com/protocol.TransferContract"
219#define TRON_URL_TRIGGER "type.googleapis.com/protocol.TriggerSmartContract"
220
222static bool any_ok(pb_t any, const char *url, const char *v1, const char *v2)
223{
224 uint32_t seen = 0U;
225 bool url_ok = false;
226 bool val_ok = false;
227 pb_field_t f;
228 while (any.pos < any.end) {
229 if (!pb_next(&any, &f) || !pb_once(&f, 2U, &seen)) { return false; }
230 if (f.num == 1U) {
231 url_ok = pb_is_str(&f.sub, url);
232 } else if (f.num == 2U) {
233 val_ok = pb_is_hex(&f.sub, v1) ||
234 ((v2 != NULL) && pb_is_hex(&f.sub, v2));
235 } else {
236 return false;
237 }
238 }
239 return url_ok && val_ok;
240}
241
243static bool contract_ok(pb_t c, uint32_t type, const char *url,
244 const char *v1, const char *v2)
245{
246 uint32_t seen = 0U;
247 bool type_ok = false;
248 bool param_ok = false;
249 pb_field_t f;
250 while (c.pos < c.end) {
251 if (!pb_next(&c, &f)) { return false; }
252 if (f.num == 1U) {
253 if (!pb_once(&f, 0U, &seen)) { return false; }
254 type_ok = (f.value == type);
255 } else if (f.num == 2U) {
256 if (!pb_once(&f, 2U, &seen)) { return false; }
257 param_ok = any_ok(f.sub, url, v1, v2);
258 } else {
259 return false;
260 }
261 }
262 return type_ok && param_ok;
263}
264
273static bool raw_ok(const char *raw_hex, uint32_t type, const char *url,
274 const char *v1, const char *v2, uint64_t fee_limit,
275 uint64_t now_ms, uint64_t *expiration_ms)
276{
277 pb_t p = { raw_hex, 0U, strlen(raw_hex) / 2U };
278 uint32_t seen = 0U;
279 bool have_contract = false;
280 bool fee_ok = (fee_limit == 0U);
281 uint64_t expiration = 0U;
282 pb_field_t f;
283 while (p.pos < p.end) {
284 if (!pb_next(&p, &f)) { return false; }
285 switch (f.num) {
286 case 1U: case 4U: /* ref_block_bytes / _hash */
287 if (!pb_once(&f, 2U, &seen)) { return false; }
288 break;
289 case 3U: case 14U: /* ref_block_num, timestamp */
290 if (!pb_once(&f, 0U, &seen)) { return false; }
291 break;
292 case 8U:
293 if (!pb_once(&f, 0U, &seen)) { return false; }
294 expiration = f.value;
295 break;
296 case 11U:
297 if (!pb_once(&f, 2U, &seen)) { return false; }
298 have_contract = contract_ok(f.sub, type, url, v1, v2);
299 break;
300 case 18U:
301 if (!pb_once(&f, 0U, &seen)) { return false; }
302 fee_ok = (fee_limit != 0U) && (f.value == fee_limit);
303 break;
304 default:
305 return false; /* data, auths, scripts, or anything unknown */
306 }
307 }
308 if (!have_contract || !fee_ok || (expiration == 0U)) { return false; }
309 if ((now_ms == 0U) || (expiration > (now_ms + TRON_TX_MAX_EXPIRY_MS))) {
310 return false;
311 }
312 if (expiration_ms != NULL) { *expiration_ms = expiration; }
313 return true;
314}
315
316bool tron_tx_contract_ok(const char *raw_data_hex, const char *owner_hex,
317 const char *to_hex, uint64_t amount_sun,
318 uint64_t now_ms, uint64_t *expiration_ms)
319{
320 if (!raw_hex_ok(raw_data_hex)) { return false; }
321 if (!addr_hex_ok(owner_hex) || !addr_hex_ok(to_hex)) { return false; }
322 /* A zero-amount transfer is not a payment; reject rather than sign it. */
323 if (amount_sun == 0U) { return false; }
324
325 char amount[24];
326 if (tron_varint_hex(amount_sun, amount, sizeof(amount)) == 0U) {
327 return false;
328 }
329
330 /* TransferContract { 1 owner, 2 to, 3 amount } — the whole value. */
331 char want[4U + TRON_ADDR_HEX_LEN + 4U + TRON_ADDR_HEX_LEN + 2U +
332 sizeof(amount)];
333 int k = snprintf(want, sizeof(want), "0a15%s1215%s18%s",
334 owner_hex, to_hex, amount);
335 if ((k <= 0) || (static_cast<size_t>(k) >= sizeof(want))) { return false; }
336
337 return raw_ok(raw_data_hex, TRON_TYPE_TRANSFER, TRON_URL_TRANSFER,
338 want, NULL, 0U, now_ms, expiration_ms);
339}
340
341size_t tron_trc20_param_hex(const char *to_hex, uint64_t amount,
342 char *out, size_t out_size)
343{
344 if (!addr_hex_ok(to_hex) || (out == NULL) ||
345 (out_size < (TRON_TRC20_PARAM_HEX_LEN + 1U))) {
346 return 0U;
347 }
348
349 size_t p = 0U;
350 /* word 1 — address: 12 zero bytes, then the key hash without its "41". */
351 for (size_t i = 0U; i < 24U; i++, p++) { out[p] = '0'; }
352 for (size_t i = 2U; i < TRON_ADDR_HEX_LEN; i++, p++) {
353 out[p] = lower_ascii(to_hex[i]);
354 }
355 /* word 2 — amount: 24 zero bytes, then the 64-bit value big-endian. */
356 for (size_t i = 0U; i < 48U; i++, p++) { out[p] = '0'; }
357 for (int sh = 60; sh >= 0; sh -= 4, p++) {
358 out[p] = nibble_hex(static_cast<unsigned>((amount >> sh) & 0x0FULL));
359 }
360
361 out[p] = '\0';
362 return p;
363}
364
365bool tron_tx_trc20_ok(const char *raw_data_hex, const char *owner_hex,
366 const char *contract_hex, const char *to_hex,
367 uint64_t amount, uint64_t fee_limit_sun,
368 uint64_t now_ms, uint64_t *expiration_ms)
369{
370 if (!raw_hex_ok(raw_data_hex)) { return false; }
371 if (!addr_hex_ok(owner_hex) || !addr_hex_ok(contract_hex) ||
372 !addr_hex_ok(to_hex)) {
373 return false;
374 }
375 /* A zero-token transfer is not a payment, and an uncapped call is not one
376 * the operator agreed to. Neither gets signed. */
377 if ((amount == 0U) || (fee_limit_sun == 0U)) { return false; }
378
379 char param[TRON_TRC20_PARAM_HEX_LEN + 1U];
380 if (tron_trc20_param_hex(to_hex, amount, param, sizeof(param)) == 0U) {
381 return false;
382 }
383
384 /* TriggerSmartContract { 1 owner, 2 contract, 4 data } — the whole value.
385 * call_value (3) is proto3-default 0 and normally omitted, but a node that
386 * emits it explicitly ("1800") is serialising the same contract. Nothing
387 * else: call_token_value / token_id would move a TRC-10 token as well. */
388 char want[4U + TRON_ADDR_HEX_LEN + 4U + TRON_ADDR_HEX_LEN + 4U + 4U +
389 sizeof(TRON_TRC20_SELECTOR) + sizeof(param) + 8U];
390 int k = snprintf(want, sizeof(want),
391 "0a15%s1215%s2244" TRON_TRC20_SELECTOR "%s",
392 owner_hex, contract_hex, param);
393 if ((k <= 0) || (static_cast<size_t>(k) >= sizeof(want))) { return false; }
394 char want_cv[sizeof(want)];
395 k = snprintf(want_cv, sizeof(want_cv),
396 "0a15%s1215%s18002244" TRON_TRC20_SELECTOR "%s",
397 owner_hex, contract_hex, param);
398 if ((k <= 0) || (static_cast<size_t>(k) >= sizeof(want_cv))) {
399 return false;
400 }
401
402 return raw_ok(raw_data_hex, TRON_TYPE_TRIGGER, TRON_URL_TRIGGER,
403 want, want_cv, fee_limit_sun, now_ms, expiration_ms);
404}
405
406size_t tron_tx_envelope_hex(const char *raw_data_hex, const char *sig_hex,
407 char *out, size_t out_size)
408{
409 if ((raw_data_hex == NULL) || (sig_hex == NULL) || (out == NULL) ||
410 (out_size == 0U)) {
411 return 0U;
412 }
413
414 const size_t raw_len = hex_strlen(raw_data_hex);
415 if ((raw_len == 0U) || ((raw_len % 2U) != 0U)) { return 0U; }
416 if (hex_strlen(sig_hex) != TRON_SIG_HEX_LEN) { return 0U; }
417
418 char len_hex[24];
419 if (tron_varint_hex(raw_len / 2U, len_hex, sizeof(len_hex)) == 0U) {
420 return 0U;
421 }
422
423 int k = snprintf(out, out_size, "0a%s%s1241%s",
424 len_hex, raw_data_hex, sig_hex);
425 if ((k <= 0) || (static_cast<size_t>(k) >= out_size)) {
426 out[0] = '\0';
427 return 0U;
428 }
429 return static_cast<size_t>(k);
430}
One field: its number and either its varint or its payload range.
Definition tron_tx.cpp:155
uint32_t num
Definition tron_tx.cpp:156
uint32_t wire
Definition tron_tx.cpp:157
uint64_t value
Definition tron_tx.cpp:158
Cursor over a hex byte string; positions count BYTES, not nibbles.
Definition tron_tx.cpp:117
const char * hex
Definition tron_tx.cpp:118
size_t end
Definition tron_tx.cpp:120
size_t pos
Definition tron_tx.cpp:119
static uint64_t now_ms(void)
Unix time in ms, 0 if the clock is unset (the checks then refuse).
Definition tron_rpc.cpp:117
bool tron_tx_contract_ok(const char *raw_data_hex, const char *owner_hex, const char *to_hex, uint64_t amount_sun, uint64_t now_ms, uint64_t *expiration_ms)
Verify that a node-serialised raw_data really transfers what we asked.
Definition tron_tx.cpp:316
static bool raw_hex_ok(const char *raw)
true if raw is usable raw_data hex: all hex, non-empty, whole bytes.
Definition tron_tx.cpp:61
static bool is_hex_digit(char c)
Definition tron_tx.cpp:34
#define TRON_TYPE_TRANSFER
Definition tron_tx.cpp:216
static bool pb_is_str(const pb_t *sub, const char *want)
true if a payload is exactly the ASCII string want.
Definition tron_tx.cpp:202
bool tron_tx_trc20_ok(const char *raw_data_hex, const char *owner_hex, const char *contract_hex, const char *to_hex, uint64_t amount, uint64_t fee_limit_sun, uint64_t now_ms, uint64_t *expiration_ms)
Verify that a node-serialised raw_data really is the TRC-20 transfer we asked for.
Definition tron_tx.cpp:365
static bool addr_hex_ok(const char *addr)
true if addr is a "41"-prefixed 21-byte Tron address in hex.
Definition tron_tx.cpp:69
size_t tron_trc20_param_hex(const char *to_hex, uint64_t amount, char *out, size_t out_size)
ABI-encode the arguments of transfer(address,uint256), hex.
Definition tron_tx.cpp:341
static bool pb_is_hex(const pb_t *sub, const char *want)
true if a payload is exactly the hex string want (any case).
Definition tron_tx.cpp:190
static bool contract_ok(pb_t c, uint32_t type, const char *url, const char *v1, const char *v2)
Contract { 1 type, 2 parameter } and nothing else.
Definition tron_tx.cpp:243
#define TRON_URL_TRANSFER
Definition tron_tx.cpp:218
static char lower_ascii(char c)
Definition tron_tx.cpp:41
static bool raw_ok(const char *raw_hex, uint32_t type, const char *url, const char *v1, const char *v2, uint64_t fee_limit, uint64_t now_ms, uint64_t *expiration_ms)
Walk raw_data. fee_limit 0 means the field must be absent.
Definition tron_tx.cpp:273
#define TRON_URL_TRIGGER
Definition tron_tx.cpp:219
static size_t hex_strlen(const char *s)
Length of s if every character is hex, 0 otherwise.
Definition tron_tx.cpp:47
#define TRON_TYPE_TRIGGER
Definition tron_tx.cpp:217
size_t tron_varint_hex(uint64_t v, char *out, size_t out_size)
Hex-encode a value as a protobuf base-128 varint.
Definition tron_tx.cpp:76
size_t tron_tx_envelope_hex(const char *raw_data_hex, const char *sig_hex, char *out, size_t out_size)
Build the hex of a signed Transaction protobuf for broadcasthex.
Definition tron_tx.cpp:406
static bool any_ok(pb_t any, const char *url, const char *v1, const char *v2)
Any { 1 type_url, 2 value }: exactly url and one of the values.
Definition tron_tx.cpp:222
static bool pb_varint(pb_t *p, uint64_t *v)
Definition tron_tx.cpp:142
static char nibble_hex(unsigned n)
Definition tron_tx.cpp:28
static bool pb_byte(pb_t *p, uint8_t *b)
Definition tron_tx.cpp:131
static int hex_val(char c)
Definition tron_tx.cpp:123
static bool pb_next(pb_t *p, pb_field_t *f)
Definition tron_tx.cpp:162
static bool pb_once(const pb_field_t *f, uint32_t wire, uint32_t *seen)
true if field f is wire wire and was not seen before.
Definition tron_tx.cpp:181
Tron transaction hex helpers — protobuf varints, the TransferContract integrity check and the signed-...
#define TRON_TX_MAX_EXPIRY_MS
Latest raw_data.expiration accepted, relative to now (15 min). A real node answer expires ~60 s out; ...
Definition tron_tx.h:51
#define TRON_ADDR_HEX_LEN
Hex length of a Tron address (21 bytes: 0x41 || 20-byte key hash).
Definition tron_tx.h:38
#define TRON_SIG_HEX_LEN
Hex length of a Tron signature (r || s || v).
Definition tron_tx.h:41
#define TRON_TRC20_PARAM_HEX_LEN
Hex length of the two ABI argument words that follow the selector.
Definition tron_tx.h:47
#define TRON_TRC20_SELECTOR
TRC-20 transfer(address,uint256) selector, hex (same as ERC-20).
Definition tron_tx.h:44