Skip to content

Typescript multisig upgrade#2758

Draft
theodorebugnet wants to merge 3 commits intotheodore/multisig-upgradefrom
theodore/typescript-multisig-upgrade
Draft

Typescript multisig upgrade#2758
theodorebugnet wants to merge 3 commits intotheodore/multisig-upgradefrom
theodore/typescript-multisig-upgrade

Conversation

@theodorebugnet
Copy link
Copy Markdown
Contributor

Description

Logic has some known bugs but initial draft to give an overview of the architecture

  • UnsignedTransaction is now UnsignedTransactionV0, with its three basic fields. The new UnsignedTransction type is the enum used for serializing for signing, as per the new rust implementation. (See below for some more thoughts on this.)
  • Remove payload from Multisig package, it's now only used to manage and aggregate signatures (similar to the rust Multisig type)
  • web3 now depends on multisig (and the web3 dependency in multisig is removed, which was only used for the integration test, which is moved into the integration-test package)
  • rollup in web3 gets extra methods for handling multisig signing specifically: signMultisigTransaction(unsignedTx, multisig, signer) (mutates the multisig) and submitMultisigTransaction(unsignedTx, multisig)

Some thoughts on the types: V0 and V1 both share the same basic fields that define a user-created transaction, and which are also included in both Transaction variants. It may or may not be worthwhile to define a special UnsignedTransactionPayload type or similar, with the semantics being that the "payload" is what users construct, then an UnsignedTransaction is the actual signed bytes (in the V0 case that's the payload + chain hash, in V1 that's payload + multisig credential + chain hash). Or keep UnsignedTransaction as the old type and introduce SigningPayload.
However, this change should probably be coordinated across both Rust and TS; right now I've kept the TS following my current Rust implementation. If we do rename, the functionality will mostly not change, so I think I'd rather go ahead with this PR and then consider the type ergonomics as a follow-up.

  • I have updated CHANGELOG.md with a new entry if my PR makes any breaking changes or fixes a bug. If my PR removes a feature or changes its behavior, I provide help for users on how to migrate to the new behavior.
  • I have carefully reviewed all my Cargo.toml changes before opening the PRs. (Are all new dependencies necessary? Is any module dependency leaked into the full-node (hint: it shouldn't)?)

Linked Issues

  • Fixes # (issue, if applicable)
  • Related to # (issue)

Testing

Describe how these changes were tested. If you've added new features, have you added unit tests?

Docs

Describe where this code is documented. If it changes a documented interface, have the docs been updated?

Rendered docs are available at https://sovlabs-ci-rustdoc-artifacts.us-east-1.amazonaws.com/<BRANCH_NAME>/index.html

@theodorebugnet theodorebugnet changed the title Initial refactor Typescript multisig upgrade Apr 20, 2026
import { hexToBytes } from "@sovereign-sdk/utils";
import * as borsh from "borsh";

const MAX_SIGNERS = 21;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this would be a useful export or a static property on Multisig

assertTxMatchesUnsignedTx(tx, this.unsignedTx);
addSignature(signature: HexString, pubKey: HexString): void;
addSignature(pair: SignatureAndPubKey): void;
addSignature(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Lets keep it simple and only have a single method signature, there's not much benefit to these overloads & it adds cognitive overhead

if (JSON.stringify(txAsUnsigned) !== JSON.stringify(unsignedTx)) {
throw new TransactionMismatchError();
seenPubKeys.add(pubKey);
hexToBytes(pubKey);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this is just being used for the side-effect of validating the pubkey is valid hex would be worth a comment. Or alternatively having a simple wrapper that throws a multisig exception so library consumers can still catch MultisigError and catch all potential errors raised by this lib


function assertIsNonceBasedTx(unsignedTx: UnsignedTransaction<unknown>): void {
if (!("nonce" in unsignedTx.uniqueness)) {
function assertDefined<T>(value: T | undefined): T {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this is intended to be a general assertion lets remove pubKey from the error message, or if its specifically for pubkeys maybe name it assertPubKeyDefined


export type StandardRollupSpec<RuntimeCall> = {
UnsignedTransaction: UnsignedTransaction<RuntimeCall>;
UnsignedTransaction: UnsignedTransactionV0<RuntimeCall>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this be UnsignedTransaction?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nope - the standard rollup lets users feed in an UnsignedTransactionV0 and internally wraps it in the V0: { ... } envelope for serialization by overriding the new unsignedTxForSigning() helper.

The type name is not super ergonomic but see my note on typing in the PR description - this works, but I'll see if I can introduce a separation between "the bytes serialized for signing" and "the payload the user assembles" (which are currently both represented by UnsignedTransaction, leading to awkward naming like this). This also affects the Rust side in the same way, so if I write a fix it'd be applied to both the rust and TS types.

return this.signVersionedUnsignedTx(signingUnsignedTx, signer);
}

async signMultisigTransaction(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be really nice if we didnt need to add these *multisig* method variants. It seems like a lot of the "transformation" methods could be defined separately - LMK thoughts or we can discuss when PR ready for real review

params: SolanaMultisigSubmitParams,
options?: SovereignClient.RequestOptions,
): Promise<SovereignClient.Sequencer.TxCreateResponse>;
async submitMultisigTransaction(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The overloads here are complex, lets see if we can simplify (or just remove the overloads preferably)

Base automatically changed from theodore/witnesless-hash to theodore/multisig-upgrade April 21, 2026 10:38
@theodorebugnet theodorebugnet force-pushed the theodore/typescript-multisig-upgrade branch from 93ff6ea to b1be85f Compare April 21, 2026 20:51
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.

2 participants