Skip to content

feat(abstract-substrate): expose rawExtrinsicPayload separate from signablePayload#9141

Merged
abhijit0943 merged 1 commit into
masterfrom
abhijitmadhusudan496/si-926-polyx-expose-raw-extrinsicpayload-separately-from-b8d7
Jun 30, 2026
Merged

feat(abstract-substrate): expose rawExtrinsicPayload separate from signablePayload#9141
abhijit0943 merged 1 commit into
masterfrom
abhijitmadhusudan496/si-926-polyx-expose-raw-extrinsicpayload-separately-from-b8d7

Conversation

@abhijit0943

@abhijit0943 abhijit0943 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

POLYX TSS follows Substrate signing rules: extrinsics over 256 bytes are signed as a blake2_256 hash rather than the raw payload. signablePayload already applies this (correct for user MPC/combine), but nothing exposed the raw ExtrinsicPayload bytes that the HSM polyx/signtx path needs. Large nominate/switch-validator txs (e.g. 9 validators) therefore broke signing on prod.

This exposes the raw payload separately while keeping signablePayload as the correct MPC/combine signing bytes.

Changes

  • abstract-substrate: add a rawExtrinsicPayload getter (full ExtrinsicPayload.toU8a({ method: true })), and refactor signablePayload to derive from it via utils.getSubstrateSigningBytes — no duplicated 256-byte logic in transaction.ts.
  • sdk-coin-polyx: surface the raw payload as rawSignableHex in the polyx toJson output, so consumers needing the raw bytes can read it alongside signableHex. The field is declared on the polyx-specific TxData (not the shared abstract-substrate one), so other Substrate coins (tao, dot, etc.) are unaffected.

Testing

yarn unit-test for the nominate builder. New cases assert:

  • ≤256B nominate: signablePayload equals the raw rawExtrinsicPayload bytes, and toJson().rawSignableHex matches the raw hex.
  • >256B nominate (9 validators): signablePayload is the 32-byte blake2_256 hash, rawExtrinsicPayload is the full raw payload (>256B), and toJson().rawSignableHex matches the raw hex. (Each target adds 33 bytes over 79 bytes of fixed overhead, so the extrinsic crosses 256B from 6 targets up; 9 mirrors the SI-926 scenario.)

@linear-code

linear-code Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

SI-926

@abhijit0943

Copy link
Copy Markdown
Contributor Author

@claude review this

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @abhijit0943's task —— View job


Code Review Progress

  • Read and analyze modified files
  • Review implementation approach and architecture
  • Check code quality and best practices
  • Review test coverage and correctness
  • Provide comprehensive feedback

Code Review Results

This PR successfully addresses the POLYX TSS signing issue by cleanly separating raw extrinsic payload bytes from the signable payload. The implementation is well-architected and maintains backward compatibility.

✅ Strengths

Architecture & Design

  • Clean separation of concerns: The new rawExtrinsicPayload getter provides access to full payload bytes while keeping signablePayload focused on MPC/TSS signing logic
  • DRY refactoring: Eliminates code duplication by making signablePayload derive from rawExtrinsicPayload via the shared utils.getSubstrateSigningBytes() function
  • Clear API surface: Both payloads are exposed at appropriate levels - rawExtrinsicPayload on the transaction object and rawSignableHex on JSON representations

Implementation Quality

  • Consistent interface: rawSignableHex field is properly added to both TxData and TransactionExplanation interfaces
  • Complete coverage: All relevant toJson() methods populate the new field (abstract-substrate and polyx override)
  • Good documentation: JSDoc clearly explains the purpose and relationship between the two payload types

Testing

  • Comprehensive test cases: New tests verify both ≤256B and >256B scenarios with appropriate assertions
  • Boundary testing: Includes edge case testing of the exact 256-byte boundary via getSubstrateSigningBytes
  • Real-world scenarios: Tests use realistic nominate transactions with varying validator counts

🔧 Minor Improvements

Type Safety (modules/abstract-substrate/src/lib/transaction.ts:151)

rawSignableHex: Buffer.from(this.rawExtrinsicPayload).toString('hex'),

Consider caching this conversion since rawExtrinsicPayload involves registry type creation:

get rawSignableHex(): string {
  return Buffer.from(this.rawExtrinsicPayload).toString('hex');
}

Documentation Enhancement (modules/abstract-substrate/src/lib/iface.ts:124-129)
The JSDoc is excellent but could mention the 256-byte threshold explicitly:

/**
 * Hex of the full raw encoded `ExtrinsicPayload` (see `Transaction.rawExtrinsicPayload`).
 * Surfaced alongside the MPC/combine `signableHex` so consumers that need the raw payload
 * (e.g. the HSM signing path) can use it for extrinsics larger than 256 bytes, where
 * `signableHex` is the blake2_256 hash rather than the raw bytes.
 */

Error Handling (modules/abstract-substrate/src/lib/transaction.ts:545-550)
The rawExtrinsicPayload getter could benefit from error handling for registry operations:

get rawExtrinsicPayload(): Uint8Array {
  if (!this._substrateTransaction) {
    throw new InvalidTransactionError('No transaction data available');
  }
  const extrinsicPayload = this._registry.createType('ExtrinsicPayload', this._substrateTransaction, {
    version: EXTRINSIC_VERSION,
  });
  return extrinsicPayload.toU8a({ method: true });
}

✅ Code Quality Assessment

  • Security: ✅ No security issues identified
  • Performance: ✅ Minimal overhead, shared utility function prevents duplication
  • Maintainability: ✅ Clean abstractions and clear naming
  • Backward Compatibility: ✅ Additive changes only, existing API unchanged
  • Test Coverage: ✅ Comprehensive test scenarios including edge cases

📋 Summary

This is a well-implemented solution that addresses the production issue with large POLYX nominate transactions. The refactoring improves code organization while solving the HSM signing path requirements. The minor suggestions above are optional enhancements that don't block the PR.

Recommendation: ✅ Approve - Ready to merge after addressing any desired improvements.


Job Run: Link

@abhijit0943 abhijit0943 force-pushed the abhijitmadhusudan496/si-926-polyx-expose-raw-extrinsicpayload-separately-from-b8d7 branch 2 times, most recently from 032ee70 to c4a57aa Compare June 30, 2026 09:37
@abhijit0943

Copy link
Copy Markdown
Contributor Author

@claude re-review

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @abhijit0943's task —— View job


Re-reviewing PR Changes

  • Read and analyze all modified files
  • Review implementation approach and code quality
  • Check test coverage and correctness
  • Provide comprehensive review feedback
  • Compare with previous review suggestions

✅ Re-Review Results

After analyzing the current implementation, this PR successfully addresses the POLYX TSS signing issue with a clean and well-architected solution. The implementation properly separates raw extrinsic payload bytes from signable payload for HSM compatibility.

✅ Strengths Confirmed

Architecture & Design Excellence

  • Perfect separation of concerns: The rawExtrinsicPayload getter provides full payload bytes while signablePayload handles MPC/TSS signing logic
  • Excellent DRY refactoring: signablePayload now derives from rawExtrinsicPayload via shared utils.getSubstrateSigningBytes(), eliminating code duplication
  • Clean API surface: Both payloads exposed at appropriate abstraction levels - transaction object and JSON representations

Implementation Quality

  • Consistent interface design: rawSignableHex properly integrated into TxData and TransactionExplanation interfaces
  • Complete implementation: All relevant toJson() methods populate the new field (abstract-substrate base class and polyx override)
  • Excellent documentation: JSDoc clearly explains purpose and relationship between payload types

Comprehensive Testing

  • Boundary testing: Thorough coverage of ≤256B and >256B scenarios with proper assertions
  • Real-world validation: Uses realistic nominate transactions with multiple validators
  • Edge case coverage: Direct testing of 256-byte boundary via getSubstrateSigningBytes

🔧 Implementation Analysis

Key Changes Reviewed:

  1. abstract-substrate/iface.ts:129: Added rawSignableHex? field with clear JSDoc explaining HSM use case
  2. abstract-substrate/transaction.ts:543-548: New rawExtrinsicPayload getter providing full ExtrinsicPayload bytes
  3. abstract-substrate/transaction.ts:560-562: Refactored signablePayload to derive from rawExtrinsicPayload
  4. polyx/transaction.ts:74: Populates rawSignableHex in main POLYX toJson flow
  5. polyx/transaction.ts:103-105: Ensures rawSignableHex included in base class fallback path
  6. Test enhancements: Comprehensive assertions for both payload types and boundary conditions

Code Quality Assessment:

  • Security: ✅ No security issues - readonly getters and proper type creation
  • Performance: ✅ Efficient - shared utility prevents duplication, minimal overhead
  • Maintainability: ✅ Excellent - clear abstractions and consistent naming
  • Backward Compatibility: ✅ Perfect - purely additive changes, no breaking modifications

✅ Test Coverage Verification

The test suite comprehensively validates:

  • Small payloads (≤256B): Confirms signablePayload equals rawExtrinsicPayload
  • Large payloads (>256B): Verifies signablePayload is 32-byte hash while rawExtrinsicPayload stays full-length
  • JSON serialization: Confirms rawSignableHex matches raw payload hex string
  • Boundary conditions: Direct testing of exact 256-byte threshold behavior

📋 Summary

This PR represents a high-quality solution that:

  • Solves the production issue with large POLYX nominate transactions failing HSM signing
  • Improves code architecture through DRY refactoring and clean separation of concerns
  • Maintains backward compatibility with purely additive changes
  • Provides comprehensive testing covering all scenarios and edge cases

The implementation is production-ready and demonstrates excellent software engineering practices.

Final Recommendation: ✅ Approved - Excellent implementation, ready for merge.


Job Run: GitHub Actions

@abhijit0943 abhijit0943 marked this pull request as ready for review June 30, 2026 10:00
@abhijit0943 abhijit0943 requested a review from a team as a code owner June 30, 2026 10:00
@Doddanna17

Copy link
Copy Markdown
Contributor

Three findings from reviewing this:

1. rawSignableHex on the wrong interface

Added to abstract-substrate's shared TxData, but modules/sdk-coin-polyx/src/lib/iface.ts already has a coin-specific TxData extends Interface.TxData with POLYX-only fields (assetId, fromDID, etc). This field belongs there. As-is, TAO and DOT consumers see rawSignableHex in their TxData type with no guarantee it's ever populated — their toJson() never sets it.

2. Missing toJson().rawSignableHex assertion in the ≤256B test

The PR tests toJson().rawSignableHex only in the >256B case. The ≤256B test should also assert tx.toJson().rawSignableHex === Buffer.from(tx.rawExtrinsicPayload).toString('hex') to cover the code path where rawSignableHex is set at the top of toJson() (before the branch logic).

3. Why 9 validators, not 8?

Old comment said "6+" with 8 validators. PR bumps to 9 with comment "9 validators pushes over 256 bytes". What changed — is 8 no longer enough? Without an explanation (e.g. different validator address length in test data, or a payload field that grew), future maintainers can't reason about the boundary and might regress it.

nvrakesh06
nvrakesh06 previously approved these changes Jun 30, 2026
@abhijit0943 abhijit0943 force-pushed the abhijitmadhusudan496/si-926-polyx-expose-raw-extrinsicpayload-separately-from-b8d7 branch from c4a57aa to 89e47e7 Compare June 30, 2026 10:19
…gnablePayload

Add a `rawExtrinsicPayload` getter that returns the full raw encoded
`ExtrinsicPayload` bytes, and refactor `signablePayload` to derive from it
via `utils.getSubstrateSigningBytes` (no duplicated 256-byte blake2 logic).

Surface the raw payload as `rawSignableHex` on the polyx-specific `TxData`,
populated in the polyx `toJson` output, so consumers that need the raw bytes
(e.g. the HSM `polyx/signtx` path) can use them for large extrinsics where
`signablePayload` is the blake2_256 hash. The field is kept on the polyx coin
interface rather than the shared one, so other Substrate coins are unaffected.

Generated with [Linear](https://linear.app/bitgo/issue/SI-926/polyx-expose-raw-extrinsicpayload-separately-from-signable-signing#agent-session-0ac3bf90)

Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com>
@abhijit0943 abhijit0943 force-pushed the abhijitmadhusudan496/si-926-polyx-expose-raw-extrinsicpayload-separately-from-b8d7 branch from 89e47e7 to 50cae11 Compare June 30, 2026 10:22
@BitGo BitGo deleted a comment from github-actions Bot Jun 30, 2026
@abhijit0943

Copy link
Copy Markdown
Contributor Author

@claude re-review the changes are made

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @abhijit0943's task —— View job


Re-reviewing PR Changes

  • Read and analyze all modified files to understand current implementation
  • Check if Doddanna17's three findings have been addressed
  • Provide comprehensive review feedback on the updated code
  • Verify architectural decisions and test coverage

✅ Analysis of Doddanna17's Findings

After reviewing the current implementation, I can confirm that all three findings from Doddanna17 have been properly addressed:

Finding 1: ✅ RESOLVED - rawSignableHex field placement

  • Issue: Field was on abstract-substrate's shared TxData instead of POLYX-specific interface
  • Resolution: The field is now correctly placed in /modules/sdk-coin-polyx/src/lib/iface.ts:23 as part of the POLYX-specific TxData extends Interface.TxData
  • Verification: Other Substrate coins (TAO, DOT) no longer see this field in their interface, preventing confusion

Finding 2: ✅ RESOLVED - Missing test assertion for ≤256B case

  • Issue: The ≤256B test didn't assert toJson().rawSignableHex
  • Resolution: Line 207 in the test now includes should.equal(tx.toJson().rawSignableHex, rawExtrinsicPayload.toString('hex')) for the ≤256B case
  • Verification: Both test cases (≤256B and >256B) now properly verify the rawSignableHex field

Finding 3: ✅ RESOLVED - Validator count explanation

  • Issue: Changed from 8 to 9 validators without explanation
  • Resolution: Line 214-215 includes detailed comment explaining the boundary calculation: "Each nominate target adds a 33-byte MultiAddress... 5 targets stay raw at 244 bytes and 6 already cross to 277 bytes... this uses 9 to mirror the multi-nomination scenario from SI-926"
  • Verification: The comment provides mathematical justification for the validator count choice

✅ Comprehensive Code Review

Architecture & Implementation Quality

  • Excellent separation of concerns: The new rawExtrinsicPayload getter (abstract-substrate/transaction.ts:543-548) provides access to full payload bytes while signablePayload (lines 560-562) handles TSS signing logic via utils.getSubstrateSigningBytes()
  • Clean DRY refactoring: Eliminates code duplication by making signablePayload derive from rawExtrinsicPayload
  • Proper interface design: rawSignableHex field is correctly scoped to POLYX-specific TxData interface (polyx/iface.ts:18-23)
  • Complete implementation coverage: Both POLYX transaction paths populate rawSignableHex (polyx/transaction.ts:74 and line 104)

Code Quality Assessment

  • Security: ✅ No security concerns - readonly getters with proper type creation
  • Performance: ✅ Efficient implementation with shared utility function
  • Maintainability: ✅ Clear abstractions, excellent JSDoc documentation
  • Backward Compatibility: ✅ Purely additive changes, no breaking modifications

Test Coverage Verification

  • Boundary testing: Comprehensive coverage of ≤256B and >256B scenarios with proper assertions
  • Real-world validation: Uses realistic nominate transactions with multiple validators
  • Edge case coverage: Direct testing of exact 256-byte threshold behavior
  • Complete field verification: Both signablePayload and rawSignableHex properly tested in all scenarios

📋 Final Assessment

This PR successfully resolves the POLYX TSS signing issue with large nominate transactions by cleanly separating raw extrinsic payload bytes from signable payload. The implementation demonstrates excellent software engineering practices with proper abstraction, comprehensive testing, and clear documentation.

All feedback from the previous review has been addressed appropriately, resulting in a production-ready solution that maintains backward compatibility while solving the HSM signing path requirements.

Final Recommendation: ✅ Approved - Excellent implementation ready for merge.


Job Run: GitHub Actions

@Doddanna17 Doddanna17 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rawExtrinsicPayload correctly exposes pre-hash bytes; signablePayload now delegates to it — no duplicated createType logic. Scope correctly limited to polyx TxData, other substrate coins unaffected.

The else branch in polyx toJson() properly propagates rawSignableHex from the pre-computed result without re-invoking the getter.

Tests cover both the sub-256B identity case and the >256B hash-vs-raw divergence, with 9-validator count mirroring SI-926.

LGTM.

@abhijit0943 abhijit0943 merged commit ec47ff9 into master Jun 30, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants