You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A dev-node-battery run against dev.node2:53650 lands native pay, stake and unstake cleanly but fails on governance propose at the confirm stage with [Tx Validation] [SIGNATURE ERROR] Transaction hash mismatch. Same family of bug PR #926 fixed for native pay (DEM↔OS canonicalisation across the osDenomination fork), but the governance-propose payload assembler is a separate code path that #926 did not touch.
Captured SDK-side payload (2026-06-15)
Reproduced via scripts/governance-diag.ts against dev.node2:53650. The SDK-side signed payload is:
The SDK signs over serializeTransactionContent(content, isPostFork) (demosdk/src/websdk/demosclass.ts:595-600). The node recomputes the same via Transaction.isCoherent → serializeTransactionContent(tx.content, height) (src/libs/blockchain/transaction.ts:235). They should be byte-identical given the same input.
Possibilities (ordered most → least likely):
transaction_fee.rpc_addressdiffers between SDK sign-time and node validate-time. The SDK leaves it null (see captured payload above). The node's applyGasFeeSeparation at src/libs/blockchain/routines/applyGasFeeSeparation.ts writes tx.content.transaction_fee.rpc_address = rpcAddressHex — but only AFTER Transaction.confirmTx runs (which is what produces the hash-mismatch error). So this MUTATION shouldn't affect isCoherent. Unless something earlier in the path (block aggregator, mempool insert, peer relay) already mutated it before this node received the tx.
SDK vs nodeserializeTransactionContentversions disagree fornetworkUpgrade. The serializer is shared via @kynesyslabs/demosdk/build/denomination/serializerGate.js on the node side and the source version on the SDK side. If the node's pinned SDK version diverges from the SDK the diagnostic was built against, the canonicalisation for networkUpgrade (passed through unchanged in both) could nonetheless differ on edge cases (nested object key order, etc.).
gcr_edits[].txhash is the empty string "" in the SDK-signed payload (captured above) because tx.hash is null when GCRGeneration.generate runs. If anywhere on the path it gets populated with the actual hash before the node serialises for hash-recompute, the bytes diverge.
Acceptance
Add temporary instrumentation to Transaction.isCoherent (node side) that dumps:
The received tx.content (post-RPC parse)
The byte-identical serializeTransactionContent(tx.content, height) output
The recomputed hash
The diff against tx.hash
Re-run scripts/governance-diag.ts against the instrumented node and capture both sides
Diff the two serialized strings byte-for-byte — that pinpoints the field
scripts/governance-diag.ts is in the repo for repro convenience (not currently exported via package.json scripts — invoke directly via bunx tsx scripts/governance-diag.ts).
A separate HTTP 401 is now blocking the battery on dev.node2 — unrelated to this hash mismatch. The dev nodes appear to have been wiped between sessions (my funded address now reports balance: 0, nonce: 0 where it had a full balance after the recovery procedure on 2026-06-13). Re-funding the test address (or repointing the battery at a funded one) is the unblock for reproduction.
This is NOT a regression from SR-4 SDK delivery (sdks#94) — that PR ships purely additive new modules under src/identity/cci/ + src/l2ps/{binding,channel,anchor}/ and does not touch governance code paths.
What
A
dev-node-batteryrun againstdev.node2:53650lands native pay, stake and unstake cleanly but fails on governance propose at theconfirmstage with[Tx Validation] [SIGNATURE ERROR] Transaction hash mismatch. Same family of bug PR #926 fixed for native pay (DEM↔OS canonicalisation across the osDenomination fork), but the governance-propose payload assembler is a separate code path that #926 did not touch.Captured SDK-side payload (2026-06-15)
Reproduced via
scripts/governance-diag.tsagainst dev.node2:53650. The SDK-side signed payload is:{ "type": "networkUpgrade", "from": "0x742e15a60e3a9400c9b890518a1cb0a38f978f77bc69826f559a76e7f44e85b5", "to": "0x742e15a60e3a9400c9b890518a1cb0a38f978f77bc69826f559a76e7f44e85b5", "amount": "0", "data": ["networkUpgrade", { "proposalId": "9cfe50df-09dd-44d1-9fd2-8d83f97b25ac", "proposedParameters": { "blockTimeMs": 1100 }, "rationale": "diag: capture hash inputs", "effectiveAtBlock": 9054 }], "nonce": 1, "timestamp": 1781533330550, "transaction_fee": { "network_fee": "1000000000", "rpc_fee": "1000000000", "additional_fee": "0", "rpc_address": null }, "from_ed25519_address": "0x742e15a60e3a9400c9b890518a1cb0a38f978f77bc69826f559a76e7f44e85b5", "gcr_edits": [ { "type": "networkUpgrade", "isRollback": false, "account": "...", "proposalId": "9cfe50df-09dd-44d1-9fd2-8d83f97b25ac", "proposedParameters": {"blockTimeMs": 1100}, "rationale": "diag: capture hash inputs", "effectiveAtBlock": 9054, "txhash": "" }, { "type": "balance", "account": "...", "operation": "remove", "amount": "1000000000", "txhash": "", "isRollback": false }, { "type": "nonce", "operation": "add", "account": "...", "amount": 1, "txhash": "", "isRollback": false } ] }SDK-derived hash:
3586826f8c42ee5a1d39bc5744e715a184424ff0fb8d201debd7cf41c1478e0e.Where the divergence likely is — three candidates
The SDK signs over
serializeTransactionContent(content, isPostFork)(demosdk/src/websdk/demosclass.ts:595-600). The node recomputes the same viaTransaction.isCoherent→serializeTransactionContent(tx.content, height)(src/libs/blockchain/transaction.ts:235). They should be byte-identical given the same input.Possibilities (ordered most → least likely):
transaction_fee.rpc_addressdiffers between SDK sign-time and node validate-time. The SDK leaves itnull(see captured payload above). The node'sapplyGasFeeSeparationatsrc/libs/blockchain/routines/applyGasFeeSeparation.tswritestx.content.transaction_fee.rpc_address = rpcAddressHex— but only AFTERTransaction.confirmTxruns (which is what produces the hash-mismatch error). So this MUTATION shouldn't affect isCoherent. Unless something earlier in the path (block aggregator, mempool insert, peer relay) already mutated it before this node received the tx.serializeTransactionContentversions disagree fornetworkUpgrade. The serializer is shared via@kynesyslabs/demosdk/build/denomination/serializerGate.json the node side and the source version on the SDK side. If the node's pinned SDK version diverges from the SDK the diagnostic was built against, the canonicalisation fornetworkUpgrade(passed through unchanged in both) could nonetheless differ on edge cases (nested object key order, etc.).gcr_edits[].txhashis the empty string""in the SDK-signed payload (captured above) becausetx.hashis null whenGCRGeneration.generateruns. If anywhere on the path it gets populated with the actual hash before the node serialises for hash-recompute, the bytes diverge.Acceptance
Transaction.isCoherent(node side) that dumps:tx.content(post-RPC parse)serializeTransactionContent(tx.content, height)outputtx.hashscripts/governance-diag.tsagainst the instrumented node and capture both sidesserializedstrings byte-for-byte — that pinpoints the fieldNotes
scripts/governance-diag.tsis in the repo for repro convenience (not currently exported via package.json scripts — invoke directly viabunx tsx scripts/governance-diag.ts).HTTP 401is now blocking the battery on dev.node2 — unrelated to this hash mismatch. The dev nodes appear to have been wiped between sessions (my funded address now reportsbalance: 0, nonce: 0where it had a full balance after the recovery procedure on 2026-06-13). Re-funding the test address (or repointing the battery at a funded one) is the unblock for reproduction.dev.node2.demos.sh:53650on 2026-06-13 after the consensus recovery (Re-validate dev environment after reboot #890).src/identity/cci/+src/l2ps/{binding,channel,anchor}/and does not touch governance code paths.