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 1 commit 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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

# 1.33.0 (2024-11-13)
- Allow optional provision of public keys in transaction simulation
- Update the multisig v2 example to demonstrate a new way to pre-check a multisig payload before it is created on-chain
Expand Down
2 changes: 1 addition & 1 deletion src/account/MultiKeyAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export class MultiKeyAccount implements Account, KeylessSigner {

/**
* Sign the given message using the MultiKeyAccount's signers
* @param message in HexInput format
* @param data in HexInput format
* @returns MultiKeySignature
*/
sign(data: HexInput): MultiKeySignature {
Expand Down
13 changes: 9 additions & 4 deletions src/core/crypto/multiKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export class MultiKey extends AccountPublicKey {
// 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]);
const bitmap: number[] = [];

// Check if duplicates exist in bits
const dupCheckSet = new Set();
Expand All @@ -168,6 +168,13 @@ export class MultiKey extends AccountPublicKey {

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);
}
GhostWalker562 marked this conversation as resolved.
Show resolved Hide resolved
}

let byte = bitmap[byteOffset];

// eslint-disable-next-line no-bitwise
Expand All @@ -176,7 +183,7 @@ export class MultiKey extends AccountPublicKey {
bitmap[byteOffset] = byte;
});

return bitmap;
return new Uint8Array(bitmap);
}

/**
Expand Down Expand Up @@ -260,8 +267,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
110 changes: 110 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,
} from "../../../src";
import { MAX_U64_BIG_INT } from "../../../src/bcs/consts";
import { longTestTimeout } from "../../unit/helper";
Expand Down Expand Up @@ -662,6 +664,114 @@ describe("transaction submission", () => {
});
expect(response.signature?.type).toBe("single_sender");
});
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("publish move module", () => {
const account = Account.generate();
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