fix(abstract-substrate): blake2_256 prehash signablePayload over 256 bytes#9133
Conversation
cadec00 to
20a0424
Compare
|
@claude review |
|
Claude finished @abhijit0943's task —— View job Code Review
|
…bytes Substrate signs the raw encoded ExtrinsicPayload when it is at most 256 bytes, but signs the blake2_256 hash of the payload when it exceeds 256 bytes. The signablePayload getter previously always returned the raw bytes, so for large extrinsics (e.g. POLYX nominate with 6+ validators) the BitGoJS user signed the raw payload while the HSM signed the hash, causing TSS signature combination to fail with InvalidSignature. Return blake2_256(raw) (32 bytes) when the payload is larger than 256 bytes, and the raw bytes otherwise, so all downstream consumers (sdk-coin-polyx, sdk-coin-dot, recovery flows) get the correct signableHex without coin-specific logic. Generated with [Linear](https://linear.app/bitgo/issue/SI-911/fix-signablepayload-to-apply-substrate-blake2-256-prehash-when#agent-session-0bfb5d75) Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com>
20a0424 to
db1108e
Compare

Summary
Substrate signs the raw encoded
ExtrinsicPayloadonly when it is at most 256 bytes; for payloads larger than 256 bytes it signs theblake2_256hash of those bytes instead (Polkadot.js@polkadot/types/extrinsic/util, mirrored in the HSM firmware).The
signablePayloadgetter in@bitgo/abstract-substratealways returned the raw payload. For large extrinsics — e.g. POLYXswitchValidator/nominate with 6+ validators — the BitGoJS user signed the raw bytes while the HSM signed the hash, so the two parties signed different messages and TSS G-share combine failed withInvalidSignature.This fix makes
signablePayloadreturn the bytes that are actually signed:length <= 256→ raw bytes (unchanged)length > 256→blake2_256(raw)(32 bytes)The decision is extracted into a shared
utils.getSubstrateSigningBytes(raw)helper (with the threshold as theMAX_RAW_SIGNING_PAYLOAD_BYTESconstant) so the rule lives in one documented place and gives the future DOT fix a copy-paste target. All downstream consumers (sdk-coin-polyx, recovery flows) then get the correctsignableHexat the source, with no coin-specific logic needed in WP.Testing
sdk-coin-polyxnominateBuilder: small nominate extrinsic returns raw bytes (≤256, not 32); 8-validator nominate returns a 32-byte hashed payload.sdk-coin-polyxbatchStakingBuilder:bond + nominatewith 1 validator stays raw (≤256); with 6 validators returns a 32-byte hash. The batch wrapsnominateand crosses the threshold sooner, so it is covered explicitly.Notes / follow-ups
sdk-coin-dothas its own standalonesignablePayloadgetter with the same raw-only bug, and the DOT wasm path (wasmTx.signablePayload()) needs separate verification. Both are out of scope here per the issue and tracked as a DOT follow-up; the new helper is the intended drop-in for that fix.@bitgo-beta/abstract-substrate; the local blake2 workaround in WPpolyx.tsshould be reverted at that point to avoid duplicate logic.