From f367d6cd1802d6c0c5b37f20b066a1a0ca43fafb Mon Sep 17 00:00:00 2001 From: highlander Date: Mon, 20 Jul 2026 21:59:30 -0300 Subject: [PATCH] fix(hive): emit the wire symbols hived uses, not the display ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hived encodes HIVE as "STEEM" and HBD as "SBD" — the 2020 rebrand renamed the tokens but not their serialization. We emitted the display spelling, so the device signed bytes hived re-serializes differently; its signature check recovers a key from those bytes, finds it in no authority, and reports "missing required active authority". Every asset-bearing op was affected. This serializer and the firmware parser were byte-exact mirrors of each other, so both were wrong together and every test passed. Added two golden vectors from hived's own condenser_api.get_transaction_hex — the only source that could have caught it. Requires the matching firmware fix (BitHighlander/keepkey-firmware#316). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/bun/txbuilder/hive-ops.test.ts | 60 ++++++++++++++++++- .../src/bun/txbuilder/hive-ops.ts | 10 +++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/projects/keepkey-vault/src/bun/txbuilder/hive-ops.test.ts b/projects/keepkey-vault/src/bun/txbuilder/hive-ops.test.ts index fd4aefac..512dc159 100644 --- a/projects/keepkey-vault/src/bun/txbuilder/hive-ops.test.ts +++ b/projects/keepkey-vault/src/bun/txbuilder/hive-ops.test.ts @@ -17,11 +17,16 @@ function vstr(s: string): Buffer { return Buffer.concat([Buffer.from([b.length]), b]) } +// Call sites pass the DISPLAY symbol; hived serializes the pre-rebrand names, +// so this is where "HIVE" becomes "STEEM". Pinned by the hived golden vector +// at the bottom of this file. +const WIRE: Record = { HIVE: 'STEEM', HBD: 'SBD' } + function vasset(amount: bigint, precision: number, symbol: string): Buffer { const out = Buffer.alloc(16) out.writeBigInt64LE(amount) out.writeUInt8(precision, 8) - out.write(symbol, 9, 'ascii') + out.write(WIRE[symbol] ?? symbol, 9, 'ascii') return out } @@ -460,3 +465,56 @@ describe('phase-3: internal market (limit orders)', () => { )).toThrow(/Cannot mix posting-tier and active-tier/) }) }) + +/** + * Golden vectors from hived itself, not from our own understanding of the + * format: + * + * curl -X POST https://api.hive.blog -H 'Content-Type: application/json' \ + * -d '{"jsonrpc":"2.0","method":"condenser_api.get_transaction_hex", + * "params":[],"id":1}' + * + * This serializer and the firmware parser were byte-exact mirrors of each + * other while both disagreed with the chain — we wrote "HIVE"/"HBD" where + * hived writes "STEEM"/"SBD". Every test passed and every signature was + * rejected on-chain as "missing required active authority", because hived + * re-serializes the operation and recovers a key from different bytes. + * Vectors the chain produced are the only ones that can catch that. + * + * get_transaction_hex emits a whole transaction, so its output ends with the + * `signatures` array count; the digest preimage stops after the extensions + * varint, so the trailing "00" is dropped below. + */ +describe('hived golden vectors', () => { + const GOLDEN_HEADER = { refBlockNum: 4660, refBlockPrefix: 0xdeadbeef, expirationUnix: 0x5fffaa40 } + + test('limit_order_create matches hived byte for byte', () => { + const { serializedTx } = serializeHiveOpsTx({ + ...GOLDEN_HEADER, + operations: [['limit_order_create', { + owner: 'alice', orderid: 42, + amount_to_sell: '1.500 HIVE', min_to_receive: '0.400 HBD', + fill_or_kill: true, expiration: 0x6553f5b0, + }]], + }) + expect(serializedTx.toString('hex')).toBe( + '3412efbeadde40aaff5f010505616c6963652a000000dc05000000000000' + + '03535445454d00009001000000000000035342440000000001b0f5536500', + ) + }) + + test('claim_reward_balance matches hived byte for byte', () => { + const { serializedTx } = serializeHiveOpsTx({ + refBlockNum: 0, refBlockPrefix: 0, expirationUnix: 0x5fffaa40, + operations: [['claim_reward_balance', { + account: 'alice', + reward_hive: '1.000 HIVE', reward_hbd: '2.000 HBD', reward_vests: '3.000000 VESTS', + }]], + }) + expect(serializedTx.toString('hex')).toBe( + '00000000000040aaff5f012705616c696365e803000000000000035354' + + '45454d0000d0070000000000000353424400000000c0c62d0000000000' + + '065645535453000000', + ) + }) +}) diff --git a/projects/keepkey-vault/src/bun/txbuilder/hive-ops.ts b/projects/keepkey-vault/src/bun/txbuilder/hive-ops.ts index c9e8298c..d859af38 100644 --- a/projects/keepkey-vault/src/bun/txbuilder/hive-ops.ts +++ b/projects/keepkey-vault/src/bun/txbuilder/hive-ops.ts @@ -159,6 +159,14 @@ function timePointSec(v: any, what: string): number { // layout). Precisions are fixed per symbol: HIVE/HBD = 3, VESTS = 6. const ASSET_PRECISION: Record = { HIVE: 3, HBD: 3, VESTS: 6 } +// The 2020 rebrand renamed the tokens but NOT their serialization: hived still +// encodes HIVE as "STEEM" and HBD as "SBD" (verify with +// condenser_api.get_transaction_hex). Emitting the display spelling makes the +// device sign bytes hived re-serializes differently, so signature recovery +// yields a key in no authority and the node reports the deeply unhelpful +// "missing required active authority". Firmware maps these back for the OLED. +const WIRE_SYMBOL: Record = { HIVE: 'STEEM', HBD: 'SBD' } + function asset(input: any, allowed: string[], what: string): Buffer { const m = /^(\d+)\.(\d+) ([A-Z]+)$/.exec(String(input ?? '')) if (!m) throw new Error(`${what}: malformed asset "${input}" — expected "0.000 SYMBOL"`) @@ -174,7 +182,7 @@ function asset(input: any, allowed: string[], what: string): Buffer { const out = Buffer.alloc(16) out.writeBigInt64LE(amount) out.writeUInt8(precision, 8) - out.write(symbol, 9, 'ascii') + out.write(WIRE_SYMBOL[symbol] ?? symbol, 9, 'ascii') return out }