diff --git a/CHANGELOG.md b/CHANGELOG.md index ab226399f..274395207 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/account/MultiKeyAccount.ts b/src/account/MultiKeyAccount.ts index cded1c52a..03c4fc8ee 100644 --- a/src/account/MultiKeyAccount.ts +++ b/src/account/MultiKeyAccount.ts @@ -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 { diff --git a/src/core/crypto/multiKey.ts b/src/core/crypto/multiKey.ts index 0317233c4..d51fc604b 100644 --- a/src/core/crypto/multiKey.ts +++ b/src/core/crypto/multiKey.ts @@ -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(); @@ -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); + } + } + let byte = bitmap[byteOffset]; // eslint-disable-next-line no-bitwise @@ -176,7 +183,7 @@ export class MultiKey extends AccountPublicKey { bitmap[byteOffset] = byte; }); - return bitmap; + return new Uint8Array(bitmap); } /** @@ -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; } diff --git a/tests/e2e/transaction/transactionSubmission.test.ts b/tests/e2e/transaction/transactionSubmission.test.ts index d9536c9ed..b35755fba 100644 --- a/tests/e2e/transaction/transactionSubmission.test.ts +++ b/tests/e2e/transaction/transactionSubmission.test.ts @@ -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"; @@ -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(); diff --git a/tests/unit/helper.ts b/tests/unit/helper.ts index 2ff918c08..f48a07eeb 100644 --- a/tests/unit/helper.ts +++ b/tests/unit/helper.ts @@ -108,7 +108,7 @@ export const multiKeyTestObject = { signaturesRequired: 2, address: "0x738a998ac1f69db4a91fc5a0152f792c98ad87354c65a2a842a118d7a17109b1", authKey: "0x738a998ac1f69db4a91fc5a0152f792c98ad87354c65a2a842a118d7a17109b1", - bitmap: [160, 0, 0, 0], + bitmap: [160], stringBytes: "0x030141049a6f7caddff8064a7dd5800e4fb512bf1ff91daee965409385dfa040e3e63008ab7ef566f4377c2de5aeb2948208a01bcee2050c1c8578ce5fa6e0c3c507cca200207a73df1afd028e75e7f9e23b2187a37d092a6ccebcb3edff6e02f93185cbde86002017fe89a825969c1c0e5f5e80b95f563a6cb6240f88c4246c19cb39c9535a148602", }; diff --git a/tests/unit/multiKey.test.ts b/tests/unit/multiKey.test.ts index c5104cd25..3aef3be1a 100644 --- a/tests/unit/multiKey.test.ts +++ b/tests/unit/multiKey.test.ts @@ -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", () => { @@ -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); + }); });