Skip to content

Commit

Permalink
[multikey] Allow variable length signatures in multikey
Browse files Browse the repository at this point in the history
  • Loading branch information
gregnazario committed Nov 14, 2024
1 parent 0e487fd commit 338bbbc
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 7 deletions.
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);
}
}

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

0 comments on commit 338bbbc

Please sign in to comment.