From 23a1a79e4acff24a9f79336c122b6be5aa3d3ce9 Mon Sep 17 00:00:00 2001 From: Junkil Park Date: Mon, 15 Jul 2024 08:56:27 -0700 Subject: [PATCH] Add the `NoAccountAuthenticator` variant Simulation API Update: Allows users to simulate transactions without providing public keys by updating simulateTransaction to accept PublicKey | null instead of PublicKey. If null is provided, NoAccountAuthenticator is used as an authenticator. Multisig V2 Example Update: The multisig v2 example (multisig_v2.ts) is updated to reflect the change in the multisig transaction simulation behavior. To pre-check the multisig payload before creation, an entry function payload simulation with the multisig account as the sender and 0x0 as the fee payer is used. --- .gitignore | 1 + CHANGELOG.md | 3 + examples/typescript-esm/multisig_v2.ts | 47 ++++-- examples/typescript/local_node.ts | 1 + src/api/transactionSubmission/helpers.ts | 6 - src/api/transactionSubmission/simulate.ts | 13 +- src/internal/transactionSubmission.ts | 2 +- src/transactions/authenticator/account.ts | 19 +++ .../transactionBuilder/transactionBuilder.ts | 42 +++--- src/transactions/types.ts | 5 +- src/types/index.ts | 1 + .../transaction/transactionSimulation.test.ts | 134 +++++++++++++++--- .../transaction/transactionSubmission.test.ts | 29 ++++ 13 files changed, 242 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index 8f735d3cd..c5dfcb860 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ examples/typescript/facoin/facoin.json # Vim swap files *.swp +/.fleet diff --git a/CHANGELOG.md b/CHANGELOG.md index 98b2c3c30..56c21159c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T # Unreleased +- 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 + # 1.29.1 (2024-10-09) - Fix the `FederatedKeylessAccount` constructor to derive the correct address. diff --git a/examples/typescript-esm/multisig_v2.ts b/examples/typescript-esm/multisig_v2.ts index d592caa99..7f0df6ff9 100644 --- a/examples/typescript-esm/multisig_v2.ts +++ b/examples/typescript-esm/multisig_v2.ts @@ -31,6 +31,7 @@ import { InputViewFunctionData, SimpleTransaction, generateTransactionPayload, + TransactionPayloadEntryFunction, } from "@aptos-labs/ts-sdk"; // Default to devnet, but allow for overriding @@ -141,19 +142,39 @@ const createMultiSigTransferTransaction = async () => { aptosConfig: config, }); - // Simulate the transfer transaction to make sure it passes - const transactionToSimulate = await generateRawTransaction({ - aptosConfig: config, - sender: owner2.accountAddress, - payload: transactionPayload, - }); - - const simulateMultisigTx = await aptos.transaction.simulate.simple({ - signerPublicKey: owner2.publicKey, - transaction: new SimpleTransaction(transactionToSimulate), - }); - - console.log("simulateMultisigTx", simulateMultisigTx); + // The simulation enhancement feature is not enabled on the devnet yet. + const isSimulationEnhancementFeatureEnabled = false; + if (!isSimulationEnhancementFeatureEnabled) { + // Simulate the transfer transaction to make sure it passes + const transactionToSimulate = await generateRawTransaction({ + aptosConfig: config, + sender: owner2.accountAddress, + payload: transactionPayload, + }); + + const simulateMultisigTx = await aptos.transaction.simulate.simple({ + signerPublicKey: owner2.publicKey, + transaction: new SimpleTransaction(transactionToSimulate), + }); + + console.log("simulateMultisigTx", simulateMultisigTx); + } else { + // Generate a raw transaction with the multisig address as the sender, + // the provided entry function payload, and 0x0 as the fee payer address. + const transactionToSimulate = await generateRawTransaction({ + aptosConfig: config, + sender: transactionPayload.multiSig.multisig_address, + payload: new TransactionPayloadEntryFunction(transactionPayload.multiSig.transaction_payload.transaction_payload), + feePayerAddress: AccountAddress.ZERO, + }); + + // Simulate the transaction, skipping the public/auth key check for both the sender and the fee payer. + const simulateMultisigTx = await aptos.transaction.simulate.simple({ + transaction: new SimpleTransaction(transactionToSimulate), + }); + + console.log("simulateMultisigTx", simulateMultisigTx); + } // Build create_transaction transaction const createMultisigTx = await aptos.transaction.build.simple({ diff --git a/examples/typescript/local_node.ts b/examples/typescript/local_node.ts index 4cc0c1380..4e6614030 100644 --- a/examples/typescript/local_node.ts +++ b/examples/typescript/local_node.ts @@ -108,6 +108,7 @@ async function stopLocalNode() { // Query localnet endpoint await fetch("http://localhost:8080"); } catch (err: any) { + // eslint-disable-next-line no-console console.log("localnet stopped"); } } diff --git a/src/api/transactionSubmission/helpers.ts b/src/api/transactionSubmission/helpers.ts index 332a92f52..b1dac8289 100644 --- a/src/api/transactionSubmission/helpers.ts +++ b/src/api/transactionSubmission/helpers.ts @@ -18,12 +18,6 @@ export function ValidateFeePayerDataOnSimulation(target: unknown, propertyKey: s const originalMethod = descriptor.value; /* eslint-disable-next-line func-names, no-param-reassign */ descriptor.value = async function (...args: any[]) { - const [methodArgs] = args; - - if (methodArgs.transaction.feePayerAddress && !methodArgs.feePayerPublicKey) { - throw new Error("You are simulating a Fee Payer transaction but missing the feePayerPublicKey"); - } - return originalMethod.apply(this, args); }; diff --git a/src/api/transactionSubmission/simulate.ts b/src/api/transactionSubmission/simulate.ts index 7cf9d4c90..447aea783 100644 --- a/src/api/transactionSubmission/simulate.ts +++ b/src/api/transactionSubmission/simulate.ts @@ -21,7 +21,7 @@ export class Simulate { /** * Simulate a simple transaction * - * @param args.signerPublicKey The signer public key + * @param args.signerPublicKey optional. The signer public key * @param args.transaction An instance of a raw transaction * @param args.options optional. Optional transaction configurations * @param args.feePayerPublicKey optional. The fee payer public key if it is a fee payer transaction @@ -30,7 +30,7 @@ export class Simulate { */ @ValidateFeePayerDataOnSimulation async simple(args: { - signerPublicKey: PublicKey; + signerPublicKey?: PublicKey; transaction: AnyRawTransaction; feePayerPublicKey?: PublicKey; options?: InputSimulateTransactionOptions; @@ -41,9 +41,10 @@ export class Simulate { /** * Simulate a multi agent transaction * - * @param args.signerPublicKey The signer public key + * @param args.signerPublicKey optional. The signer public key * @param args.transaction An instance of a raw transaction - * @param args.secondarySignersPublicKeys An array of the secondary signers public keys + * @param args.secondarySignersPublicKeys optional. An array of the secondary signers' public keys. + * Each element of the array can be optional, allowing the corresponding key check to be skipped. * @param args.options optional. Optional transaction configurations * @param args.feePayerPublicKey optional. The fee payer public key if it is a fee payer transaction * @@ -51,9 +52,9 @@ export class Simulate { */ @ValidateFeePayerDataOnSimulation async multiAgent(args: { - signerPublicKey: PublicKey; + signerPublicKey?: PublicKey; transaction: AnyRawTransaction; - secondarySignersPublicKeys: Array; + secondarySignersPublicKeys?: Array; feePayerPublicKey?: PublicKey; options?: InputSimulateTransactionOptions; }): Promise> { diff --git a/src/internal/transactionSubmission.ts b/src/internal/transactionSubmission.ts index e52c0fafd..3c6e40929 100644 --- a/src/internal/transactionSubmission.ts +++ b/src/internal/transactionSubmission.ts @@ -227,7 +227,7 @@ export function signAsFeePayer(args: { signer: Account; transaction: AnyRawTrans /** * Simulates a transaction before singing it. * - * @param args.signerPublicKey The signer public key + * @param args.signerPublicKey optional. The signer public key * @param args.transaction The raw transaction to simulate * @param args.secondarySignersPublicKeys optional. For when the transaction is a multi signers transaction * @param args.feePayerPublicKey optional. For when the transaction is a fee payer (aka sponsored) transaction diff --git a/src/transactions/authenticator/account.ts b/src/transactions/authenticator/account.ts index fa9e5d5b9..f2128dad4 100644 --- a/src/transactions/authenticator/account.ts +++ b/src/transactions/authenticator/account.ts @@ -24,6 +24,8 @@ export abstract class AccountAuthenticator extends Serializable { return AccountAuthenticatorSingleKey.load(deserializer); case AccountAuthenticatorVariant.MultiKey: return AccountAuthenticatorMultiKey.load(deserializer); + case AccountAuthenticatorVariant.NoAccountAuthenticator: + return AccountAuthenticatorNoAccountAuthenticator.load(deserializer); default: throw new Error(`Unknown variant index for AccountAuthenticator: ${index}`); } @@ -169,3 +171,20 @@ export class AccountAuthenticatorMultiKey extends AccountAuthenticator { return new AccountAuthenticatorMultiKey(public_keys, signatures); } } + +/** + * AccountAuthenticatorNoAccountAuthenticator for no account authenticator + * It represents the absence of a public key for transaction simulation. + * It allows skipping the public/auth key check during the simulation. + */ +export class AccountAuthenticatorNoAccountAuthenticator extends AccountAuthenticator { + // eslint-disable-next-line class-methods-use-this + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(AccountAuthenticatorVariant.NoAccountAuthenticator); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static load(deserializer: Deserializer): AccountAuthenticatorNoAccountAuthenticator { + return new AccountAuthenticatorNoAccountAuthenticator(); + } +} diff --git a/src/transactions/transactionBuilder/transactionBuilder.ts b/src/transactions/transactionBuilder/transactionBuilder.ts index 79e23ee92..11ae07160 100644 --- a/src/transactions/transactionBuilder/transactionBuilder.ts +++ b/src/transactions/transactionBuilder/transactionBuilder.ts @@ -27,6 +27,7 @@ import { normalizeBundle } from "../../utils/normalizeBundle"; import { AccountAuthenticator, AccountAuthenticatorEd25519, + AccountAuthenticatorNoAccountAuthenticator, AccountAuthenticatorSingleKey, } from "../authenticator/account"; import { @@ -395,16 +396,18 @@ export function generateSignedTransactionForSimulation(args: InputSimulateTransa transaction.feePayerAddress, ); let secondaryAccountAuthenticators: Array = []; - if (secondarySignersPublicKeys) { - secondaryAccountAuthenticators = secondarySignersPublicKeys.map((publicKey) => - getAuthenticatorForSimulation(publicKey), - ); - } - if (!feePayerPublicKey) { - throw new Error( - "Must provide a feePayerPublicKey argument to generate a signed fee payer transaction for simulation", - ); + if (transaction.secondarySignerAddresses) { + if (secondarySignersPublicKeys) { + secondaryAccountAuthenticators = secondarySignersPublicKeys.map((publicKey) => + getAuthenticatorForSimulation(publicKey), + ); + } else { + secondaryAccountAuthenticators = new Array(transaction.secondarySignerAddresses.length) + .fill(undefined) + .map((publicKey) => getAuthenticatorForSimulation(publicKey)); + } } + const feePayerAuthenticator = getAuthenticatorForSimulation(feePayerPublicKey); const transactionAuthenticator = new TransactionAuthenticatorFeePayer( @@ -428,16 +431,16 @@ export function generateSignedTransactionForSimulation(args: InputSimulateTransa let secondaryAccountAuthenticators: Array = []; - if (!secondarySignersPublicKeys) { - throw new Error( - "Must provide a secondarySignersPublicKeys argument to generate a signed multi agent transaction for simulation", + if (secondarySignersPublicKeys) { + secondaryAccountAuthenticators = secondarySignersPublicKeys.map((publicKey) => + getAuthenticatorForSimulation(publicKey), ); + } else { + secondaryAccountAuthenticators = new Array(transaction.secondarySignerAddresses.length) + .fill(undefined) + .map((publicKey) => getAuthenticatorForSimulation(publicKey)); } - secondaryAccountAuthenticators = secondarySignersPublicKeys.map((publicKey) => - getAuthenticatorForSimulation(publicKey), - ); - const transactionAuthenticator = new TransactionAuthenticatorMultiAgent( accountAuthenticator, transaction.secondarySignerAddresses, @@ -456,13 +459,18 @@ export function generateSignedTransactionForSimulation(args: InputSimulateTransa ); } else if (accountAuthenticator instanceof AccountAuthenticatorSingleKey) { transactionAuthenticator = new TransactionAuthenticatorSingleSender(accountAuthenticator); + } else if (accountAuthenticator instanceof AccountAuthenticatorNoAccountAuthenticator) { + transactionAuthenticator = new TransactionAuthenticatorSingleSender(accountAuthenticator); } else { throw new Error("Invalid public key"); } return new SignedTransaction(transaction.rawTransaction, transactionAuthenticator).bcsToBytes(); } -export function getAuthenticatorForSimulation(publicKey: PublicKey) { +export function getAuthenticatorForSimulation(publicKey?: PublicKey) { + if (!publicKey) { + return new AccountAuthenticatorNoAccountAuthenticator(); + } // Wrap the public key types below with AnyPublicKey as they are only support through single sender. // Learn more about AnyPublicKey here - https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-55.md const convertToAnyPublicKey = diff --git a/src/transactions/types.ts b/src/transactions/types.ts index 53d05073e..6fdfce4d5 100644 --- a/src/transactions/types.ts +++ b/src/transactions/types.ts @@ -279,12 +279,13 @@ export type InputSimulateTransactionData = { transaction: AnyRawTransaction; /** * For a single signer transaction + * This is optional and can be undefined to skip the public/auth key check during the transaction simulation. */ - signerPublicKey: PublicKey; + signerPublicKey?: PublicKey; /** * For a fee payer or multi-agent transaction that requires additional signers in */ - secondarySignersPublicKeys?: Array; + secondarySignersPublicKeys?: Array; /** * For a fee payer transaction (aka Sponsored Transaction) */ diff --git a/src/types/index.ts b/src/types/index.ts index 032951fce..4d28fa191 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -104,6 +104,7 @@ export enum AccountAuthenticatorVariant { MultiEd25519 = 1, SingleKey = 2, MultiKey = 3, + NoAccountAuthenticator = 4, } export enum AnyPublicKeyVariant { diff --git a/tests/e2e/transaction/transactionSimulation.test.ts b/tests/e2e/transaction/transactionSimulation.test.ts index 87169e1e4..c2b8bdd28 100644 --- a/tests/e2e/transaction/transactionSimulation.test.ts +++ b/tests/e2e/transaction/transactionSimulation.test.ts @@ -1,4 +1,4 @@ -import { Account, U64, SigningSchemeInput, InputEntryFunctionData } from "../../../src"; +import { Account, AccountAddress, U64, SigningSchemeInput, InputEntryFunctionData } from "../../../src"; import { longTestTimeout } from "../../unit/helper"; import { getAptosClient } from "../helper"; import { @@ -586,7 +586,7 @@ describe("transaction simulation", () => { }); }); describe("validate fee payer data on transaction simulation", () => { - test("it throws when trying to simluate a fee payer transaction without the feePayerPublicKey", async () => { + test("simluate a fee payer transaction without the feePayerPublicKey", async () => { const rawTxn = await aptos.transaction.build.simple({ sender: singleSignerSecp256k1Account.accountAddress, data: { @@ -597,15 +597,14 @@ describe("transaction simulation", () => { }); rawTxn.feePayerAddress = feePayerAccount.accountAddress; - await expect( - aptos.transaction.simulate.simple({ - signerPublicKey: singleSignerSecp256k1Account.publicKey, - transaction: rawTxn, - }), - ).rejects.toThrow(); + const [response] = await aptos.transaction.simulate.simple({ + signerPublicKey: singleSignerSecp256k1Account.publicKey, + transaction: rawTxn, + }); + expect(response.success).toBeTruthy(); }); - test("it throws when trying to simluate a multi agent fee payer transaction without the feePayerPublicKey", async () => { + test("simluate a multi agent fee payer transaction without the feePayerPublicKey", async () => { const rawTxn = await aptos.transaction.build.multiAgent({ sender: singleSignerSecp256k1Account.accountAddress, secondarySignerAddresses: [secondarySignerAccount.accountAddress], @@ -617,13 +616,116 @@ describe("transaction simulation", () => { }); rawTxn.feePayerAddress = feePayerAccount.accountAddress; - await expect( - aptos.transaction.simulate.multiAgent({ - signerPublicKey: singleSignerSecp256k1Account.publicKey, - transaction: rawTxn, - secondarySignersPublicKeys: [secondarySignerAccount.publicKey], - }), - ).rejects.toThrow(); + const [response] = await aptos.transaction.simulate.multiAgent({ + signerPublicKey: singleSignerSecp256k1Account.publicKey, + transaction: rawTxn, + secondarySignersPublicKeys: [secondarySignerAccount.publicKey], + }); + expect(response.vm_status).toContain("NUMBER_OF_SIGNER_ARGUMENTS_MISMATCH"); + }); + }); + + describe("simulations with no account authenticator", () => { + test("single signer with script payload", async () => { + const transaction = await aptos.transaction.build.simple({ + sender: singleSignerED25519SenderAccount.accountAddress, + data: { + bytecode: singleSignerScriptBytecode, + functionArguments: [new U64(1), receiverAccounts[0].accountAddress], + }, + }); + const [response] = await aptos.transaction.simulate.simple({ + transaction, + }); + expect(response.success).toBeTruthy(); + }); + }); + test("fee payer with script payload", async () => { + const rawTxn = await aptos.transaction.build.simple({ + sender: legacyED25519SenderAccount.accountAddress, + data: { + bytecode: singleSignerScriptBytecode, + functionArguments: [new U64(1), receiverAccounts[0].accountAddress], + }, + withFeePayer: true, + }); + rawTxn.feePayerAddress = feePayerAccount.accountAddress; + + const [response] = await aptos.transaction.simulate.simple({ + transaction: rawTxn, + }); + expect(response.success).toBeTruthy(); + }); + test("fee payer as 0x0 with script payload", async () => { + const rawTxn = await aptos.transaction.build.simple({ + sender: legacyED25519SenderAccount.accountAddress, + data: { + bytecode: singleSignerScriptBytecode, + functionArguments: [new U64(1), receiverAccounts[0].accountAddress], + }, + withFeePayer: true, + }); + // Note that the rawTxn.feePayerAddress is 0x0 by default. + + const [response] = await aptos.transaction.simulate.simple({ + transaction: rawTxn, + }); + expect(response.success).toBeTruthy(); + }); + test("fee payer as 0x4 with script payload", async () => { + const rawTxn = await aptos.transaction.build.simple({ + sender: legacyED25519SenderAccount.accountAddress, + data: { + bytecode: singleSignerScriptBytecode, + functionArguments: [new U64(1), receiverAccounts[0].accountAddress], + }, + withFeePayer: true, + }); + // 0x4 is a fee payer who does not have a sufficient fund. + rawTxn.feePayerAddress = AccountAddress.FOUR; + + const [response] = await aptos.transaction.simulate.simple({ + transaction: rawTxn, + }); + expect(response.vm_status).toContain("INSUFFICIENT_BALANCE_FOR_TRANSACTION_FEE"); + }); + test("with multi agent transaction without providing the secondary signer public key", async () => { + const rawTxn = await aptos.transaction.build.multiAgent({ + sender: legacyED25519SenderAccount.accountAddress, + secondarySignerAddresses: [secondarySignerAccount.accountAddress], + data: { + function: `${contractPublisherAccount.accountAddress}::transfer::two_by_two`, + functionArguments: [100, 200, receiverAccounts[0].accountAddress, receiverAccounts[1].accountAddress, 50], + }, + withFeePayer: true, + }); + rawTxn.feePayerAddress = feePayerAccount.accountAddress; + + const [response] = await aptos.transaction.simulate.multiAgent({ + signerPublicKey: legacyED25519SenderAccount.publicKey, + transaction: rawTxn, + secondarySignersPublicKeys: [undefined], + feePayerPublicKey: feePayerAccount.publicKey, + }); + expect(response.success).toBeTruthy(); + }); + test("with multi agent transaction without providing the secondary signer public key array", async () => { + const rawTxn = await aptos.transaction.build.multiAgent({ + sender: legacyED25519SenderAccount.accountAddress, + secondarySignerAddresses: [secondarySignerAccount.accountAddress], + data: { + function: `${contractPublisherAccount.accountAddress}::transfer::two_by_two`, + functionArguments: [100, 200, receiverAccounts[0].accountAddress, receiverAccounts[1].accountAddress, 50], + }, + withFeePayer: true, + }); + rawTxn.feePayerAddress = feePayerAccount.accountAddress; + + const [response] = await aptos.transaction.simulate.multiAgent({ + signerPublicKey: legacyED25519SenderAccount.publicKey, + transaction: rawTxn, + feePayerPublicKey: feePayerAccount.publicKey, }); + expect(response.success).toBeTruthy(); }); }); diff --git a/tests/e2e/transaction/transactionSubmission.test.ts b/tests/e2e/transaction/transactionSubmission.test.ts index cbea950af..d9536c9ed 100644 --- a/tests/e2e/transaction/transactionSubmission.test.ts +++ b/tests/e2e/transaction/transactionSubmission.test.ts @@ -17,6 +17,7 @@ import { MAX_U64_BIG_INT } from "../../../src/bcs/consts"; import { longTestTimeout } from "../../unit/helper"; import { getAptosClient } from "../helper"; import { fundAccounts, multiSignerScriptBytecode, publishTransferPackage, singleSignerScriptBytecode } from "./helper"; +import { AccountAuthenticatorNoAccountAuthenticator } from "../../../src/transactions"; const { aptos } = getAptosClient(); @@ -887,4 +888,32 @@ describe("transaction submission", () => { expect(submittedTransaction.success).toBe(true); }); }); + + describe("transactions with no account authenticator", () => { + test("it fails to submit a transaction when authenticator in not provided", async () => { + const transaction = await aptos.transaction.build.simple({ + sender: singleSignerED25519SenderAccount.accountAddress, + data: { + bytecode: singleSignerScriptBytecode, + functionArguments: [new U64(1), receiverAccounts[0].accountAddress], + }, + }); + const authenticator = new AccountAuthenticatorNoAccountAuthenticator(); + try { + const response = await aptos.transaction.submit.simple({ + transaction, + senderAuthenticator: authenticator, + }); + await aptos.waitForTransaction({ + transactionHash: response.hash, + }); + fail("Expected an error to be thrown"); + } catch (error: any) { + const errorStr = error.toString(); + expect(errorStr).toContain("Invalid transaction"); + expect(errorStr).toContain("INVALID_SIGNATURE"); + expect(errorStr).toContain("vm_error"); + } + }); + }); });