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

Fix multikey #579

Open
wants to merge 3 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T

# Unreleased

- [`fix`] Allow variable length bitmaps in Multikey accounts, allowing for compatibility between SDKs properly
- Add `truncateAddress` helper function to truncate an address at the middle with an ellipsis.

# 1.35.0 (2025-02-11)
Expand Down
52 changes: 50 additions & 2 deletions src/core/crypto/multiKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,56 @@ export class MultiKey extends AbstractMultiKey {

// 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
* @group Implementation
* @category Serialization
*/
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: number[] = [];

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

// Extend by required number of bytes
if (bitmap.length < byteOffset) {
for (let i = bitmap.length; i < byteOffset; i += 1) {
bitmap.push(0);
}
}

let byte = bitmap[byteOffset];

// eslint-disable-next-line no-bitwise
byte |= firstBitInByte >> bit % 8;

bitmap[byteOffset] = byte;
});

return new Uint8Array(bitmap);
}

/**
* Get the index of the provided public key.
*
Expand Down Expand Up @@ -318,8 +368,6 @@ export class MultiKeySignature extends Signature {

if (!(bitmap instanceof Uint8Array)) {
this.bitmap = MultiKeySignature.createBitmap({ bits: bitmap });
} else if (bitmap.length !== MultiKeySignature.BITMAP_LEN) {
throw new Error(`"bitmap" length should be ${MultiKeySignature.BITMAP_LEN}`);
} else {
this.bitmap = bitmap;
}
Expand Down
111 changes: 111 additions & 0 deletions tests/e2e/transaction/transactionSubmission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
TransactionPayloadEntryFunction,
Bool,
MoveString,
Ed25519PublicKey,
AnyPublicKey,
CallArgument,
MultiEd25519PublicKey,
Ed25519PrivateKey,
Expand Down Expand Up @@ -776,6 +778,115 @@ describe("transaction submission", () => {
});
expect(() => new MultiKeyAccount({ multiKey, signers: [singleSignerED25519SenderAccount] })).toThrow();
});

test("it submits a multi key transaction with lots of signers", async () => {
const subAccounts = [];
for (let i = 0; i < 32; i += 1) {
switch (i % 3) {
case 0:
subAccounts.push(Account.generate({ scheme: SigningSchemeInput.Ed25519, legacy: false }));
break;
case 1:
subAccounts.push(Account.generate({ scheme: SigningSchemeInput.Ed25519, legacy: true }));
break;
case 2:
subAccounts.push(Account.generate({ scheme: SigningSchemeInput.Secp256k1Ecdsa }));
break;
default:
break;
}
}
const publicKeys = subAccounts.map((account) => {
if (account.publicKey instanceof Ed25519PublicKey) {
return new AnyPublicKey(account.publicKey);
}
return account.publicKey;
});

const multiKey = new MultiKey({
publicKeys,
signaturesRequired: 1,
});

const account = new MultiKeyAccount({
multiKey,
signers: subAccounts,
});

await aptos.fundAccount({ accountAddress: account.accountAddress, amount: 100_000_000 });

const transaction = await aptos.transaction.build.simple({
sender: account.accountAddress,
data: {
function: `0x${contractPublisherAccount.accountAddress.toStringWithoutPrefix()}::transfer::transfer`,
functionArguments: [1, receiverAccounts[0].accountAddress],
},
});

const senderAuthenticator = aptos.transaction.sign({ signer: account, transaction });

const response = await aptos.transaction.submit.simple({ transaction, senderAuthenticator });
await aptos.waitForTransaction({
transactionHash: response.hash,
});
expect(response.signature?.type).toBe("single_sender");

// Sign with only one of them now
const account2 = new MultiKeyAccount({
multiKey,
signers: [subAccounts[0]],
});
expect(account2.accountAddress).toEqual(account.accountAddress);

await aptos.fundAccount({ accountAddress: account2.accountAddress, amount: 100_000_000 });

const transaction2 = await aptos.transaction.build.simple({
sender: account2.accountAddress,
data: {
function: `0x${contractPublisherAccount.accountAddress.toStringWithoutPrefix()}::transfer::transfer`,
functionArguments: [1, receiverAccounts[0].accountAddress],
},
});

const senderAuthenticator2 = aptos.transaction.sign({ signer: account2, transaction: transaction2 });

const response2 = await aptos.transaction.submit.simple({
transaction: transaction2,
senderAuthenticator: senderAuthenticator2,
});
await aptos.waitForTransaction({
transactionHash: response2.hash,
});
expect(response2.signature?.type).toBe("single_sender");

// Sign with the last one now
const account3 = new MultiKeyAccount({
multiKey,
signers: [subAccounts[31]],
});
expect(account3.accountAddress).toEqual(account.accountAddress);

await aptos.fundAccount({ accountAddress: account3.accountAddress, amount: 100_000_000 });

const transaction3 = await aptos.transaction.build.simple({
sender: account3.accountAddress,
data: {
function: `0x${contractPublisherAccount.accountAddress.toStringWithoutPrefix()}::transfer::transfer`,
functionArguments: [1, receiverAccounts[0].accountAddress],
},
});

const senderAuthenticator3 = aptos.transaction.sign({ signer: account3, transaction: transaction3 });

const response3 = await aptos.transaction.submit.simple({
transaction: transaction3,
senderAuthenticator: senderAuthenticator3,
});
await aptos.waitForTransaction({
transactionHash: response3.hash,
});
expect(response3.signature?.type).toBe("single_sender");
});
});

describe("MultiEd25519", () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export const multiKeyTestObject = {
signaturesRequired: 2,
address: "0x738a998ac1f69db4a91fc5a0152f792c98ad87354c65a2a842a118d7a17109b1",
authKey: "0x738a998ac1f69db4a91fc5a0152f792c98ad87354c65a2a842a118d7a17109b1",
bitmap: [160, 0, 0, 0],
bitmap: [160],
stringBytes:
"0x030141049a6f7caddff8064a7dd5800e4fb512bf1ff91daee965409385dfa040e3e63008ab7ef566f4377c2de5aeb2948208a01bcee2050c1c8578ce5fa6e0c3c507cca200207a73df1afd028e75e7f9e23b2187a37d092a6ccebcb3edff6e02f93185cbde86002017fe89a825969c1c0e5f5e80b95f563a6cb6240f88c4246c19cb39c9535a148602",
};
Expand Down
23 changes: 22 additions & 1 deletion tests/unit/multiKey.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import { Deserializer, Ed25519PublicKey, Secp256k1PublicKey, MultiKey } from "../../src";
import {
Deserializer,
Ed25519PublicKey,
Secp256k1PublicKey,
MultiKey,
Hex,
MultiKeySignature,
Serializer,
} from "../../src";
import { multiKeyTestObject } from "./helper";

describe("MultiKey", () => {
Expand Down Expand Up @@ -117,4 +125,17 @@ describe("MultiKey", () => {
const bitmap = multiKey.createBitmap({ bits: [0, 2] });
expect(bitmap).toEqual(new Uint8Array(multiKeyTestObject.bitmap));
});

it("should be able to decode from other SDKs", () => {
const serializedBytes = Hex.fromHexString(
// eslint-disable-next-line max-len
"020140118d6ebe543aaf3a541453f98a5748ab5b9e3f96d781b8c0a43740af2b65c03529fdf62b7de7aad9150770e0994dc4e0714795fdebf312be66cd0550c607755e00401a90421453aa53fa5a7aa3dfe70d913823cbf087bf372a762219ccc824d3a0eeecccaa9d34f22db4366aec61fb6c204d2440f4ed288bc7cc7e407b766723a60901c0",
).toUint8Array();
const deserializer = new Deserializer(serializedBytes);
const multiKeySig = MultiKeySignature.deserialize(deserializer);
const serializer = new Serializer();
multiKeySig.serialize(serializer);
const outBytes = serializer.toUint8Array();
expect(outBytes).toEqual(serializedBytes);
});
});
Loading