Skip to content

Commit 338bbbc

Browse files
committed
[multikey] Allow variable length signatures in multikey
1 parent 0e487fd commit 338bbbc

File tree

6 files changed

+145
-7
lines changed

6 files changed

+145
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T
44

55
# Unreleased
66

7+
- [`fix`] Allow variable length bitmaps in Multikey accounts, allowing for compatibility between SDKs properly
8+
79
# 1.33.0 (2024-11-13)
810
- Allow optional provision of public keys in transaction simulation
911
- Update the multisig v2 example to demonstrate a new way to pre-check a multisig payload before it is created on-chain

src/account/MultiKeyAccount.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export class MultiKeyAccount implements Account, KeylessSigner {
168168

169169
/**
170170
* Sign the given message using the MultiKeyAccount's signers
171-
* @param message in HexInput format
171+
* @param data in HexInput format
172172
* @returns MultiKeySignature
173173
*/
174174
sign(data: HexInput): MultiKeySignature {

src/core/crypto/multiKey.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export class MultiKey extends AccountPublicKey {
150150
// Bits are read from left to right. e.g. 0b10000000 represents the first bit is set in one byte.
151151
// The decimal value of 0b10000000 is 128.
152152
const firstBitInByte = 128;
153-
const bitmap = new Uint8Array([0, 0, 0, 0]);
153+
const bitmap: number[] = [];
154154

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

169169
const byteOffset = Math.floor(bit / 8);
170170

171+
// Extend by required number of bytes
172+
if (bitmap.length < byteOffset) {
173+
for (let i = bitmap.length; i < byteOffset; i += 1) {
174+
bitmap.push(0);
175+
}
176+
}
177+
171178
let byte = bitmap[byteOffset];
172179

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

179-
return bitmap;
186+
return new Uint8Array(bitmap);
180187
}
181188

182189
/**
@@ -260,8 +267,6 @@ export class MultiKeySignature extends Signature {
260267

261268
if (!(bitmap instanceof Uint8Array)) {
262269
this.bitmap = MultiKeySignature.createBitmap({ bits: bitmap });
263-
} else if (bitmap.length !== MultiKeySignature.BITMAP_LEN) {
264-
throw new Error(`"bitmap" length should be ${MultiKeySignature.BITMAP_LEN}`);
265270
} else {
266271
this.bitmap = bitmap;
267272
}

tests/e2e/transaction/transactionSubmission.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
TransactionPayloadEntryFunction,
1313
Bool,
1414
MoveString,
15+
Ed25519PublicKey,
16+
AnyPublicKey,
1517
} from "../../../src";
1618
import { MAX_U64_BIG_INT } from "../../../src/bcs/consts";
1719
import { longTestTimeout } from "../../unit/helper";
@@ -662,6 +664,114 @@ describe("transaction submission", () => {
662664
});
663665
expect(response.signature?.type).toBe("single_sender");
664666
});
667+
test("it submits a multi key transaction with lots of signers", async () => {
668+
const subAccounts = [];
669+
for (let i = 0; i < 32; i += 1) {
670+
switch (i % 3) {
671+
case 0:
672+
subAccounts.push(Account.generate({ scheme: SigningSchemeInput.Ed25519, legacy: false }));
673+
break;
674+
case 1:
675+
subAccounts.push(Account.generate({ scheme: SigningSchemeInput.Ed25519, legacy: true }));
676+
break;
677+
case 2:
678+
subAccounts.push(Account.generate({ scheme: SigningSchemeInput.Secp256k1Ecdsa }));
679+
break;
680+
default:
681+
break;
682+
}
683+
}
684+
const publicKeys = subAccounts.map((account) => {
685+
if (account.publicKey instanceof Ed25519PublicKey) {
686+
return new AnyPublicKey(account.publicKey);
687+
}
688+
return account.publicKey;
689+
});
690+
691+
const multiKey = new MultiKey({
692+
publicKeys,
693+
signaturesRequired: 1,
694+
});
695+
696+
const account = new MultiKeyAccount({
697+
multiKey,
698+
signers: subAccounts,
699+
});
700+
701+
await aptos.fundAccount({ accountAddress: account.accountAddress, amount: 100_000_000 });
702+
703+
const transaction = await aptos.transaction.build.simple({
704+
sender: account.accountAddress,
705+
data: {
706+
function: `0x${contractPublisherAccount.accountAddress.toStringWithoutPrefix()}::transfer::transfer`,
707+
functionArguments: [1, receiverAccounts[0].accountAddress],
708+
},
709+
});
710+
711+
const senderAuthenticator = aptos.transaction.sign({ signer: account, transaction });
712+
713+
const response = await aptos.transaction.submit.simple({ transaction, senderAuthenticator });
714+
await aptos.waitForTransaction({
715+
transactionHash: response.hash,
716+
});
717+
expect(response.signature?.type).toBe("single_sender");
718+
719+
// Sign with only one of them now
720+
const account2 = new MultiKeyAccount({
721+
multiKey,
722+
signers: [subAccounts[0]],
723+
});
724+
expect(account2.accountAddress).toEqual(account.accountAddress);
725+
726+
await aptos.fundAccount({ accountAddress: account2.accountAddress, amount: 100_000_000 });
727+
728+
const transaction2 = await aptos.transaction.build.simple({
729+
sender: account2.accountAddress,
730+
data: {
731+
function: `0x${contractPublisherAccount.accountAddress.toStringWithoutPrefix()}::transfer::transfer`,
732+
functionArguments: [1, receiverAccounts[0].accountAddress],
733+
},
734+
});
735+
736+
const senderAuthenticator2 = aptos.transaction.sign({ signer: account2, transaction: transaction2 });
737+
738+
const response2 = await aptos.transaction.submit.simple({
739+
transaction: transaction2,
740+
senderAuthenticator: senderAuthenticator2,
741+
});
742+
await aptos.waitForTransaction({
743+
transactionHash: response2.hash,
744+
});
745+
expect(response2.signature?.type).toBe("single_sender");
746+
747+
// Sign with the last one now
748+
const account3 = new MultiKeyAccount({
749+
multiKey,
750+
signers: [subAccounts[31]],
751+
});
752+
expect(account3.accountAddress).toEqual(account.accountAddress);
753+
754+
await aptos.fundAccount({ accountAddress: account3.accountAddress, amount: 100_000_000 });
755+
756+
const transaction3 = await aptos.transaction.build.simple({
757+
sender: account3.accountAddress,
758+
data: {
759+
function: `0x${contractPublisherAccount.accountAddress.toStringWithoutPrefix()}::transfer::transfer`,
760+
functionArguments: [1, receiverAccounts[0].accountAddress],
761+
},
762+
});
763+
764+
const senderAuthenticator3 = aptos.transaction.sign({ signer: account3, transaction: transaction3 });
765+
766+
const response3 = await aptos.transaction.submit.simple({
767+
transaction: transaction3,
768+
senderAuthenticator: senderAuthenticator3,
769+
});
770+
await aptos.waitForTransaction({
771+
transactionHash: response3.hash,
772+
});
773+
expect(response3.signature?.type).toBe("single_sender");
774+
});
665775
});
666776
describe("publish move module", () => {
667777
const account = Account.generate();

tests/unit/helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export const multiKeyTestObject = {
108108
signaturesRequired: 2,
109109
address: "0x738a998ac1f69db4a91fc5a0152f792c98ad87354c65a2a842a118d7a17109b1",
110110
authKey: "0x738a998ac1f69db4a91fc5a0152f792c98ad87354c65a2a842a118d7a17109b1",
111-
bitmap: [160, 0, 0, 0],
111+
bitmap: [160],
112112
stringBytes:
113113
"0x030141049a6f7caddff8064a7dd5800e4fb512bf1ff91daee965409385dfa040e3e63008ab7ef566f4377c2de5aeb2948208a01bcee2050c1c8578ce5fa6e0c3c507cca200207a73df1afd028e75e7f9e23b2187a37d092a6ccebcb3edff6e02f93185cbde86002017fe89a825969c1c0e5f5e80b95f563a6cb6240f88c4246c19cb39c9535a148602",
114114
};

tests/unit/multiKey.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
// Copyright © Aptos Foundation
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { Deserializer, Ed25519PublicKey, Secp256k1PublicKey, MultiKey } from "../../src";
4+
import {
5+
Deserializer,
6+
Ed25519PublicKey,
7+
Secp256k1PublicKey,
8+
MultiKey,
9+
Hex,
10+
MultiKeySignature,
11+
Serializer,
12+
} from "../../src";
513
import { multiKeyTestObject } from "./helper";
614

715
describe("MultiKey", () => {
@@ -117,4 +125,17 @@ describe("MultiKey", () => {
117125
const bitmap = multiKey.createBitmap({ bits: [0, 2] });
118126
expect(bitmap).toEqual(new Uint8Array(multiKeyTestObject.bitmap));
119127
});
128+
129+
it("should be able to decode from other SDKs", () => {
130+
const serializedBytes = Hex.fromHexString(
131+
// eslint-disable-next-line max-len
132+
"020140118d6ebe543aaf3a541453f98a5748ab5b9e3f96d781b8c0a43740af2b65c03529fdf62b7de7aad9150770e0994dc4e0714795fdebf312be66cd0550c607755e00401a90421453aa53fa5a7aa3dfe70d913823cbf087bf372a762219ccc824d3a0eeecccaa9d34f22db4366aec61fb6c204d2440f4ed288bc7cc7e407b766723a60901c0",
133+
).toUint8Array();
134+
const deserializer = new Deserializer(serializedBytes);
135+
const multiKeySig = MultiKeySignature.deserialize(deserializer);
136+
const serializer = new Serializer();
137+
multiKeySig.serialize(serializer);
138+
const outBytes = serializer.toUint8Array();
139+
expect(outBytes).toEqual(serializedBytes);
140+
});
120141
});

0 commit comments

Comments
 (0)