Skip to content

Commit

Permalink
add MultiEd25519Account
Browse files Browse the repository at this point in the history
  • Loading branch information
yeptos committed Sep 1, 2024
1 parent f54cac8 commit d876bfd
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 0 deletions.
125 changes: 125 additions & 0 deletions src/account/MultiEd25519Account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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;
privateKeys: Ed25519PrivateKey[];
address?: AccountAddressInput;
}

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

/**
* Signer implementation for the Multi-Ed25519 authentication scheme.
*
* Note: 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;

/**
* Private keys associated with the account
*/
readonly privateKeys: Ed25519PrivateKey[];

/**
* An array of indicies 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.
*/
readonly signerIndicies: number[];

readonly signaturesBitmap: Uint8Array;

// region Constructors

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

// Get the index of each respective signer in the bitmap
const bitPositions: number[] = [];
for (const privateKey of privateKeys) {
bitPositions.push(this.publicKey.getIndex(privateKey.publicKey()));
}
// Zip privateKeys and bit positions and sort privateKeys by bit positions in order
// to ensure the signature is signed in ascending order according to the bitmap.
// Authentication on chain will fail otherwise.
const privateKeysAndBitPosition = privateKeys.map((signer, index) => [signer, bitPositions[index]] as const);
privateKeysAndBitPosition.sort((a, b) => a[1] - b[1]);
this.privateKeys = privateKeysAndBitPosition.map((value) => value[0]);
this.signerIndicies = privateKeysAndBitPosition.map((value) => value[1]);
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.privateKeys) {
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
}
1 change: 1 addition & 0 deletions src/account/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./Ed25519Account";
export * from "./MultiEd25519Account";
export * from "./Account";
export * from "./SingleKeyAccount";
export * from "./EphemeralKeyPair";
Expand Down
56 changes: 56 additions & 0 deletions src/core/crypto/multiEd25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,62 @@ export class MultiEd25519PublicKey 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
*/
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.
*
* @param publicKey array of the index mapping to the matching public keys
* @returns the corresponding index of the publicKey, if it exists
*/
getIndex(publicKey: Ed25519PublicKey): number {
const index = this.publicKeys.findIndex((pk) => pk.toString() === publicKey.toString());

if (index !== -1) {
return index;
}
throw new Error("Public key not found in MultiEd25519PublicKey");
}
}

/**
Expand Down

0 comments on commit d876bfd

Please sign in to comment.