Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MultiEd25519Account #502

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions src/account/MultiEd25519Account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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[];

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.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
Loading