Skip to content

Commit

Permalink
Refactor core/accounts Remove private key from the Account interface.…
Browse files Browse the repository at this point in the history
… Separate out the signing message functionality. Adds MultiKeyAccount (#379)

* add accounts

* separate the signing message

* add multisig

* clean up

* update transaction submission

* fix account imports

* update singlekey sign

* delete old accounts

* update changelog

* update

* lint

* fix imports

* fix the domain separator salt

* remove test.only

* fix tests

* fmt

* Update CHANGELOG.md

* address comments

* address more comments

* fix test

* use test constant

* remove old code

* fmt

* revert simulation changes

* fix import

* revert multikeysig rename

* Update account interface

* add docstrings and implement verifySignature

* add test for verification

* fmt

* add test for misordering signers
  • Loading branch information
heliuchuan authored May 15, 2024
1 parent 5b66470 commit b41ea4b
Show file tree
Hide file tree
Showing 34 changed files with 642 additions and 213 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T

# Unreleased

- [`Breaking`] Removes private key from the Account class to support MultiKey accounts.
- [`Breaking`] Removes the `sign` function from transactionBuilder.ts. Use `Account.signTransactionWithAuthenticator` instead.
- Refactors the core/accounts folder to the top level
- Separates the signing message functionality out of the transactionSubmission.ts file
- Adds an Account implementation for MultiKey accounts

# 1.14.0 (2024-05-09)

- [`Fix`] fixed `trasnferFungibleAsset` function
Expand Down
40 changes: 25 additions & 15 deletions src/core/account/Account.ts → src/account/Account.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { AccountAuthenticator } from "../../transactions/authenticator/account";
import { HexInput, SigningScheme, SigningSchemeInput } from "../../types";
import type { AccountAddress, AccountAddressInput } from "../accountAddress";
import { AuthenticationKey } from "../authenticationKey";
import { AccountPublicKey, Ed25519PrivateKey, PrivateKey, Signature, VerifySignatureArgs } from "../crypto";
import type { AccountAuthenticator } from "../transactions/authenticator/account";
import { HexInput, SigningScheme, SigningSchemeInput } from "../types";
import type { AccountAddress, AccountAddressInput } from "../core/accountAddress";
import { AuthenticationKey } from "../core/authenticationKey";
import { AccountPublicKey, Ed25519PrivateKey, PrivateKey, Signature, VerifySignatureArgs } from "../core/crypto";
import { Ed25519Account } from "./Ed25519Account";
import { SingleKeyAccount } from "./SingleKeyAccount";
import { AnyRawTransaction } from "../transactions/types";

/**
* Arguments for creating an `Ed25519Account` from an `Ed25519PrivateKey`.
Expand Down Expand Up @@ -106,13 +107,6 @@ export interface PrivateKeyFromDerivationPathArgs {
* Note: Generating an account instance does not create the account on-chain.
*/
export abstract class Account {
/**
* Private key associated with the account.
* Note: this will be removed in the next major release,
* as not all accounts have a private key.
*/
abstract readonly privateKey: PrivateKey;

/**
* Public key associated with the account
*/
Expand Down Expand Up @@ -229,15 +223,31 @@ export abstract class Account {
abstract signWithAuthenticator(message: HexInput): AccountAuthenticator;

/**
* Sign the given message with the private key.
* Sign a transaction using the available signing capabilities.
* @param transaction the raw transaction
* @return the AccountAuthenticator containing the signature of the transaction, together with the account's public key
*/
abstract signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticator;

/**
* Sign the given message using the available signing capabilities.
* @param message in HexInput format
* @returns AccountSignature
* @returns Signature
*/
abstract sign(message: HexInput): Signature;

/**
* Sign the given transaction using the available signing capabilities.
* @param transaction the transaction to be signed
* @returns Signature
*/
abstract signTransaction(transaction: AnyRawTransaction): Signature;

/**
* 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
* @param args.signature signed message Signature
* @returns
*/
verifySignature(args: VerifySignatureArgs): boolean {
return this.publicKey.verifySignature(args);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { AccountAuthenticatorEd25519 } from "../../transactions/authenticator/account";
import { HexInput, SigningScheme } from "../../types";
import { AccountAddress, AccountAddressInput } from "../accountAddress";
import { Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature } from "../crypto";
import { AccountAuthenticatorEd25519 } from "../transactions/authenticator/account";
import { HexInput, SigningScheme } from "../types";
import { AccountAddress, AccountAddressInput } from "../core/accountAddress";
import { Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature } from "../core/crypto";
import type { Account } from "./Account";
import { AnyRawTransaction } from "../transactions/types";
import { generateSigningMessageForTransaction } from "../transactions/transactionBuilder/signingMessage";

export interface Ed25519SignerConstructorArgs {
privateKey: Ed25519PrivateKey;
Expand Down Expand Up @@ -71,17 +73,51 @@ export class Ed25519Account implements Account {

// 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: VerifyEd25519SignatureArgs): boolean {
return this.publicKey.verifySignature(args);
}

signWithAuthenticator(message: HexInput) {
const signature = this.privateKey.sign(message);
return new AccountAuthenticatorEd25519(this.publicKey, signature);
/**
* 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): AccountAuthenticatorEd25519 {
return new AccountAuthenticatorEd25519(this.publicKey, this.privateKey.sign(message));
}

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

/**
* Sign the given message using the account's Ed25519 private key.
* @param message in HexInput format
* @returns Signature
*/
sign(message: HexInput): Ed25519Signature {
return this.privateKey.sign(message);
}

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

// endregion
Expand Down
176 changes: 176 additions & 0 deletions src/account/MultiKeyAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import { Account } from "./Account";
import { MultiKey, MultiKeySignature, PublicKey } from "../core/crypto";
import { AccountAddress } from "../core/accountAddress";
import { HexInput, SigningScheme } from "../types";
import { AccountAuthenticatorMultiKey } from "../transactions/authenticator/account";
import { AnyRawTransaction } from "../transactions/types";

export interface VerifyMultiKeySignatureArgs {
message: HexInput;
signature: MultiKeySignature;
}

/**
* Signer implementation for the MultiKey authentication scheme.
*
* This accounts to use a M of N signing scheme. M and N are specified in the {@link MultiKey}
* It signs messages via the array of M number of Accounts that individually correspond to a public key in the {@link MultiKey}.
*
* Note: Generating a signer instance does not create the account on-chain.
*/
export class MultiKeyAccount implements Account {
/**
* Public key associated with the account
*/
readonly publicKey: MultiKey;

/**
* Account address associated with the account
*/
readonly accountAddress: AccountAddress;

/**
* Signing scheme used to sign transactions
*/
readonly signingScheme: SigningScheme;

/**
* 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
*/
readonly signers: Account[];

/**
* 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;

/**
* constructor for MultiKeyAccount
*
* @param args.multiKey the multikey of the account which consists of N public keys and a number M which is
* the number of required signatures.
* @param args.signers an array of M signers that will be used to sign the transaction
* @returns MultiKeyAccount
*/
constructor(args: { multiKey: MultiKey; signers: Account[] }) {
const { multiKey, signers } = args;

this.publicKey = multiKey;
this.signingScheme = SigningScheme.MultiKey;

this.accountAddress = this.publicKey.authKey().derivedAddress();

// Get the index of each respective signer in the bitmap
const bitPositions: number[] = [];
for (const signer of signers) {
bitPositions.push(this.publicKey.getIndex(signer.publicKey));
}
// Zip signers and bit positions and sort signers 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 signersAndBitPosition: [Account, number][] = signers.map((signer, index) => [signer, bitPositions[index]]);
signersAndBitPosition.sort((a, b) => a[1] - b[1]);
this.signers = signersAndBitPosition.map((value) => value[0]);
this.signerIndicies = signersAndBitPosition.map((value) => value[1]);
this.signaturesBitmap = this.publicKey.createBitmap({ bits: bitPositions });
}

/**
* Static constructor for MultiKeyAccount
*
* @param args.publicKeys the N public keys of the MultiKeyAccount
* @param args.signaturesRequired the number of signatures required
* @param args.signers an array of M signers that will be used to sign the transaction
* @returns MultiKeyAccount
*/
static fromPublicKeysAndSigners(args: {
publicKeys: PublicKey[];
signaturesRequired: number;
signers: Account[];
}): MultiKeyAccount {
const { publicKeys, signaturesRequired, signers } = args;
const multiKey = new MultiKey({ publicKeys, signaturesRequired });
return new MultiKeyAccount({ multiKey, signers });
}

static isMultiKeySigner(account: Account): account is MultiKeyAccount {
return account instanceof MultiKeyAccount;
}

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

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

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

/**
* Sign the given transaction using the MultiKeyAccount's signers
* @param transaction the transaction to be signed
* @returns MultiKeySignature
*/
signTransaction(transaction: AnyRawTransaction): MultiKeySignature {
const signatures = [];
for (const signer of this.signers) {
signatures.push(signer.signTransaction(transaction));
}
return new MultiKeySignature({ signatures, bitmap: this.signaturesBitmap });
}

/**
* Verify the given message and signature with the public key.
*
* @param args.message raw message data in HexInput format
* @param args.signatures signed message MultiKeySignature
* @returns boolean
*/
verifySignature(args: VerifyMultiKeySignatureArgs): boolean {
const { message, signature } = args;
const isSignerIndiciesSorted = this.signerIndicies.every(
(value, i) => i === 0 || value >= this.signerIndicies[i - 1],
);
if (!isSignerIndiciesSorted) {
return false;
}
for (let i = 0; i < signature.signatures.length; i += 1) {
const singleSignature = signature.signatures[i];
const publicKey = this.publicKey.publicKeys[this.signerIndicies[i]];
if (!publicKey.verifySignature({ message, signature: singleSignature })) {
return false;
}
}
return true;
}
}
Loading

0 comments on commit b41ea4b

Please sign in to comment.