Skip to content

feat(sdk-coin-stx): migrate @stacks/transactions to v7.6.0 - #9994

Merged
Doddanna17 merged 2 commits into
masterfrom
SI-2024-bitgojs-stacks-transactions-v7
Oct 17, 2026
Merged

Doddanna17 merged 2 commits into
masterfrom
SI-2024-bitgojs-stacks-transactions-v7

Conversation

@Doddanna17

@Doddanna17 Doddanna17 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Context

@bitgo/sdk-coin-stx pinned @stacks/transactions 2.0.1. The v7 line is required to build the
SIP-044 staking post-condition (PostConditionType.Staking = 3) — the blocker for PoX-5
register-for-bond (SI-1936).

  • Ticket: SI-2024 (child of SI-2023 umbrella; related SI-2022, SI-2013, SI-1936).

Change

  • modules/sdk-coin-stx/package.json, yarn.lock: @stacks/transactions 2.0.1 → 7.6.0,
    @stacks/network ^4.3.0 → ^7.6.0.
  • Port sdk-coin-stx src + tests to the v7 API (renames: BufferReader → BytesReader,
    StacksTransaction → StacksTransactionWire, StacksMessageType → StacksWireType,
    makeSigHashPreSign → sigHashPreSign, publicKeyFromSignature → publicKeyFromSignatureVrs,
    makeStandard*PostCondition → Pc.*).
  • New src/lib/postConditions.ts: makeStakingPostCondition — the SIP-044 type-3 staking
    post-condition builder (the SI-1936 enabler). No other new exports; the module's export surface is
    otherwise unchanged.
  • Fix findContractTokenNameUsingContract (src/lib/utils.ts): filter() always returns a (truthy)
    array, so an unknown contract dereferenced sip10Token[0].assetId and threw a TypeError instead
    of returning undefined. Callers now get the intended invalid contract address or contract name, not supported. Same always-truthy pattern tidied in findTokenNameByContract.
  • Fail fast on an unresolved token name (fungibleTokenTransferBuilder): the builder now throws
    InvalidParameterValueError: unknown fungible token contract: <addr>.<name> instead of serialising
    a post-condition with assetName: "undefined" — which can never match and would only abort on-chain
    under Deny mode, burning the fee. (Verified by probe before/after.)

8 behavioural adaptations v7 requires (not just renames)

  1. contractBuilder: detect raw ClarityValues with a positive shape check (the v2 numeric-enum
    reverse lookup breaks under v7 string enums); a malformed JSON descriptor ({ type: 'list' }) still
    hits parseCv's validation.
  2. abstractContractBuilder.initBuilder: read clarity args from the raw payload, not toJson()'s
    stringified shape.
  3. Default an unset post-condition mode to Deny (v7 throws on an explicit undefined).
  4. sign() / appendOrigin(): bypass the stricter v7 TransactionSigner constructor.
  5. Reset the multisig auth-field list that v7 pre-populates in makeUnsigned*, for byte parity.
  6. appendOrigin appends the origin pubkey as given (v2 semantics); initBuilder normalises the
    deserialized pubkey to compressed — keeps the auth-field bytes identical to v2 and avoids duplicate
    entries on a rebuild.
  7. isValidMemo(): reject non-string memos explicitly (v7's exceedsMaxLengthBytes no longer throws
    on non-strings).
  8. Remove dead _anchorMode / ANCHOR_MODE (anchorMode was dropped from the options).

⚠️ BREAKING CHANGE — toJson() / explainTransaction shape

@stacks/transactions v7 changed the Clarity representation, so the JSON returned by sdk-coin-stx
toJson() / explainTransaction (payload.functionArgs) changes shape:

v2 v7
{ type: 1, value } (numeric tag) { type: 'uint', value } (string tag)
{ type: 5, address: { version, hash160 } } { type: 'address', value: <c32 address> } (prefix varies by network/version: SP/ST/SN/SM)
{ type: 2, buffer } { type: 'buffer', value: <hex> }

Tuple key data and list key list are preserved. Serialized transaction bytes are unchanged
(byte-parity preserved) — signing and the HSM are unaffected; this is a JSON/API-shape change only.
One pre-existing exception, now pinned by a test: rebuilding an uncompressed-key multisig tx
re-derives the sender from the compressed keys recovered on deserialization (v2 did the same).

Consumer expectations (recorded for the breaking change):

  • Wallet Platform — adapted to the v7 shapes (SI-2025 PR #63608).
  • policy service — no change: it evaluates coin/amount/address facts, not Clarity tags.
  • staking-service — no change: it sends hex-encoded args, doesn't read this JSON.
  • OVC — no change (verified): its explain/blind-signing path has no Stacks/STX guard and does not
    read functionArgs; only its STX test imports v2 @stacks symbols (tracked in SI-2026).
  • HSM — no change: consumes serialized bytes (wire Clarity type bytes are numeric & unchanged;
    hsm-firmware/src/bg/stx.cc already has POST_CONDITION_TYPE_STAKING = 3) — verify-only.

Commits

SHA subject contents
754db4e2f1 feat(sdk-coin-stx): migrate @stacks/transactions to v7.6.0 dep bump + v7 port + makeStakingPostCondition + findContractTokenNameUsingContract / fail-fast fixes (review follow-ups folded in)
8eb89e683b test(sdk-coin-stx): cover builders, recovery and validators module-wide coverage uplift (builders, recovery, validators, token-name + sBTC-transfer + uncompressed-multisig v2 parity)

Tests

  • stx.pox5RegisterForBond-style module coverage extended; new: pox5Builder builds
    register-for-bond with PostConditionMode.Deny + a type-3 staking post-condition
    (makeStakingPostCondition), serializes, rebuilds via factory.from(hex), and asserts
    byte-identical output with the post-condition restored (the initBuilder restore path).
  • makeStakingPostCondition unit-tested across all comparators — eq/gt/gte/lt/lte as both
    wire strings and FungibleConditionCode, all three amount forms (string/number/bigint), and the
    unsupported-code rejection.
  • Token-name resolution covered: a known contract resolves to its SIP-10 name, an unknown one returns
    undefined, building a token transfer for it fails with the intended
    invalid contract address or contract name, not supported (the guard the fix made reachable), and
    the fungible-token builder fails the build (unknown fungible token contract) rather than emitting
    an undefined asset name.
  • sBTC token transfer covered (previously untested — the generic SIP-10 path was only exercised via
    tstx:tsip6dp): tstx:tsbtc / stx:sbtc transfers resolve the token name from statics and emit a
    <contract>.sbtc-token::sbtc-token post-condition, byte-identical across a factory.from(hex)
    rebuild. Note this is the SIP-10 transfer path; the separate sBTC withdrawal
    (initiate-withdrawal-request) and sBTC register-for-bond lockup flows were already covered.
  • Multisig key encodings pinned against v2.0.1-generated fixtures (closes the reviewer's non-blocking ask):
    the uncompressed build + appendOrigin bytes match v2 exactly; a compressed-key multisig tx
    verifyOrigin()s and round-trips byte-identically; and the uncompressed-key rebuild/verification
    limitation is asserted (pre-existing and identical in v2.0.1, v6.17.0 and v7.6.0) rather than assumed.
  • Uncompressed-key multisig pinned against a v2.0.1-generated fixture (build + appendOrigin;
    unsigned and 2-of-3 signed) — closes the reviewer's non-blocking ask. The from(hex) rebuild of such
    a tx re-derives the sender from the compressed keys recovered on deserialization; that is a
    pre-existing limitation (v2 behaves identically — its deserialized signature fields recover the
    compressed key and the appended origin field deserializes to a 33-byte key), asserted explicitly
    rather than left implicit.
  • Module suite: 328 passing / 0 failing — the v7 migration suites plus a module-wide coverage
    uplift (registration, coin metadata, builder validators, transfer/sendmany/fungible-token/contract
    builders, sBTC-withdraw deserialization guards, PoX-5 rewards, the tx factory, tx error branches,
    key pair, BTC address decoding, and the stx node-request/recovery paths).

Verification (local)

  • tsc --build . (src + test): 0 errors.
  • eslint --quiet .: 0 errors; prettier --check: clean.
  • mocha: 328 passing / 0 failing.
  • Coverage (c8, src/**): 98.98% stmts / 91.27% branch / 99.07% funcs (migration baseline was
    93.5% / 81.3%); lib/postConditions.ts is 100% stmts / 100% funcs / 100% lines. The remaining
    gaps are closing-brace artifacts and unreachable defensive guards (e.g. the tstx/tstxsignet
    constructor checks behind the Stx super call, the duplicate total-amount re-check, and
    signTransaction's !transaction guard). Note: the module's stock yarn coverage nyc transform
    does not instrument TS in this environment, so the numbers above are measured with c8.
  • Consumer build (with dependency chain): @bitgo/account-lib, bitgo, @bitgo/web-demo — success.
  • Cross-repo (local, pre-publish): packed as @bitgo-beta/sdk-coin-stx and run inside WP → WP
    tsc -b 0 errors + all STX/SIP-10 suites green (see SI-2025).

Review follow-ups (folded into 754db4e2f1)

Finding Status
(1) appendOrigin compressing pubkeys can change the multisig redeem script for uncompressed keys Reverted to append as given (v2 semantics); compressed normalisation moved to initBuilder; existing uncompressed-multisig round-trip test covers it
(2) isClarityValue negative heuristic lets malformed JSON through Positive shape check (value, or a valueless CV); test pins { type: 'list' } / { type: 'tuple' } → InvalidParameterValueError
(3) dead _anchorMode / ANCHOR_MODE Removed
(4) postConditions.ts compat-style surface + Pc re-export Trimmed to only makeStakingPostCondition; Pc re-export dropped
(5) deprecated.ts re-uses v2 names returning v7 types Not added (was removed); export surface unchanged
(6) toJson()/explain shape change undocumented Documented as a BREAKING CHANGE (+ consumer expectations above)
(7) OVC "confirm if any surface reads it" Closed — OVC's explain/blind-signing path has no Stacks/STX guard; it doesn't read functionArgs (only its STX test imports v2 @stacks symbols → SI-2026)
nits import ordering (ContractCallPayload), transactionVersionToNetworkName moved above the JSDoc, non-Testnet→mainnet comment added

Related

  • SI-2024 (this), SI-2023 (umbrella), SI-2022, SI-2013, SI-1936, SI-1925 (WP beta bump), SI-2026
    (OVC), SI-2028 (HSM verify-only).

@linear-code

linear-code Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

SI-2024

SI-2025

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Unit tests are failing on Node 26.x (Current release line, non-blocking). This is not an LTS version yet, so it does not block merge, but it signals an incompatibility to fix before Node 26.x becomes LTS.

View run

@Doddanna17
Doddanna17 force-pushed the SI-2024-bitgojs-stacks-transactions-v7 branch from bd449c4 to ce4cf4d Compare October 16, 2026 18:23
@Vijay-Jagannathan
Vijay-Jagannathan force-pushed the SI-2024-bitgojs-stacks-transactions-v7 branch from ce4cf4d to 990fdcc Compare October 16, 2026 20:42
@Doddanna17
Doddanna17 force-pushed the SI-2024-bitgojs-stacks-transactions-v7 branch 2 times, most recently from 11e8c38 to 7aa96cc Compare October 17, 2026 05:04
@Doddanna17
Doddanna17 marked this pull request as ready for review October 17, 2026 05:05
@Doddanna17
Doddanna17 requested review from a team as code owners October 17, 2026 05:05

@Vijay-Jagannathan Vijay-Jagannathan 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.

Solid, well-scoped migration. The rename mapping is correct, the v7 behavioural adaptations are explained inline, and the register-for-bond → from(hex) byte-identity test covers the riskiest path. CI is green (22/24/26). Not approving yet because of one concern that touches signing bytes, plus a few smaller items.

1. Compressing appended origin pubkeys can change the multisig redeem script (needs verification)

Transaction.appendOrigin now always does appendPubkey(compressPublicKey(pubKey)).

  • v2 appendPubkey kept the key as given (isCompressed(publicKey) → tagged Compressed/Uncompressed, serialized as 33 or 65 bytes).
  • The spending-condition signer (hash160 of the P2SH redeem script) is computed in makeUnsigned* from this._fromPubKeys as given. getSTXAddressFromPubKeys / isValidPublicKey accept uncompressed keys.
  • If a multisig is built with uncompressed fromPubKey(...) values, the signer is hashed over the 65-byte keys but the auth field now carries the 33-byte key. The node re-derives the redeem script from the auth fields, the hash no longer matches signer, and the tx is rejected.

The PR claims "serialized bytes unchanged", but this is a byte change for uncompressed-key multisig. All multisig tests I found use getKeys(true).pub (compressed), so they can't catch it.

Please either:

  • add a test with uncompressed pubkeys in a multisig (build → appendOrigin → from(hex) → compare against a v2-generated fixture), or
  • compress only when the key was already compressed, and instead fix the rebuild-duplication problem on the deserialize side (initBuilder reads field.contents.data → normalise there).

If WP/BitGo flows can only ever supply compressed keys, say so in the comment and in fromPubKey validation so this is an enforced invariant rather than an accident.

2. ContractBuilder.isClarityValue is a negative heuristic

type ∈ ClarityType values && !('val' in arg). A JSON descriptor missing val for a type that is also a ClarityType name ({ type: 'list' }, { type: 'tuple' }, { type: 'buffer' }) is now passed through as a "raw ClarityValue" instead of raising list requires Array val / tuple require Array val. The invalid CV then fails later (serialize) with a worse error. Prefer a positive check on the CV shape ('value' in arg, or a isClarityValue guard based on the v7 shapes) so malformed JSON still hits parseCv's validation. A test for { type: 'list' } → InvalidParameterValueError would pin it.

3. Dead code left by the cutover

_anchorMode (transactionBuilder.ts) and ANCHOR_MODE (constants.ts) are no longer read anywhere now that anchorMode is dropped from the options. Remove them (clean cutover). Also check ANCHOR_MODE isn't re-exported/consumed by another module.

4. New public API in postConditions.ts

export * from './postConditions' adds makeStxPostCondition, makeFungiblePostCondition, assetInfo, Pc, PostConditionCode, AssetInfo, … to the package surface. The doc comments call them "replacements for removed v2 helpers", but they never existed in BitGoJS' exports. Only makeStakingPostCondition is needed for SI-1936, and the in-repo builders already use Pc directly. Consider exporting only what has a caller (staking), or trim the docs so they don't read as a compat layer. Re-exporting Pc also pins a transitive library's API into ours.

5. Breaking-change handling

toJson() / explainTransaction shape change is documented well. Two follow-ups:

  • This is feat: with a BREAKING CHANGE: footer, so it will cut a major for @bitgo/sdk-coin-stx and cascade to account-lib/bitgo. Confirm that's intended and that WP #63608 is merged/released in lock-step (consumers pinned to the old shape will break on bump).
  • The "OVC — confirm if any surface reads it" item in the description is still open. Please close it before merge.

Nits

  • sbtcWithdrawBuilder.ts / sendmanyBuilder.ts: ContractCallPayload is appended after the value imports; move it alongside the other type imports.
  • utils.ts: transactionVersionToNetworkName was inserted between getAddressFromPublicKeyHash and its JSDoc block, which now documents the wrong function. Move the helper above the JSDoc.
  • keyPair.getSTXAddress: any non-Testnet TransactionVersion silently maps to mainnet; fine for the two enum values, but worth a comment.

Positive: the lockfile dedupes to a single @stacks/* v7 line (utxo-descriptors already used it) and drops @stacks/common v1/v2/v4, c32check and cross-sha256, which is a good side effect.

@Doddanna17
Doddanna17 force-pushed the SI-2024-bitgojs-stacks-transactions-v7 branch 3 times, most recently from 67e234a to 22b119a Compare October 17, 2026 05:56
Bump @stacks/transactions 2.0.1 -> 7.6.0 (and @stacks/network ^4.3.0 -> ^7.6.0).
v7 is required to build the SIP-044 staking post-condition
(PostConditionType.Staking = 3), the blocker for PoX-5 register-for-bond
(SI-1936).

Port the sdk-coin-stx src + tests to the v7 API, keeping serialized
transactions byte-identical. v7 renames (BufferReader -> BytesReader,
StacksTransaction -> StacksTransactionWire, StacksMessageType ->
StacksWireType, sigHashPreSign, publicKeyFromSignatureVrs) and moves most
post-condition builders to the `Pc` builder namespace.

Behavioural adaptations required by v7 (not just renames):
- contractBuilder: detect raw ClarityValues with a positive shape check (the v2
  numeric-enum reverse lookup breaks under v7 string enums); a malformed JSON
  descriptor still hits `parseCv`'s validation.
- abstractContractBuilder.initBuilder: read clarity args from the raw payload
  instead of toJson()'s stringified shape.
- Default an unset post-condition mode to Deny (v7 throws on explicit undefined).
- sign()/appendOrigin(): avoid the stricter v7 TransactionSigner constructor.
- Reset the multisig auth-field list that v7 pre-populates in makeUnsigned*, and
  append the origin pubkey as given (v2 semantics) with the deserialized pubkey
  normalised to compressed in initBuilder — keeps the auth-field bytes identical
  to v2 and avoids duplicate entries on a rebuild.
- isValidMemo: reject non-string memos explicitly (v7's exceedsMaxLengthBytes no
  longer throws on non-strings).
- Remove dead `_anchorMode` / `ANCHOR_MODE` (anchorMode was dropped from options).

Add `src/lib/postConditions.ts` with `makeStakingPostCondition` (the SIP-044
type-3 staking post-condition builder, the SI-1936 enabler). No other new exports
are added — the module's export surface is otherwise unchanged.

Also fix `findContractTokenNameUsingContract`: its guard was
`sip10Token ? sip10Token[0].assetId.split('::')[1] : undefined`, but `filter()`
always returns an array — and an empty array is truthy — so an unknown contract
dereferenced `sip10Token[0].assetId` and threw a TypeError instead of returning
`undefined`. Callers now get the intended 'invalid contract address or contract
name, not supported' error. The same always-truthy pattern is tidied in
`findTokenNameByContract` (harmless there, but identical intent).

The fungible-token builder also fails fast on an unresolved token name: it now
throws an InvalidParameterValueError instead of serialising an 'undefined' asset
name, which could never match a Deny-mode post-condition and would only abort
on-chain, burning the fee.

Tests: `stx.pox5RegisterForBond`-style module coverage plus a `pox5Builder` test
that builds register-for-bond with `PostConditionMode.Deny` + a type-3 staking
post-condition, serializes, rebuilds via `factory.from(hex)` and asserts
byte-identical output.

BREAKING CHANGE: @stacks/transactions v7 changes the Clarity representation, so
the JSON returned by sdk-coin-stx `toJson()` / `explainTransaction`
(`payload.functionArgs`) changes shape: Clarity type tags are now strings
(`'uint'` vs `1`) and principal/buffer values use the v7 form
(`{ type: 'address', value: <c32 address> }` — prefix varies by network/version,
e.g. SP/ST/SN/SM; `{ type: 'buffer', value: <hex> }`) instead of v2
(`{ type: 5, address: { version, hash160 } }`, `{ type: 2, buffer }`). Consumers
that read `functionArgs` (Wallet Platform, policy service, OVC) must expect the
v7 shape. Serialized transaction bytes are unchanged (byte-parity preserved) —
signing and the HSM are unaffected.

Ticket: SI-2024
@Doddanna17
Doddanna17 force-pushed the SI-2024-bitgojs-stacks-transactions-v7 branch from 22b119a to 83d6e3c Compare October 17, 2026 06:01

@Vijay-Jagannathan Vijay-Jagannathan 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.

Re-reviewed 83d6e3c32a against my earlier comments. Every code finding is addressed. Approving.

Previous findings

  1. Uncompressed multisig keys: fixed. appendOrigin appends the key as given again. I checked both libs: v2 serializeTransactionAuthField and v7 serializeTransactionAuthFieldBytes write the uncompressed tag plus the 33-byte compressed key. So the auth-field bytes match v2. Compression now happens only in initBuilder on the rebuild path, where v2 also read back a 33-byte key. The explanation in the code comment is accurate.
  2. isClarityValue: fixed. It is now a positive shape check (value present, or one of the valueless none/true/false). The new test pins that { type: 'list' } and { type: 'tuple' } throw InvalidParameterValueError.
  3. Dead code: fixed. _anchorMode and ANCHOR_MODE are removed, with no remaining references in modules/.
  4. Public API: fixed. postConditions.ts is trimmed to makeStakingPostCondition plus its comparator and amount types. Pc, assetInfo and the STX/FT helpers are no longer exported.
  5. Nits: fixed. The imports are reordered, the utils.ts helper no longer sits between a function and its JSDoc, and getSTXAddress has an explanatory comment.

Fixes beyond my review (good catches)

  • findTokenNameByContract and findContractTokenNameUsingContract tested arr ? arr[0] : undefined. An empty array is truthy, so the second one threw on [0].assetId. Both now check .length.
  • FungibleTokenTransferBuilder now throws InvalidParameterValueError for an unknown token contract instead of serialising an undefined asset name. That post-condition could never match, so a Deny-mode tx would abort on-chain and still burn the fee. This is a small behaviour change, but the right one.

Non-blocking, please consider

  • There is still no test that builds a multisig from uncompressed pubkeys through build → appendOrigin → from(hex) and compares the bytes to a v2-generated fixture. The code now looks right to me on a read of both libs, but a fixture would pin it.
  • The BREAKING CHANGE footer will trigger a major bump for sdk-coin-stx, and its consumers (account-lib, bitgo) need to be bumped with it. Before merging, confirm the WP #63608 release ordering and close the "OVC reads functionArgs?" item in the description.
  • CI: Node 26, browser-test, docker-build and the lint, format and dependencies jobs are green. The unit-test jobs for Node 22 and 24 and "Internal Checks" were still pending when I reviewed. Please wait for them before merging.

@Vijay-Jagannathan Vijay-Jagannathan 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.

Re-approving after the force-push dismissed my earlier approval.

Delta since the head I approved (83d6e3c32a → dc7c1b869c): the source commit 754db4e2f1 is byte-for-byte unchanged. The test commit only adds:

  • sBTC transfer tests for tstx:tsbtc and stx:sbtc: token name resolved from statics, sbtc-token::sbtc-token post-condition, byte-identical rebuild via factory.from(hex).
  • Two findContractTokenNameUsingContract assertions for the sBTC contracts.

These exercise a real path (statics-resolved asset name on the new Pc.ft post-condition) and the rebuild check is the right invariant. No new concerns.

My earlier non-blocking notes still stand (uncompressed-key multisig fixture vs v2; major-bump/WP #63608 ordering; close the OVC item). CI was fully re-queued on the new head, so please wait for unit-test (22/24/26) and Internal Checks to go green before merging.

Add unit tests for the branches left uncovered across the module: coin
registration and base-coin methods, transaction builder validators,
transfer/sendmany/fungible-token/contract builders, the sBTC withdraw
deserialization guards, PoX-5 calculate/claim rewards, the transaction
builder factory, transaction error branches, key pair, btc address
decoding, sip10 token metadata and the stx node-request/recovery paths.

Also pins the token-name lookup fixed in the migration commit: a contract
that is not a known SIP-10 token resolves to undefined instead of throwing
a TypeError, building a token transfer for it fails with the intended
'invalid contract address or contract name, not supported', and the
fungible-token builder now fails the build rather than serialising an
'undefined' asset name.

Closes the sBTC token-transfer gap (tstx:tsbtc / stx:sbtc resolve from
statics and emit a sbtc-token::sbtc-token post-condition, byte-identical
across a factory.from(hex) rebuild).

Pins the multisig key encodings against v2.0.1-generated fixtures: the
uncompressed build + appendOrigin bytes match v2 exactly, a compressed-key
multisig tx verifies and round-trips byte-identically, and the
uncompressed-key rebuild/verification limitation is asserted (pre-existing
and identical in v2.0.1, v6.17.0 and v7.6.0) instead of assumed.

Test/coverage status vs the pre-upgrade baseline (module):
- tests: ~226 -> 328 passing; test files 16 -> 21
- coverage (c8, src/**): ~93.5% stmts / ~81.3% branch -> 98.98% stmts /
  91.27% branch / 99.07% funcs

Ticket: SI-2024
@Doddanna17
Doddanna17 force-pushed the SI-2024-bitgojs-stacks-transactions-v7 branch from 52762ac to 8eb89e6 Compare October 17, 2026 07:10
@Doddanna17

Copy link
Copy Markdown
Contributor Author

v2 → v7 verification summary (old vs new), and one pre-existing anomaly

Rather than trust the fixtures alone, I installed the real old SDKs in scratch dirs (@stacks/transactions 2.0.1 and 6.17.0) and ran the same inputs through them, then compared against this branch (7.6.0).

Build output — identical

case old (2.0.1) new (7.6.0)
single-sig transfer (compressed) SIGNED_TRANSACTION fixture byte-identical
multisig 2-of-3 (compressed, + appendOrigin) MULTI_SIG_SIGNED_TRANSACTION byte-identical
contract calls / send-many / fungible / sBTC withdraw / recovery v2 fixtures byte-identical
multisig from uncompressed keys cb97b1df… byte-identical to a v2.0.1-generated fixture (6.17.0 also produces the same 572-byte hex)

Behaviour parity

Multisig auth-field pre-population (cleared to match v2), appendOrigin encoding (uncompressed tag + 33-byte compressed key), unset postConditionMode → Deny, the stricter v7 TransactionSigner (worked around), and the rebuild key form all match the old behaviour. The only capability difference is the new SIP-044 type-3 staking post-condition (makeStakingPostCondition), which v2 cannot express.

Intended differences (behaviour changes in this PR)

  • toJson() / explainTransaction JSON: numeric Clarity tags → string tags, principal/buffer shapes → the documented BREAKING CHANGE. Serialized bytes are unaffected.
  • findContractTokenNameUsingContract with an unknown contract: previously TypeError: Cannot read properties of undefined (reading 'assetId'), now returns undefined so callers raise the intended invalid contract address or contract name, not supported.
  • Fungible-token builder with an unknown token: previously serialized assetName: "undefined" (a post-condition that can never match → on-chain abort, fee burned), now fails the build with InvalidParameterValueError.

One anomaly — pre-existing, not introduced here

Uncompressed-key multisig produces a tx that does not verify. The signer hash is derived from the uncompressed keys, while the auth fields only carry the compressed key bytes, so a re-derivation from the tx's own fields can never match the signer. Identical across versions:

version build signer verifies
2.0.1 cb97b1df… no (recovered 2fe5…)
6.17.0 cb97b1df… no (Signer hash does not equal hash of public key(s))
7.6.0 cb97b1df… no (same error)

Compressed keys (what BitGo wallets use — xpub-derived, and sign() uses the compressed form) build, verify and round-trip byte-identically; existing transactions are untouched. The rebuild of an uncompressed-key tx also re-derives the compressed sender in all three versions, so nothing regressed there either.

Suggested follow-up (separate ticket, deliberately out of scope here): fail fast on uncompressed keys for multisig builds so this path errors instead of emitting an unverifiable tx. The alternative — normalising to compressed — would change those multisig addresses, so it needs a product decision first.

Tests / coverage (module)

  • tests: ~226 → 328 passing; test files 16 → 21
  • coverage (c8, src/**): ~93.5% stmts / ~81.3% branch → 98.98% stmts / 91.27% branch / 99.07% funcs

The two new multisig tests (compressed verification + the uncompressed limitation) landed in the test commit; CI is re-running on the new head.

@Vijay-Jagannathan Vijay-Jagannathan 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.

Re-approving (8eb89e683b); the force-push dismissed the previous approval. The source commit 754db4e2f1 is unchanged, and the test commit touches no src/ files.

The new tests answer my last open note, the uncompressed-key multisig check against v2:

  • v2 fixtures. UNCOMPRESSED_MULTISIG_UNSIGNED_TRANSFER and ..._SIGNED_TRANSFER were generated with @stacks/transactions 2.0.1. The uncompressed build + appendOrigin path matches them byte-for-byte, so the "append as given" fix is now pinned rather than argued from reading the code.
  • Known limitation. Uncompressed-key multisig txs don't self-verify (signer hash over 65-byte keys, auth fields carry 33-byte keys) and a from(hex) rebuild isn't byte-identical. The tests assert this is pre-existing and v2-identical. That corrects my first review: this is not a regression from the migration.
  • Compressed-key multisig. It is covered by a verify-and-rebuild-byte-identical test.

Nit: the two "limitation" tests pin behaviour that is arguably a latent bug, so they will need updating if it is ever fixed. That's fine as documented, but consider a follow-up ticket for uncompressed-key multisig handling.

Remaining non-blocking items from earlier: confirm the major-bump and WP #63608 release ordering, and close the OVC item in the description. CI was re-queued on this head, so wait for unit-test (22/24/26) and Internal Checks before merging.

@diksha190 diksha190 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approved. SafeChain passed, Socket 92 (deep = shallow, no dependencies, no high/critical alerts).
Package: [email protected].

@Doddanna17
Doddanna17 merged commit 91eeaac into master Oct 17, 2026
26 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.

4 participants