From 3fdc142a8bfc9bb4559ffa62a7a25d868897cfbf Mon Sep 17 00:00:00 2001 From: Ye Park Date: Sun, 1 Sep 2024 06:00:52 +0000 Subject: [PATCH] add MultiEd25519Account --- src/account/MultiEd25519Account.ts | 118 +++++++++++++++++++++++++++++ src/account/index.ts | 1 + src/core/crypto/multiEd25519.ts | 56 ++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 src/account/MultiEd25519Account.ts diff --git a/src/account/MultiEd25519Account.ts b/src/account/MultiEd25519Account.ts new file mode 100644 index 000000000..3bafe1f3b --- /dev/null +++ b/src/account/MultiEd25519Account.ts @@ -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 +} diff --git a/src/account/index.ts b/src/account/index.ts index 53d6d7ccd..132130461 100644 --- a/src/account/index.ts +++ b/src/account/index.ts @@ -1,4 +1,5 @@ export * from "./Ed25519Account"; +export * from "./MultiEd25519Account"; export * from "./Account"; export * from "./SingleKeyAccount"; export * from "./EphemeralKeyPair"; diff --git a/src/core/crypto/multiEd25519.ts b/src/core/crypto/multiEd25519.ts index aa02f0a76..8773a65e7 100644 --- a/src/core/crypto/multiEd25519.ts +++ b/src/core/crypto/multiEd25519.ts @@ -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"); + } } /**