From 3fdc142a8bfc9bb4559ffa62a7a25d868897cfbf Mon Sep 17 00:00:00 2001
From: Ye Park <ye@tauruslabs.xyz>
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");
+  }
 }
 
 /**