Skip to content

Governance propose Transaction hash mismatch — separate code path from PR #926 #941

Description

@linear

What

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:

{
  "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 via Transaction.isCoherentserializeTransactionContent(tx.content, height) (src/libs/blockchain/transaction.ts:235). They should be byte-identical given the same input.

Possibilities (ordered most → least likely):

  1. transaction_fee.rpc_address differs 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.
  2. SDK vs node serializeTransactionContent versions disagree for networkUpgrade. 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.).
  3. 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
  • Apply the matching canonicalisation (probably mirror PR fix(l2ps): canonicalise inner native amount across the osDenomination fork #926's approach: bind the SDK-side serializer to the node-derived shape for that field) and verify the next battery run lands governance propose

Notes

  • 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.
  • Found during dev-node battery run against dev.node2.demos.sh:53650 on 2026-06-13 after the consensus recovery (Re-validate dev environment after reboot #890).
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions