Skip to content

Commit

Permalink
Implement MultiKey authentication key (#141)
Browse files Browse the repository at this point in the history
* implement multi key

* fixlogic and add tests

* typo
  • Loading branch information
0xmaayan authored Oct 30, 2023
1 parent 97bd81d commit 7626d37
Show file tree
Hide file tree
Showing 15 changed files with 450 additions and 42 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T
- Rename publishModuleTransaction to publishPackageTransaction and fix functionality accordingly
- Added toString() for type tags, and reference placeholder type
- Add ability to generate transactions with known ABI and remote ABI
- Fix verify signature logic
- Implement `MultiKey`support for multi authentication key

## 0.0.2 (2023-10-25)

Expand Down
11 changes: 5 additions & 6 deletions src/core/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export class Account {
*
* @param args.privateKey PrivateKey - private key of the account
* @param args.address AccountAddress - address of the account
* @param args.legacy optional. If set to true, the keypair generated is a Legacy keypair. Defaults
* to generating a Unified keypair
* @param args.legacy optional. If set to true, the keypair authentication keys will be derived with a Legacy scheme.
* Defaults to deriving an authentication key with a Unified scheme
*
* This method is private because it should only be called by the factory static methods.
* @returns Account
Expand Down Expand Up @@ -129,7 +129,6 @@ export class Account {
case SigningSchemeInput.Secp256k1Ecdsa:
privateKey = Secp256k1PrivateKey.generate();
break;
// TODO: Add support for MultiEd25519 as AnyMultiKey
default:
privateKey = Ed25519PrivateKey.generate();
}
Expand All @@ -141,7 +140,7 @@ export class Account {

const address = new AccountAddress({
data: Account.authKey({
publicKey, // TODO support AnyMultiKey
publicKey,
}).toUint8Array(),
});
return new Account({ privateKey, address, legacy: args?.legacy });
Expand All @@ -153,8 +152,8 @@ export class Account {
*
* @param privateKey PrivateKey - private key of the account
* @param address The account address
* @param args.legacy optional. If set to true, the keypair generated is a Legacy keypair. Defaults
* to generating a Unified keypair
* @param args.legacy optional. If set to true, the keypair authentication keys will be derived with a Legacy scheme.
* Defaults to deriving an authentication key with a Unified scheme
*
* @returns Account
*/
Expand Down
7 changes: 6 additions & 1 deletion src/core/authenticationKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { MultiEd25519PublicKey } from "./crypto/multiEd25519";
import { Hex } from "./hex";
import { AuthenticationKeyScheme, HexInput, SigningScheme } from "../types";
import { AnyPublicKey } from "./crypto/anyPublicKey";
import { MultiKey } from "./crypto/multiKey";

/**
* Each account stores an authentication key. Authentication key enables account owners to rotate
Expand Down Expand Up @@ -57,20 +58,22 @@ export class AuthenticationKey {
const { publicKey, scheme } = args;
let authKeyBytes: Uint8Array;

// TODO - support multied25519 key and MultiKey
switch (scheme) {
case SigningScheme.MultiKey:
case SigningScheme.SingleKey: {
const singleKeyBytes = publicKey.bcsToBytes();
authKeyBytes = new Uint8Array([...singleKeyBytes, scheme]);
break;
}

case SigningScheme.Ed25519:
case SigningScheme.MultiEd25519: {
const ed25519PublicKeyBytes = publicKey.toUint8Array();
const inputBytes = Hex.fromHexInput(ed25519PublicKeyBytes).toUint8Array();
authKeyBytes = new Uint8Array([...inputBytes, scheme]);
break;
}

default:
throw new Error(`Scheme ${scheme} is not supported`);
}
Expand Down Expand Up @@ -99,6 +102,8 @@ export class AuthenticationKey {
scheme = SigningScheme.MultiEd25519.valueOf();
} else if (publicKey instanceof AnyPublicKey) {
scheme = SigningScheme.SingleKey.valueOf();
} else if (publicKey instanceof MultiKey) {
scheme = SigningScheme.MultiKey.valueOf();
} else {
throw new Error("No supported authentication scheme for public key");
}
Expand Down
32 changes: 14 additions & 18 deletions src/core/crypto/anyPublicKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@ import { Serializer, Deserializer } from "../../bcs";
import { AnyPublicKeyVariant, HexInput } from "../../types";
import { AnySignature } from "./anySignature";
import { PublicKey } from "./asymmetricCrypto";
import { Ed25519PublicKey, Ed25519Signature } from "./ed25519";
import { Secp256k1PublicKey, Secp256k1Signature } from "./secp256k1";
import { Ed25519PublicKey } from "./ed25519";
import { Secp256k1PublicKey } from "./secp256k1";

/**
* Represents any public key supported by Aptos.
*
* Since [AIP-55](https://github.com/aptos-foundation/AIPs/pull/263) Aptos supports
* `Legacy` and `Unified` authentication keys.
*
* Any unified authentication key is represented in the SDK as `AnyPublicKey`.
*/
export class AnyPublicKey extends PublicKey {
/**
* Reference to the inner public key
*/
public readonly publicKey: PublicKey;

constructor(publicKey: PublicKey) {
Expand Down Expand Up @@ -40,22 +51,7 @@ export class AnyPublicKey extends PublicKey {
*/
verifySignature(args: { message: HexInput; signature: AnySignature }): boolean {
const { message, signature } = args;
if (this.isED25519Signature(signature)) {
return this.publicKey.verifySignature({ message, signature: signature.signature });
// eslint-disable-next-line no-else-return
} else if (this.isSecp256k1Signature(signature)) {
return this.publicKey.verifySignature({ message, signature: signature.signature });
} else {
throw new Error("Unknown public key type");
}
}

isED25519Signature(signature: AnySignature): boolean {
return this.publicKey instanceof Ed25519PublicKey && signature.signature instanceof Ed25519Signature;
}

isSecp256k1Signature(signature: AnySignature): boolean {
return this.publicKey instanceof Secp256k1PublicKey && signature.signature instanceof Secp256k1Signature;
return this.publicKey.verifySignature({ message, signature });
}

serialize(serializer: Serializer): void {
Expand Down
6 changes: 6 additions & 0 deletions src/core/crypto/ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { HexInput } from "../../types";

/**
* Represents the public key of an Ed25519 key pair.
*
* Since [AIP-55](https://github.com/aptos-foundation/AIPs/pull/263) Aptos supports
* `Legacy` and `Unified` authentication keys.
*
* Ed25519 scheme is represented in the SDK as `Legacy authentication key` and also
* as `AnyPublicKey` that represents any `Unified authentication key`
*/
export class Ed25519PublicKey extends PublicKey {
/**
Expand Down
1 change: 1 addition & 0 deletions src/core/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./asymmetricCrypto";
export * from "./ed25519";
export * from "./multiEd25519";
export * from "./secp256k1";
export * from "./multiKey";
122 changes: 122 additions & 0 deletions src/core/crypto/multiKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Hex } from "../hex";
import { HexInput } from "../../types";
import { Deserializer } from "../../bcs/deserializer";
import { Serializer } from "../../bcs/serializer";
import { AnyPublicKey } from "./anyPublicKey";
import { AnySignature } from "./anySignature";
import { PublicKey } from "./asymmetricCrypto";

export class MultiKey extends PublicKey {
/**
* List of any public keys
*/
public readonly publicKeys: AnyPublicKey[];

/**
* The minimum number of valid signatures required, for the number of public keys specified
*/
public readonly signaturesRequired: number;

constructor(args: { publicKeys: PublicKey[]; signaturesRequired: number }) {
super();
const { publicKeys, signaturesRequired } = args;

// Validate number of public keys is greater than signature required
if (signaturesRequired < 1) {
throw new Error("The number of required signatures needs to be greater then 0");
}

// Validate number of public keys is greater than signature required
if (publicKeys.length < signaturesRequired) {
throw new Error(
`Provided ${publicKeys.length} public keys is smaller than the ${signaturesRequired} required signatures`,
);
}

const keys: AnyPublicKey[] = [];
publicKeys.forEach((publicKey) => {
if (publicKey instanceof AnyPublicKey) {
keys.push(publicKey);
} else {
// if public key is instance of a legacy authentication key, i.e
// Legacy Ed25519, convert it into AnyPublicKey
keys.push(new AnyPublicKey(publicKey));
}
});

this.publicKeys = keys;
this.signaturesRequired = signaturesRequired;
}

toUint8Array(): Uint8Array {
return this.bcsToBytes();
}

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

/**
* Hex string representation the multi key bytes
*
* @returns string
*/
toString(): string {
return Hex.fromHexInput(this.toUint8Array()).toString();
}

// TODO
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
verifySignature(args: { message: HexInput; signature: AnySignature }): boolean {
throw new Error("not implemented");
}

serialize(serializer: Serializer): void {
serializer.serializeVector(this.publicKeys);
serializer.serializeU8(this.signaturesRequired);
}

static deserialize(deserializer: Deserializer): MultiKey {
const keys = deserializer.deserializeVector(AnyPublicKey);
const signaturesRequired = deserializer.deserializeU8();

return new MultiKey({ publicKeys: keys, signaturesRequired });
}
}
2 changes: 2 additions & 0 deletions src/core/crypto/secp256k1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { HexInput } from "../../types";

/**
* Represents the Secp256k1 ecdsa public key
*
* Secp256k1 authentication key is represented in the SDK as `AnyPublicKey`.
*/
export class Secp256k1PublicKey extends PublicKey {
// Secp256k1 ecdsa public keys contain a prefix indicating compression and two 32-byte coordinates.
Expand Down
39 changes: 39 additions & 0 deletions src/transactions/authenticator/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AnyPublicKey } from "../../core/crypto/anyPublicKey";
import { AnySignature } from "../../core/crypto/anySignature";
import { Ed25519PublicKey, Ed25519Signature } from "../../core/crypto/ed25519";
import { MultiEd25519PublicKey, MultiEd25519Signature } from "../../core/crypto/multiEd25519";
import { MultiKey } from "../../core/crypto/multiKey";
import { AccountAuthenticatorVariant } from "../../types";

export abstract class AccountAuthenticator extends Serializable {
Expand All @@ -22,6 +23,8 @@ export abstract class AccountAuthenticator extends Serializable {
return AccountAuthenticatorMultiEd25519.load(deserializer);
case AccountAuthenticatorVariant.SingleKey:
return AccountAuthenticatorSingleKey.load(deserializer);
case AccountAuthenticatorVariant.MultiKey:
return AccountAuthenticatorMultiKey.load(deserializer);
default:
throw new Error(`Unknown variant index for AccountAuthenticator: ${index}`);
}
Expand Down Expand Up @@ -120,3 +123,39 @@ export class AccountAuthenticatorSingleKey extends AccountAuthenticator {
return new AccountAuthenticatorSingleKey(public_key, signature);
}
}

/**
* AccountAuthenticatorMultiKey for a multi signer
*
* @param public_keys MultiKey
* @param signatures Signature
*
*/
export class AccountAuthenticatorMultiKey extends AccountAuthenticator {
public readonly public_keys: MultiKey;

public readonly signatures: Array<AnySignature>;

public readonly signatures_bitmap: Uint8Array;

constructor(public_keys: MultiKey, signatures: Array<AnySignature>, signatures_bitmap: Uint8Array) {
super();
this.public_keys = public_keys;
this.signatures = signatures;
this.signatures_bitmap = signatures_bitmap;
}

serialize(serializer: Serializer): void {
serializer.serializeU32AsUleb128(AccountAuthenticatorVariant.MultiKey);
this.public_keys.serialize(serializer);
serializer.serializeVector<AnySignature>(this.signatures);
serializer.serializeBytes(this.signatures_bitmap);
}

static load(deserializer: Deserializer): AccountAuthenticatorMultiKey {
const public_keys = MultiKey.deserialize(deserializer);
const signatures = deserializer.deserializeVector(AnySignature);
const signatures_bitmap = deserializer.deserializeBytes();
return new AccountAuthenticatorMultiKey(public_keys, signatures, signatures_bitmap);
}
}
21 changes: 10 additions & 11 deletions src/transactions/transaction_builder/transaction_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import {
AccountAuthenticator,
AccountAuthenticatorEd25519,
AccountAuthenticatorMultiKey,
AccountAuthenticatorSingleKey,
} from "../authenticator/account";
import {
Expand Down Expand Up @@ -490,27 +491,25 @@ export function generateSignedTransaction(args: {

// submit single signer transaction

// deserialize the senderAuthenticator
const deserializer = new Deserializer(senderAuthenticator.bcsToBytes());
const accountAuthenticator = AccountAuthenticator.deserialize(deserializer);
// check what instance is accountAuthenticator
if (accountAuthenticator instanceof AccountAuthenticatorEd25519) {
if (senderAuthenticator instanceof AccountAuthenticatorEd25519) {
const transactionAuthenticator = new TransactionAuthenticatorEd25519(
accountAuthenticator.public_key,
accountAuthenticator.signature,
senderAuthenticator.public_key,
senderAuthenticator.signature,
);
// return signed transaction
return new SignedTransaction(transactionToSubmit as RawTransaction, transactionAuthenticator).bcsToBytes();
}

if (accountAuthenticator instanceof AccountAuthenticatorSingleKey) {
const transactionAuthenticator = new TransactionAuthenticatorSingleSender(accountAuthenticator);
// return signed transaction
if (
senderAuthenticator instanceof AccountAuthenticatorSingleKey ||
senderAuthenticator instanceof AccountAuthenticatorMultiKey
) {
const transactionAuthenticator = new TransactionAuthenticatorSingleSender(senderAuthenticator);
return new SignedTransaction(transactionToSubmit as RawTransaction, transactionAuthenticator).bcsToBytes();
}

throw new Error(
`Cannot generate a signed transaction, ${accountAuthenticator} is not a supported account authentication scheme`,
`Cannot generate a signed transaction, ${senderAuthenticator} is not a supported account authentication scheme`,
);
}

Expand Down
Loading

0 comments on commit 7626d37

Please sign in to comment.