Skip to content

Commit

Permalink
Add MultiEd25519 account support (#627)
Browse files Browse the repository at this point in the history
* Add MultiEd25519 account support

* update CL

* add MultiEd25519Account to index

* Update CHANGELOG.md

* revert only
  • Loading branch information
heliuchuan authored Feb 10, 2025
1 parent 5584c07 commit e8f771a
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 83 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T

# Unreleased

- Add `MultiEd25519Account` to support the legacy MultiEd25519 authentication scheme.

# 1.34.0 (2025-02-06)

- Add new `scriptComposer` api in `transactionSubmission` api to allow SDK callers to invoke multiple Move functions inside a same transaction and compose the calls dynamically.
Expand Down
154 changes: 154 additions & 0 deletions src/account/MultiEd25519Account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { AccountAddress, AccountAddressInput } from "../core/accountAddress";
import { Ed25519PrivateKey } from "../core/crypto";
import { MultiEd25519PublicKey, MultiEd25519Signature } from "../core/crypto/multiEd25519";
import { AccountAuthenticatorMultiEd25519 } from "../transactions/authenticator/account";
import { generateSigningMessageForTransaction } from "../transactions/transactionBuilder/signingMessage";
import { AnyRawTransaction } from "../transactions/types";
import { HexInput, SigningScheme } from "../types";
import type { Account } from "./Account";

export interface MultiEd25519SignerConstructorArgs {
publicKey: MultiEd25519PublicKey;
signers: Ed25519PrivateKey[];
address?: AccountAddressInput;
}

export interface VerifyMultiEd25519SignatureArgs {
message: HexInput;
signature: MultiEd25519Signature;
}

/**
* Signer implementation for the Multi-Ed25519 authentication scheme.
*
* Note: This authentication scheme is a legacy authentication scheme. Prefer using MultiKeyAccounts as a
* MultiKeyAccount can support any type of signer, not just Ed25519. Generating a signer instance does not
* create the account on-chain.
*/
export class MultiEd25519Account implements Account {
readonly publicKey: MultiEd25519PublicKey;

readonly accountAddress: AccountAddress;

readonly signingScheme = SigningScheme.MultiEd25519;

/**
* The signers used to sign messages. These signers should correspond to public keys in the
* MultiEd25519Account. The number of signers should be equal to this.publicKey.threshold.
* @group Implementation
* @category Account (On-Chain Model)
*/
readonly signers: Ed25519PrivateKey[];

/**
* An array of indices where for signer[i], signerIndicies[i] is the index of the corresponding public key in
* publicKey.publicKeys. Used to derive the right public key to use for verification.
* @group Implementation
* @category Account (On-Chain Model)
*/
readonly signerIndices: number[];

readonly signaturesBitmap: Uint8Array;

// region Constructors

constructor(args: MultiEd25519SignerConstructorArgs) {
const { signers, publicKey, address } = args;
this.publicKey = publicKey;
this.accountAddress = address ? AccountAddress.from(address) : this.publicKey.authKey().derivedAddress();

if (publicKey.threshold > signers.length) {
throw new Error(
// eslint-disable-next-line max-len
`Not enough signers provided to satisfy the required signatures. Need ${publicKey.threshold} signers, but only ${signers.length} provided`,
);
} else if (publicKey.threshold < signers.length) {
throw new Error(
// eslint-disable-next-line max-len
`More signers provided than required. Need ${publicKey.threshold} signers, but ${signers.length} provided`,
);
}

// For each signer, find its corresponding position in the public keys array
const bitPositions: number[] = [];
for (const signer of signers) {
bitPositions.push(this.publicKey.getIndex(signer.publicKey()));
}

// Create pairs of [signer, position] and sort them by position
// This sorting is critical because:
// 1. The on-chain verification expects signatures to be in ascending order by bit position
// 2. The bitmap must match the order of signatures when verifying
const signersAndBitPosition: [Ed25519PrivateKey, number][] = signers.map((signer, index) => [
signer,
bitPositions[index],
]);
signersAndBitPosition.sort((a, b) => a[1] - b[1]);

// Extract the sorted signers and their positions into separate arrays
this.signers = signersAndBitPosition.map((value) => value[0]);
this.signerIndices = signersAndBitPosition.map((value) => value[1]);

// Create a bitmap representing which public keys from the MultiEd25519PublicKey are being used
// This bitmap is used during signature verification to identify which public keys
// should be used to verify each signature
this.signaturesBitmap = this.publicKey.createBitmap({ bits: bitPositions });
}

// endregion

// region Account

/**
* Verify the given message and signature with the public key.
*
* @param args.message raw message data in HexInput format
* @param args.signature signed message Signature
* @returns
*/
verifySignature(args: VerifyMultiEd25519SignatureArgs): boolean {
return this.publicKey.verifySignature(args);
}

/**
* Sign a message using the account's Ed25519 private key.
* @param message the signing message, as binary input
* @return the AccountAuthenticator containing the signature, together with the account's public key
*/
signWithAuthenticator(message: HexInput): AccountAuthenticatorMultiEd25519 {
return new AccountAuthenticatorMultiEd25519(this.publicKey, this.sign(message));
}

/**
* Sign a transaction using the account's Ed25519 private keys.
* @param transaction the raw transaction
* @return the AccountAuthenticator containing the signature of the transaction, together with the account's public key
*/
signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticatorMultiEd25519 {
return new AccountAuthenticatorMultiEd25519(this.publicKey, this.signTransaction(transaction));
}

/**
* Sign the given message using the account's Ed25519 private keys.
* @param message in HexInput format
* @returns MultiEd25519Signature
*/
sign(message: HexInput): MultiEd25519Signature {
const signatures = [];
for (const signer of this.signers) {
signatures.push(signer.sign(message));
}
return new MultiEd25519Signature({ signatures, bitmap: this.signaturesBitmap });
}

/**
* Sign the given transaction using the available signing capabilities.
* @param transaction the transaction to be signed
* @returns Signature
*/
signTransaction(transaction: AnyRawTransaction): MultiEd25519Signature {
return this.sign(generateSigningMessageForTransaction(transaction));
}

// endregion
}
3 changes: 1 addition & 2 deletions src/account/MultiKeyAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ export class MultiKeyAccount implements Account, KeylessSigner {

/**
* The signers used to sign messages. These signers should correspond to public keys in the
* MultiKeyAccount's public key. The number of signers should be equal or greater
* than this.publicKey.signaturesRequired
* MultiKeyAccount's public key. The number of signers should be equal to this.publicKey.signaturesRequired.
* @group Implementation
* @category Account (On-Chain Model)
*/
Expand Down
1 change: 1 addition & 0 deletions src/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export * from "./KeylessAccount";
export * from "./AbstractKeylessAccount";
export * from "./FederatedKeylessAccount";
export * from "./MultiKeyAccount";
export * from "./MultiEd25519Account";
export * from "./AccountUtils";
export * from "./AbstractedAccount";
22 changes: 19 additions & 3 deletions src/core/crypto/multiEd25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { Deserializer, Serializer } from "../../bcs";
import { SigningScheme as AuthenticationKeyScheme } from "../../types";
import { AuthenticationKey } from "../authenticationKey";
import { Ed25519PublicKey, Ed25519Signature } from "./ed25519";
import { AccountPublicKey, VerifySignatureArgs } from "./publicKey";
import { AbstractMultiKey } from "./multiKey";
import { VerifySignatureArgs } from "./publicKey";
import { Signature } from "./signature";

/**
Expand All @@ -19,7 +20,7 @@ import { Signature } from "./signature";
* @group Implementation
* @category Serialization
*/
export class MultiEd25519PublicKey extends AccountPublicKey {
export class MultiEd25519PublicKey extends AbstractMultiKey {
/**
* Maximum number of public keys supported
* @group Implementation
Expand Down Expand Up @@ -69,8 +70,8 @@ export class MultiEd25519PublicKey extends AccountPublicKey {
* @category Serialization
*/
constructor(args: { publicKeys: Ed25519PublicKey[]; threshold: number }) {
super();
const { publicKeys, threshold } = args;
super({ publicKeys });

// Validate number of public keys
if (publicKeys.length > MultiEd25519PublicKey.MAX_KEYS || publicKeys.length < MultiEd25519PublicKey.MIN_KEYS) {
Expand Down Expand Up @@ -209,6 +210,21 @@ export class MultiEd25519PublicKey extends AccountPublicKey {
}

// endregion

/**
* Get the index of the provided public key.
*
* This function retrieves the index of a specified public key within the MultiKey.
* If the public key does not exist, it throws an error.
*
* @param publicKey - The public key to find the index for.
* @returns The corresponding index of the public key, if it exists.
* @throws Error - If the public key is not found in the MultiKey.
* @group Implementation
*/
getIndex(publicKey: Ed25519PublicKey): number {
return super.getIndex(publicKey);
}
}

/**
Expand Down
128 changes: 76 additions & 52 deletions src/core/crypto/multiKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,79 @@ function bitCount(byte: number) {
}
/* eslint-enable no-bitwise */

export abstract class AbstractMultiKey extends AccountPublicKey {
publicKeys: PublicKey[];

constructor(args: { publicKeys: PublicKey[] }) {
super();
this.publicKeys = args.publicKeys;
}

/**
* Create a bitmap that holds the mapping from the original public keys
* to the signatures passed in
*
* @param args.bits array of the index mapping to the matching public keys
* @returns Uint8array bit map
* @group Implementation
* @category Serialization
*/
createBitmap(args: { bits: number[] }): Uint8Array {
const { bits } = args;
// Bits are read from left to right. e.g. 0b10000000 represents the first bit is set in one byte.
// The decimal value of 0b10000000 is 128.
const firstBitInByte = 128;
const bitmap = new Uint8Array([0, 0, 0, 0]);

// Check if duplicates exist in bits
const dupCheckSet = new Set();

bits.forEach((bit: number, idx: number) => {
if (idx + 1 > this.publicKeys.length) {
throw new Error(`Signature index ${idx + 1} is out of public keys range, ${this.publicKeys.length}.`);
}

if (dupCheckSet.has(bit)) {
throw new Error(`Duplicate bit ${bit} detected.`);
}

dupCheckSet.add(bit);

const byteOffset = Math.floor(bit / 8);

let byte = bitmap[byteOffset];

// eslint-disable-next-line no-bitwise
byte |= firstBitInByte >> bit % 8;

bitmap[byteOffset] = byte;
});

return bitmap;
}

/**
* Get the index of the provided public key.
*
* This function retrieves the index of a specified public key within the MultiKey.
* If the public key does not exist, it throws an error.
*
* @param publicKey - The public key to find the index for.
* @returns The corresponding index of the public key, if it exists.
* @throws Error - If the public key is not found in the MultiKey.
* @group Implementation
* @category Serialization
*/
getIndex(publicKey: PublicKey): number {
const index = this.publicKeys.findIndex((pk) => pk.toString() === publicKey.toString());

if (index !== -1) {
return index;
}
throw new Error(`Public key ${publicKey} not found in multi key set ${this.publicKeys}`);
}
}

/**
* Represents a multi-key authentication scheme for accounts, allowing multiple public keys
* to be associated with a single account. This class enforces a minimum number of valid signatures
Expand All @@ -34,7 +107,7 @@ function bitCount(byte: number) {
* @group Implementation
* @category Serialization
*/
export class MultiKey extends AccountPublicKey {
export class MultiKey extends AbstractMultiKey {
/**
* List of any public keys
* @group Implementation
Expand Down Expand Up @@ -65,8 +138,8 @@ export class MultiKey extends AccountPublicKey {
*/
// region Constructors
constructor(args: { publicKeys: Array<PublicKey>; signaturesRequired: number }) {
super();
const { publicKeys, signaturesRequired } = args;
super({ publicKeys });

// Validate number of public keys is greater than signature required
if (signaturesRequired < 1) {
Expand Down Expand Up @@ -156,49 +229,6 @@ export class MultiKey extends AccountPublicKey {

// endregion

/**
* Create a bitmap that holds the mapping from the original public keys
* to the signatures passed in
*
* @param args.bits array of the index mapping to the matching public keys
* @returns Uint8array bit map
* @group Implementation
* @category Serialization
*/
createBitmap(args: { bits: number[] }): Uint8Array {
const { bits } = args;
// Bits are read from left to right. e.g. 0b10000000 represents the first bit is set in one byte.
// The decimal value of 0b10000000 is 128.
const firstBitInByte = 128;
const bitmap = new Uint8Array([0, 0, 0, 0]);

// Check if duplicates exist in bits
const dupCheckSet = new Set();

bits.forEach((bit: number, idx: number) => {
if (idx + 1 > this.publicKeys.length) {
throw new Error(`Signature index ${idx + 1} is out of public keys range, ${this.publicKeys.length}.`);
}

if (dupCheckSet.has(bit)) {
throw new Error(`Duplicate bit ${bit} detected.`);
}

dupCheckSet.add(bit);

const byteOffset = Math.floor(bit / 8);

let byte = bitmap[byteOffset];

// eslint-disable-next-line no-bitwise
byte |= firstBitInByte >> bit % 8;

bitmap[byteOffset] = byte;
});

return bitmap;
}

/**
* Get the index of the provided public key.
*
Expand All @@ -209,16 +239,10 @@ export class MultiKey extends AccountPublicKey {
* @returns The corresponding index of the public key, if it exists.
* @throws Error - If the public key is not found in the MultiKey.
* @group Implementation
* @category Serialization
*/
getIndex(publicKey: PublicKey): number {
const anyPublicKey = publicKey instanceof AnyPublicKey ? publicKey : new AnyPublicKey(publicKey);
const index = this.publicKeys.findIndex((pk) => pk.toString() === anyPublicKey.toString());

if (index !== -1) {
return index;
}
throw new Error(`Public key ${publicKey} not found in MultiKey ${this.publicKeys}`);
return super.getIndex(anyPublicKey);
}

public static isInstance(value: PublicKey): value is MultiKey {
Expand Down
Loading

0 comments on commit e8f771a

Please sign in to comment.