Multi-Signature Transactions
The IOTA TypeScript SDK provides a MultiSigPublicKey
class to support
Multi-Signature (MultiSig)
transaction and personal message signing.
This class implements the same interface as the PublicKey
classes that Keypairs uses
and you call the same methods to verify signatures for PersonalMessages
and Transactions
.
Creating a MultiSigPublicKey
To create a MultiSigPublicKey
, you provide a threshold
(u16) value and an array of objects that
contain publicKey
and weight
(u8) values. If the combined weight of valid signatures for a
transaction is equal to or greater than the threshold value, then the IOTA network considers the
transdaction valid.
import { Ed25519Keypair } from '@iota/iota-sdk/keypairs/ed25519';
import { MultiSigPublicKey } from '@iota/iota-sdk/multisig';
const kp1 = new Ed25519Keypair();
const kp2 = new Ed25519Keypair();
const kp3 = new Ed25519Keypair();
const multiSigPublicKey = MultiSigPublicKey.fromPublicKeys({
threshold: 2,
publicKeys: [
{
publicKey: kp1.getPublicKey(),
weight: 1,
},
{
publicKey: kp2.getPublicKey(),
weight: 1,
},
{
publicKey: kp3.getPublicKey(),
weight: 2,
},
],
});
const multisigAddress = multiSigPublicKey.toIotaAddress();
The multiSigPublicKey
in the preceding code enables you to verify that signatures have a combined
weight of at least 2
. A signature signed with only kp1
or kp2
is not valid, but a signature
signed with both kp1
and kp2
, or just kp3
is valid.
Combining signatures with a MultiSigPublicKey
To sign a message or transaction for a MultiSig address, you must collect signatures from the
individual key pairs, and then combine them into a signature using the MultiSigPublicKey
class for
the address.
// This example uses the same imports, key pairs, and multiSigPublicKey from the previous example
const message = new TextEncoder().encode('hello world');
const signature1 = (await kp1.signPersonalMessage(message)).signature;
const signature2 = (await kp2.signPersonalMessage(message)).signature;
const combinedSignature = multiSigPublicKey.combinePartialSignatures([signature1, signature2]);
const isValid = await multiSigPublicKey.verifyPersonalMessage(message, combinedSignature);
Creating a MultiSigSigner
The MultiSigSigner
class allows you to create a Signer that can be used to sign personal messages
and Transactions like any other keypair or signer class. This is often easier than manually
combining signatures, since many methods accept Signers and handle signing directly.
A MultiSigSigner
is created by providing the underlying Signers to the getSigner
method on the
MultiSigPublicKey
. You can provide a subset of the Signers that make up the public key, so long as
their combined weight is equal to or greater than the threshold.
import { Ed25519Keypair } from '@iota/iota-sdk/keypairs/ed25519';
import { MultiSigPublicKey } from '@iota/iota-sdk/multisig';
const kp1 = new Ed25519Keypair();
const kp2 = new Ed25519Keypair();
const multiSigPublicKey = MultiSigPublicKey.fromPublicKeys({
threshold: 1,
publicKeys: [
{
publicKey: kp1.getPublicKey(),
weight: 1,
},
{
publicKey: kp2.getPublicKey(),
weight: 1,
},
],
});
const signer = multiSigPublicKey.getSigner(kp1);
const message = new TextEncoder().encode('hello world');
const { signature } = await signer.signPersonalMessage(message);
const isValid = await multiSigPublicKey.verifyPersonalMessage(message, signature);
Multisig with zkLogin
You can use zkLogin to participate in multisig just like keys for other signature schemes. Unlike other keys that come with a public key, you define a public identifier for zkLogin.
For example, the following example creates a 1-out-of-2 multisig with a single key and a zkLogin public identifier:
// a single Ed25519 keypair and its public key.
const kp1 = new Ed25519Keypair();
const pkSingle = kp1.getPublicKey();
// compute the address seed based on user salt and jwt token values.
const decodedJWT = decodeJwt('a valid jwt token here');
const userSalt = BigInt('123'); // a valid user salt
const addressSeed = genAddressSeed(userSalt, 'sub', decodedJwt.sub, decodedJwt.aud).toString();
// a zkLogin public identifier derived from an address seed and an iss string.
let pkZklogin = toZkLoginPublicIdentifier(addressSeed, decodedJwt.iss);
// derive multisig address from multisig public key defined by the single key and zkLogin public
// identifier with weight and threshold.
const multiSigPublicKey = MultiSigPublicKey.fromPublicKeys({
threshold: 1,
publicKeys: [
{ publicKey: pkSingle, weight: 1 },
{ publicKey: pkZklogin, weight: 1 },
],
});
// this is the sender of any transactions from this multisig account.
const multisigAddress = multiSigPublicKey.toIotaAddress();
// create a regular zklogin signature from the zkproof and ephemeral signature for zkLogin.
// see zklogin-integration.mdx for more details.
const zkLoginSig = getZkLoginSignature({
inputs: zkLoginInputs,
maxEpoch: '2',
userSignature: fromB64(ephemeralSig),
});
// a valid multisig with just the zklogin signature.
const multisig = multiSigPublicKey.combinePartialSignatures([zkLoginSig]);
Benefits and Design for zkLogin in Multisig
Because zkLogin assumes the application client ID and its issuer (such as Google) liveliness, using zkLogin with multisig provides improved recoverability to a zkLogin account. In the previous example of 1-out-of-2 multisig, users can use zkLogin in their regular wallet flow, but if the application or the issuer is deprecated, the user can still use the regular private key account to access funds in the multisig wallet.
This also opens the door to design multisig across any number of zkLogin accounts and of different providers (max number is capped at 10 accounts) with customizable weights and thresholds. For example, you can set up a multisig address with threshold of 2, where the public keys or identifiers are defined as:
- Charlie's own Google account with weight 2
- Charlie's friend Alice's Apple account with weight 1
- Charlie's friend Bob's Facebook account with weight 1
In this case, Charlie can always use their Google account for transactions out of the multisig address for the threshold. At the same time, Charlie still has access to his account by combining partial signatures from Alice and Bob.