Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,14 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
// throwing, so tryGetV8Builder never fires. See SI-981.
const bondArgs = args.calls[0].args as { controller?: unknown };
if (bondArgs.controller === undefined) {
return this.getV8BatchStakingBuilder();
// Forward the factory's material (set by the caller, e.g. wallet-platform's
// getSignedTx via `.material(liveMaterial).from(...)`). Unlike the v7 getters,
// getV8BatchStakingBuilder() does NOT forward this._material and would otherwise
// keep the builder's static getV8Material(). On the decode-success path this._material
// is the material that just decoded the tx, so it is the correct one to rebuild with —
// without this, a rebuild uses stale static specVersion and the signable no longer
// matches the HSM-signed bytes (InvalidSignature on combine). See SI-1034.
return this.getV8BatchStakingBuilder().material(this._material);
}
return this.getBatchBuilder();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
V8WithdrawUnbondedBuilder,
V8NominateBuilder,
SingletonRegistry,
Interface,
} from '../../../src/lib';
import { BatchArgs, NominateArgs, V8BondArgs } from '../../../src/lib/iface';
import utils from '../../../src/lib/utils';
Expand Down Expand Up @@ -170,6 +171,50 @@ describe('V8BatchStakingBuilder', function () {
const builder = v8Factory.from(rawTxHex);
should.ok(builder instanceof V8BatchStakingBuilder);
});

it('factory.material(liveMaterial).from(...) forwards the live material into the rebuilt v8 batch staking builder (SI-1034)', async () => {
// Reproduces the custodial staking incident: the HSM signs against LIVE chain material
// (e.g. a specVersion bump after a runtime upgrade), but wallet-platform's getSignedTx
// re-verifies by calling `factory.material(liveMaterial).from(serializedTxHex)`. On the
// decode-success path, `getBuilder()` used to return `getV8BatchStakingBuilder()` without
// forwarding `this._material`, so the rebuild silently fell back to the builder's static
// `getV8Material()` (stale specVersion) instead of the caller's live material. Because
// specVersion is embedded in the raw ExtrinsicPayload bytes that are actually signed, the
// rebuilt signable payload would then diverge from the one the HSM signed, producing
// `InvalidSignature: invalid key signature` at Trust approval.
//
// Simulate a "live" chain that has moved past the pinned static testnet v8 material by
// bumping specVersion on a copy of it.
const liveMaterial = {
...testnetV8Material,
specVersion: testnetV8Material.specVersion + 10,
} as unknown as Interface.Material;

const originalTx = await new V8BatchStakingBuilder(buildTestConfig())
.material(liveMaterial)
.amount(bondAmount)
.payee('Staked')
.validators([validator])
.sender({ address: stash })
.validity({ firstValid, maxDuration: 64 })
.referenceBlock(referenceBlock)
.sequenceId({ name: 'Nonce', keyword: 'nonce', value: nonce })
.build();
const rawTxHex = originalTx.toBroadcastFormat();
const originalSignablePayload = originalTx.signablePayload.toString('hex');

const rebuiltBuilder = new TransactionBuilderFactory(coins.get('tpolyx')).material(liveMaterial).from(rawTxHex);
// The raw signing payload does not carry the sender address or the raw `firstValid` block
// number (only the encoded era), so — like any real caller re-verifying a decoded
// transaction — they must be supplied again before build() can run.
(rebuiltBuilder as V8BatchStakingBuilder).sender({ address: stash }).validity({ firstValid, maxDuration: 64 });
const rebuiltTx = await rebuiltBuilder.build();
const rebuiltSignablePayload = rebuiltTx.signablePayload.toString('hex');

// Without the SI-1034 fix, this fails: the rebuild uses the static material (specVersion
// 8000000) instead of liveMaterial (specVersion 8000010), so the payloads differ.
should.equal(rebuiltSignablePayload, originalSignablePayload);
});
});
});

Expand Down
Loading