From 1db2c4265bf61bf39f3142643ef15c586f807a00 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 4 Oct 2026 02:10:51 +0200 Subject: [PATCH] Aegis: refuse a Tron transaction whose fields appear twice The decoder read the first copy of a singular protobuf field; java-tron keeps the last. Any.value is opaque bytes, so a TransferContract carrying two recipients and two amounts hashes to the same txid either way: the overlay showed "1 TRX to X" while the chain would move 999 TRX to another address. It also defeated the plan-time and sign-time draft checks against a hostile node. A repeated singular field, or a known field with the wrong wire type, now refuses the transaction. --- bundled-addons/aegis/lib/tron-decode.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/bundled-addons/aegis/lib/tron-decode.js b/bundled-addons/aegis/lib/tron-decode.js index 8ecc60ae..396fc3b9 100644 --- a/bundled-addons/aegis/lib/tron-decode.js +++ b/bundled-addons/aegis/lib/tron-decode.js @@ -81,9 +81,20 @@ module.exports = function makeTronDecode({ sha256, base58check }) { } return out; } - const one = (fields, n) => fields.find((f) => f.field === n); - const bytesOf = (fields, n) => { const f = one(fields, n); return f && f.wire === 2 ? f.value : null; }; - const numOf = (fields, n) => { const f = one(fields, n); return f && f.wire === 0 ? f.value : null; }; + // A singular field that appears twice is legal protobuf: the parser keeps + // the LAST copy (and merges repeated sub-messages). java-tron does that, so + // reading the first copy would show the user one recipient and amount while + // the chain executes another — and Any.value is opaque bytes, so the signed + // hash is the same either way. Refuse instead of guessing, and refuse a + // known field sent with the wrong wire type for the same reason. + const one = (fields, n, wire) => { + const hits = fields.filter((f) => f.field === n); + if (hits.length > 1) throw new Error(`field ${n} appears ${hits.length} times; refusing an ambiguous transaction`); + if (hits[0] && hits[0].wire !== wire) throw new Error(`field ${n} has wire type ${hits[0].wire}, expected ${wire}`); + return hits[0]; + }; + const bytesOf = (fields, n) => { const f = one(fields, n, 2); return f ? f.value : null; }; + const numOf = (fields, n) => { const f = one(fields, n, 0); return f ? f.value : null; }; // 21-byte Tron address (0x41 || h20) → "T…" base58check. Anything else is // returned as hex so the overlay never hides a malformed field.