From 8f9f28db0f83311011d0ac8af367fa12f5d11572 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 24 Jul 2024 13:33:58 +0100 Subject: [PATCH 001/135] feat: intitial support for deploying large contract via blob tx --- internal/forc/VERSION | 2 +- internal/fuel-core/VERSION | 2 +- .../src/providers/fuel-core-schema.graphql | 9 ++ packages/account/src/providers/provider.ts | 11 +- .../blob-transaction-request.ts | 62 ++++++++++ .../transaction-request.ts | 9 +- .../providers/transaction-request/types.ts | 12 +- .../assemble-transaction-summary.test.ts | 4 + .../assemble-transaction-summary.ts | 2 + .../transaction-summary/operations.test.ts | 6 + .../transaction-summary/operations.ts | 7 ++ .../providers/transaction-summary/types.ts | 2 + packages/contract/src/contract-factory.ts | 111 ++++++++++++++--- packages/contract/src/util.ts | 3 - .../fuel-gauge/src/contract-factory.test.ts | 26 +++- .../src/transaction-response.test.ts | 1 + .../src/transaction-summary.test.ts | 1 + .../test/fixtures/forc-projects/Forc.toml | 1 + .../forc-projects/large-contract/Forc.toml | 7 ++ .../forc-projects/large-contract/src/main.sw | 14 +++ .../src/coders/transaction.test.ts | 36 ++++++ .../transactions/src/coders/transaction.ts | 115 +++++++++++++++++- 22 files changed, 409 insertions(+), 34 deletions(-) create mode 100644 packages/account/src/providers/transaction-request/blob-transaction-request.ts create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/large-contract/Forc.toml create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw diff --git a/internal/forc/VERSION b/internal/forc/VERSION index 4d74f323830..645b2e0a0c5 100644 --- a/internal/forc/VERSION +++ b/internal/forc/VERSION @@ -1 +1 @@ -0.62.0 +git:kayagokalp/proxy-with-chunks \ No newline at end of file diff --git a/internal/fuel-core/VERSION b/internal/fuel-core/VERSION index 26bea73e811..6256560ecdb 100644 --- a/internal/fuel-core/VERSION +++ b/internal/fuel-core/VERSION @@ -1 +1 @@ -0.31.0 +git:dento/blob-tx \ No newline at end of file diff --git a/packages/account/src/providers/fuel-core-schema.graphql b/packages/account/src/providers/fuel-core-schema.graphql index dca24b863a2..b2a4b135b31 100644 --- a/packages/account/src/providers/fuel-core-schema.graphql +++ b/packages/account/src/providers/fuel-core-schema.graphql @@ -63,6 +63,8 @@ input BalanceFilterInput { owner: Address! } +scalar BlobId + type Block { version: BlockVersion! id: BlockId! @@ -974,6 +976,11 @@ type Query { """ estimatePredicates(tx: HexString!): Transaction! + """ + Returns all possible receipts for test purposes. + """ + allReceipts: [Receipt!]! + """ Returns true when the GraphQL API is serving requests. """ @@ -1268,6 +1275,7 @@ type Transaction { isMint: Boolean! isUpgrade: Boolean! isUpload: Boolean! + isBlob: Boolean! inputs: [Input!] outputs: [Output!]! outputContract: ContractOutput @@ -1277,6 +1285,7 @@ type Transaction { script: HexString scriptData: HexString bytecodeWitnessIndex: U16 + blobId: BlobId salt: Salt storageSlots: [HexString!] bytecodeRoot: Bytes32 diff --git a/packages/account/src/providers/provider.ts b/packages/account/src/providers/provider.ts index 093783d64a5..4d200368223 100644 --- a/packages/account/src/providers/provider.ts +++ b/packages/account/src/providers/provider.ts @@ -45,10 +45,9 @@ import type { TransactionRequest, TransactionRequestInput, CoinTransactionRequestInput, - ScriptTransactionRequest, JsonAbisFromAllCalls, } from './transaction-request'; -import { transactionRequestify } from './transaction-request'; +import { ScriptTransactionRequest, transactionRequestify } from './transaction-request'; import type { TransactionResultReceipt } from './transaction-response'; import { TransactionResponse, getDecodedLogs } from './transaction-response'; import { processGqlReceipt } from './transaction-summary/receipt'; @@ -865,7 +864,7 @@ Supported fuel-core version: ${supportedVersion}.` const hasMissingOutputs = missingOutputVariables.length !== 0 || missingOutputContractIds.length !== 0; - if (hasMissingOutputs) { + if (hasMissingOutputs && transactionRequest instanceof ScriptTransactionRequest) { outputVariables += missingOutputVariables.length; transactionRequest.addVariableOutputs(missingOutputVariables.length); missingOutputContractIds.forEach(({ contractId }) => { @@ -951,7 +950,7 @@ Supported fuel-core version: ${supportedVersion}.` const hasMissingOutputs = missingOutputVariables.length > 0 || missingOutputContractIds.length > 0; const request = allRequests[requestIdx]; - if (hasMissingOutputs && request?.type === TransactionType.Script) { + if (hasMissingOutputs && request instanceof ScriptTransactionRequest) { result.outputVariables += missingOutputVariables.length; request.addVariableOutputs(missingOutputVariables.length); missingOutputContractIds.forEach(({ contractId }) => { @@ -1031,7 +1030,7 @@ Supported fuel-core version: ${supportedVersion}.` let gasLimit = bn(0); // Only Script transactions consume gas - if (transactionRequest.type === TransactionType.Script) { + if (transactionRequest instanceof ScriptTransactionRequest) { // If the gasLimit is set to 0, it means we need to estimate it. gasLimit = transactionRequest.gasLimit; if (transactionRequest.gasLimit.eq(0)) { @@ -1128,7 +1127,7 @@ Supported fuel-core version: ${supportedVersion}.` { signatureCallback }: TransactionCostParams = {} ): Promise> { const txRequestClone = clone(transactionRequestify(transactionRequestLike)); - const isScriptTransaction = txRequestClone.type === TransactionType.Script; + const isScriptTransaction = txRequestClone instanceof ScriptTransactionRequest; const updateMaxFee = txRequestClone.maxFee.eq(0); // Remove gasLimit to avoid gasLimit when estimating predicates diff --git a/packages/account/src/providers/transaction-request/blob-transaction-request.ts b/packages/account/src/providers/transaction-request/blob-transaction-request.ts new file mode 100644 index 00000000000..aaaf43556da --- /dev/null +++ b/packages/account/src/providers/transaction-request/blob-transaction-request.ts @@ -0,0 +1,62 @@ +import { hash } from '@fuel-ts/hasher'; +import type { TransactionBlob } from '@fuel-ts/transactions'; + +import { hashTransaction } from './hash-transaction'; +import type { BaseTransactionRequestLike } from './transaction-request'; +import { BaseTransactionRequest, TransactionType } from './transaction-request'; + +export interface BlobTransactionRequestLike extends BaseTransactionRequestLike { + /** Witness index of contract bytecode to create */ + bytecodeWitnessIndex?: number; +} + +export class BlobTransactionRequest extends BaseTransactionRequest { + static from(obj: BlobTransactionRequestLike) { + if (obj instanceof this) { + return obj; + } + return new this(obj); + } + + /** Type of the transaction */ + type = TransactionType.Blob as const; + /** Witness index of contract bytecode to create */ + bytecodeWitnessIndex: number; + + /** + * Creates an instance `CreateTransactionRequest`. + * + * @param createTransactionRequestLike - The initial values for the instance + */ + constructor({ bytecodeWitnessIndex, ...rest }: BlobTransactionRequestLike) { + super(rest); + this.bytecodeWitnessIndex = bytecodeWitnessIndex ?? 0; + } + + /** + * Converts the transaction request to a `TransactionBlob`. + * + * @returns The transaction create object. + */ + toTransaction(): TransactionBlob { + const baseTransaction = this.getBaseTransaction(); + const bytecodeWitnessIndex = this.bytecodeWitnessIndex; + return { + type: TransactionType.Blob, + ...baseTransaction, + blobId: hash(this.witnesses[bytecodeWitnessIndex]), + bytecodeWitnessIndex, + }; + } + + /** + * Gets the Transaction Request by hashing the transaction. + * + * @param chainId - The chain ID. + * + * @returns - A hash of the transaction, which is the transaction ID. + */ + getTransactionId(chainId: number): string { + return hashTransaction(this, chainId); + } +} diff --git a/packages/account/src/providers/transaction-request/transaction-request.ts b/packages/account/src/providers/transaction-request/transaction-request.ts index b3a894f7ef9..df903f200da 100644 --- a/packages/account/src/providers/transaction-request/transaction-request.ts +++ b/packages/account/src/providers/transaction-request/transaction-request.ts @@ -5,7 +5,12 @@ import { randomBytes } from '@fuel-ts/crypto'; import type { AddressLike, AbstractAddress, BytesLike } from '@fuel-ts/interfaces'; import type { BN, BigNumberish } from '@fuel-ts/math'; import { bn } from '@fuel-ts/math'; -import type { TransactionScript, Policy, TransactionCreate } from '@fuel-ts/transactions'; +import type { + TransactionScript, + Policy, + TransactionCreate, + TransactionBlob, +} from '@fuel-ts/transactions'; import { PolicyType, TransactionCoder, @@ -186,7 +191,7 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi }; } - abstract toTransaction(): TransactionCreate | TransactionScript; + abstract toTransaction(): TransactionCreate | TransactionScript | TransactionBlob; /** * Converts the transaction request to a byte array. diff --git a/packages/account/src/providers/transaction-request/types.ts b/packages/account/src/providers/transaction-request/types.ts index fe271296ca5..10569374f0c 100644 --- a/packages/account/src/providers/transaction-request/types.ts +++ b/packages/account/src/providers/transaction-request/types.ts @@ -1,6 +1,10 @@ import type { JsonAbi } from '@fuel-ts/abi-coder'; import type { TransactionType } from '@fuel-ts/transactions'; +import type { + BlobTransactionRequest, + BlobTransactionRequestLike, +} from './blob-transaction-request'; import type { CreateTransactionRequest, CreateTransactionRequestLike, @@ -10,10 +14,14 @@ import type { ScriptTransactionRequestLike, } from './script-transaction-request'; -export type TransactionRequest = ScriptTransactionRequest | CreateTransactionRequest; +export type TransactionRequest = + | ScriptTransactionRequest + | CreateTransactionRequest + | BlobTransactionRequest; export type TransactionRequestLike = | ({ type: TransactionType.Script } & ScriptTransactionRequestLike) - | ({ type: TransactionType.Create } & CreateTransactionRequestLike); + | ({ type: TransactionType.Create } & CreateTransactionRequestLike) + | ({ type: TransactionType.Blob } & BlobTransactionRequestLike); export type JsonAbisFromAllCalls = { main: JsonAbi; diff --git a/packages/account/src/providers/transaction-summary/assemble-transaction-summary.test.ts b/packages/account/src/providers/transaction-summary/assemble-transaction-summary.test.ts index 310ba48daa8..a54afd0f9a3 100644 --- a/packages/account/src/providers/transaction-summary/assemble-transaction-summary.test.ts +++ b/packages/account/src/providers/transaction-summary/assemble-transaction-summary.test.ts @@ -99,6 +99,7 @@ describe('TransactionSummary', () => { isTypeCreate: false, isTypeMint: false, isTypeScript: true, + isTypeBlob: false, isStatusPending: false, isStatusFailure: false, isStatusSuccess: true, @@ -121,6 +122,7 @@ describe('TransactionSummary', () => { gasUsed: expect.any(BN), isTypeCreate: false, isTypeMint: false, + isTypeBlob: false, isTypeScript: true, isStatusPending: false, isStatusFailure: true, @@ -145,6 +147,7 @@ describe('TransactionSummary', () => { isTypeCreate: false, isTypeMint: false, isTypeScript: true, + isTypeBlob: false, isStatusPending: true, isStatusFailure: false, isStatusSuccess: false, @@ -168,6 +171,7 @@ describe('TransactionSummary', () => { isTypeCreate: false, isTypeMint: false, isTypeScript: true, + isTypeBlob: false, isStatusPending: false, isStatusFailure: false, isStatusSuccess: false, diff --git a/packages/account/src/providers/transaction-summary/assemble-transaction-summary.ts b/packages/account/src/providers/transaction-summary/assemble-transaction-summary.ts index cbf69527c60..0b003eb5c28 100644 --- a/packages/account/src/providers/transaction-summary/assemble-transaction-summary.ts +++ b/packages/account/src/providers/transaction-summary/assemble-transaction-summary.ts @@ -15,6 +15,7 @@ import { isTypeScript, isTypeUpgrade, isTypeUpload, + isTypeBlob, } from './operations'; import { extractBurnedAssetsFromReceipts, extractMintedAssetsFromReceipts } from './receipt'; import { processGraphqlStatus } from './status'; @@ -120,6 +121,7 @@ export function assembleTransactionSummary( isTypeScript: isTypeScript(transaction.type), isTypeUpgrade: isTypeUpgrade(transaction.type), isTypeUpload: isTypeUpload(transaction.type), + isTypeBlob: isTypeBlob(transaction.type), isStatusFailure, isStatusSuccess, isStatusPending, diff --git a/packages/account/src/providers/transaction-summary/operations.test.ts b/packages/account/src/providers/transaction-summary/operations.test.ts index cf20482aca0..a7fe8c48d07 100644 --- a/packages/account/src/providers/transaction-summary/operations.test.ts +++ b/packages/account/src/providers/transaction-summary/operations.test.ts @@ -42,6 +42,7 @@ import { getTransactionTypeName, getWithdrawFromFuelOperations, isType, + isTypeBlob, isTypeCreate, isTypeMint, isTypeScript, @@ -828,6 +829,7 @@ describe('operations', () => { expect(isType(TransactionType.Script, TransactionTypeName.Create)).toBeFalsy(); expect(isType(TransactionType.Mint, TransactionTypeName.Script)).toBeFalsy(); expect(isType(TransactionType.Create, TransactionTypeName.Mint)).toBeFalsy(); + expect(isType(TransactionType.Blob, TransactionTypeName.Blob)).toBeTruthy(); }); it('should isTypeMint return if is mint', () => { @@ -847,6 +849,10 @@ describe('operations', () => { expect(isTypeScript(TransactionType.Mint)).toBeFalsy(); expect(isTypeScript(TransactionType.Create)).toBeFalsy(); }); + + it('should isTypeBlob return if is Blob', () => { + expect(isTypeBlob(TransactionType.Blob)).toBeTruthy(); + }); }); describe('getReceipts', () => { diff --git a/packages/account/src/providers/transaction-summary/operations.ts b/packages/account/src/providers/transaction-summary/operations.ts index 17e4f677302..e502f6f8695 100644 --- a/packages/account/src/providers/transaction-summary/operations.ts +++ b/packages/account/src/providers/transaction-summary/operations.ts @@ -55,6 +55,8 @@ export function getTransactionTypeName(transactionType: TransactionType): Transa return TransactionTypeName.Create; case TransactionType.Script: return TransactionTypeName.Script; + case TransactionType.Blob: + return TransactionTypeName.Blob; default: throw new FuelError( ErrorCode.UNSUPPORTED_TRANSACTION_TYPE, @@ -95,6 +97,11 @@ export function isTypeUpload(transactionType: TransactionType) { return isType(transactionType, TransactionTypeName.Upload); } +/** @hidden */ +export function isTypeBlob(transactionType: TransactionType) { + return isType(transactionType, TransactionTypeName.Blob); +} + /** @hidden */ export function hasSameAssetId(a: OperationCoin) { return (b: OperationCoin) => a.assetId === b.assetId; diff --git a/packages/account/src/providers/transaction-summary/types.ts b/packages/account/src/providers/transaction-summary/types.ts index 74bf3de02b2..185ee7dc7f3 100644 --- a/packages/account/src/providers/transaction-summary/types.ts +++ b/packages/account/src/providers/transaction-summary/types.ts @@ -35,6 +35,7 @@ export enum TransactionTypeName { Script = 'Script', Upgrade = 'Upgrade', Upload = 'Upload', + Blob = 'Blob', } /** @@ -170,6 +171,7 @@ export type TransactionSummary = { isTypeScript: boolean; isTypeUpgrade: boolean; isTypeUpload: boolean; + isTypeBlob: boolean; isStatusPending: boolean; isStatusSuccess: boolean; isStatusFailure: boolean; diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index 0de99deb295..fc540c0b861 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -8,19 +8,16 @@ import type { TransactionType, } from '@fuel-ts/account'; import { CreateTransactionRequest } from '@fuel-ts/account'; +import { BlobTransactionRequest } from '@fuel-ts/account/dist/providers/transaction-request/blob-transaction-request'; import { randomBytes } from '@fuel-ts/crypto'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; import type { BytesLike } from '@fuel-ts/interfaces'; +import { SparseMerkleTree } from '@fuel-ts/merkle'; import { Contract } from '@fuel-ts/program'; import type { StorageSlot } from '@fuel-ts/transactions'; import { arrayify, isDefined } from '@fuel-ts/utils'; -import { - MAX_CONTRACT_SIZE, - getContractId, - getContractStorageRoot, - hexlifyWithPrefix, -} from './util'; +import { getContractId, getContractStorageRoot, hexlifyWithPrefix } from './util'; /** * Options for deploying a contract. @@ -41,11 +38,17 @@ export type DeployContractResult = { }>; }; +export type ContractChunk = { + id: number; + size: number; + bytecode: Uint8Array; +}; + /** * `ContractFactory` provides utilities for deploying and configuring contracts. */ export default class ContractFactory { - bytecode: BytesLike; + bytecode: Uint8Array; interface: Interface; provider!: Provider | null; account!: Account | null; @@ -154,16 +157,94 @@ export default class ContractFactory { async deployContract( deployContractOptions: DeployContractOptions = {} ): Promise> { - if (this.bytecode.length > MAX_CONTRACT_SIZE) { - throw new FuelError( - ErrorCode.CONTRACT_SIZE_EXCEEDS_LIMIT, - 'Contract bytecode is too large. Max contract size is 100KB' - ); - } + const account = this.getAccount(); - const { contractId, transactionRequest } = await this.prepareDeploy(deployContractOptions); + const { consensusParameters } = account.provider.getChain(); + const maxContractSize = consensusParameters.contractParameters.contractMaxSize.toNumber(); - const account = this.getAccount(); + let contractId: string; + let transactionRequest: CreateTransactionRequest; + + // Checker whether the contract needs to be chunked + if (this.bytecode.length > maxContractSize) { + // Ensure the max contract size is byte aligned (VM requirement) + if (maxContractSize % 8 !== 0) { + throw new FuelError( + ErrorCode.CONTRACT_SIZE_EXCEEDS_LIMIT, // Todo: change error + `Max contract size of ${maxContractSize} bytes is not byte aligned.` + ); + } + + // Set configurables + const { configurableConstants } = deployContractOptions; + const hasConfigurable = Object.keys(this.interface.configurables).length; + if (hasConfigurable && configurableConstants) { + this.setConfigurableConstants(configurableConstants); + } + + // Chunk the bytecode + const chunks: ContractChunk[] = []; + const chunkSize = maxContractSize; + for (let offset = 0, index = 0; offset < this.bytecode.length; offset += chunkSize, index++) { + const chunk = this.bytecode.slice(offset, offset + chunkSize); + chunks.push({ id: index, size: chunk.length, bytecode: chunk }); + } + + // Deploy chunk as blob tx + const blobIds: string[] = []; + const { maxFee: setMaxFee } = deployContractOptions; + for (const { bytecode } of chunks) { + const blobTxRequest = new BlobTransactionRequest({ + bytecodeWitnessIndex: 0, + witnesses: [bytecode], + }); + + const txCost = await account.getTransactionCost(blobTxRequest); + + if (isDefined(setMaxFee)) { + if (txCost.maxFee.gt(setMaxFee)) { + throw new FuelError( + ErrorCode.MAX_FEE_TOO_LOW, + `Max fee '${deployContractOptions.maxFee}' is lower than the required: '${txCost.maxFee}'.` + ); + } + } else { + blobTxRequest.maxFee = txCost.maxFee; + } + + await account.fund(blobTxRequest, txCost); + + const response = await account.sendTransaction(blobTxRequest, { awaitExecution: true }); + const { id } = await response.waitForResult(); + if (!id) { + throw new Error('Blob ID not returned'); + } + blobIds.push(id); + } + + // Deploy contract via loader contract + // 1. Get bytes for loader contract + // 2. Encode byteIds as function arguments + + // this.bytecode should be from encoded loader + const salt = deployContractOptions.salt || randomBytes(32); + const stateRoot = deployContractOptions.stateRoot || getContractStorageRoot([]); + + contractId = getContractId(this.bytecode, salt, stateRoot); + const createTxRequest = new CreateTransactionRequest({ + bytecodeWitnessIndex: 0, + witnesses: [this.bytecode], + ...deployContractOptions, + }); + createTxRequest.addContractCreatedOutput(contractId, stateRoot); + + transactionRequest = createTxRequest; + } else { + const { contractId: id, transactionRequest: req } = + await this.prepareDeploy(deployContractOptions); + contractId = id; + transactionRequest = req; + } const transactionResponse = await account.sendTransaction(transactionRequest, { awaitExecution: false, diff --git a/packages/contract/src/util.ts b/packages/contract/src/util.ts index e540d55235f..610f7b73ff1 100644 --- a/packages/contract/src/util.ts +++ b/packages/contract/src/util.ts @@ -4,9 +4,6 @@ import { calcRoot, SparseMerkleTree } from '@fuel-ts/merkle'; import type { StorageSlot } from '@fuel-ts/transactions'; import { chunkAndPadBytes, hexlify, concat, arrayify } from '@fuel-ts/utils'; -// Max contract size in bytes is 100KB -export const MAX_CONTRACT_SIZE = 102400; - /** * @hidden * diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index 03e918c17ee..0489c3c4187 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -4,7 +4,12 @@ import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import { BN, bn, toHex, Interface, ContractFactory } from 'fuels'; import { launchTestNode } from 'fuels/test-utils'; -import { StorageTestContractAbi__factory } from '../test/typegen/contracts'; +import type { LargeContractAbi } from '../test/typegen/contracts'; +import { + StorageTestContractAbi__factory, + LargeContractAbi__factory, +} from '../test/typegen/contracts'; +import largeContractHex from '../test/typegen/contracts/LargeContractAbi.hex'; import StorageTestContractAbiHex from '../test/typegen/contracts/StorageTestContractAbi.hex'; import { launchTestContract } from './utils'; @@ -62,6 +67,7 @@ describe('Contract Factory', () => { isTypeUpgrade: expect.any(Boolean), isTypeUpload: expect.any(Boolean), isTypeScript: expect.any(Boolean), + isTypeBlob: expect.any(Boolean), logs: expect.any(Array), date: expect.any(Date), mintedAssets: expect.any(Array), @@ -254,7 +260,7 @@ describe('Contract Factory', () => { wallets: [wallet], } = launched; - const largeByteCode = `0x${'00'.repeat(112400)}`; + const largeByteCode = `0x${'00'.repeat(265144)}`; const factory = new ContractFactory(largeByteCode, StorageTestContractAbi__factory.abi, wallet); await expectToThrowFuelError( @@ -265,4 +271,20 @@ describe('Contract Factory', () => { ) ); }); + + it('should deploy contracts greater than 100KB in chunks', async () => { + using launched = await launchTestNode(); + const { + wallets: [wallet], + } = launched; + + const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); + + const { waitForResult: waitForDeployResult } = await factory.deployContract(); + const { contract } = await waitForDeployResult(); + + const { waitForResult } = await contract.functions.gen().call(); + const { value } = await waitForResult(); + expect(value).toBe(true); + }); }); diff --git a/packages/fuel-gauge/src/transaction-response.test.ts b/packages/fuel-gauge/src/transaction-response.test.ts index 1ad07b059b2..352321616d4 100644 --- a/packages/fuel-gauge/src/transaction-response.test.ts +++ b/packages/fuel-gauge/src/transaction-response.test.ts @@ -140,6 +140,7 @@ describe('TransactionResponse', () => { expect(transactionSummary.mintedAssets).toBeDefined(); expect(transactionSummary.burnedAssets).toBeDefined(); expect(transactionSummary.isTypeMint).toBeDefined(); + expect(transactionSummary.isTypeBlob).toBeDefined(); expect(transactionSummary.isTypeCreate).toBeDefined(); expect(transactionSummary.isTypeScript).toBeDefined(); expect(transactionSummary.isStatusFailure).toBeDefined(); diff --git a/packages/fuel-gauge/src/transaction-summary.test.ts b/packages/fuel-gauge/src/transaction-summary.test.ts index 89bfae3935a..75fbcaaa708 100644 --- a/packages/fuel-gauge/src/transaction-summary.test.ts +++ b/packages/fuel-gauge/src/transaction-summary.test.ts @@ -41,6 +41,7 @@ describe('TransactionSummary', () => { expect(transaction.isTypeMint).toBe(false); expect(transaction.isTypeCreate).toBe(false); expect(transaction.isTypeScript).toBe(true); + expect(transaction.isTypeBlob).toBe(false); expect(transaction.isStatusFailure).toBe(false); expect(transaction.isStatusSuccess).toBe(!isRequest); expect(transaction.isStatusPending).toBe(false); diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml index b017851d107..2dcf604f5d3 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml +++ b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml @@ -13,6 +13,7 @@ members = [ "configurable-contract", "coverage-contract", "generic-types-contract", + "large-contract", "multi-token-contract", "payable-annotation", "predicate-address", diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/Forc.toml new file mode 100644 index 00000000000..09e85255d65 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "large-contract" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw new file mode 100644 index 00000000000..d6345f48436 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw @@ -0,0 +1,14 @@ +contract; + +abi LargeContract { + fn gen() -> bool; +} + +impl LargeContract for Contract { + fn gen() -> bool { + asm() { + blob i500000; + } + true + } +} diff --git a/packages/transactions/src/coders/transaction.test.ts b/packages/transactions/src/coders/transaction.test.ts index c4ca284623e..c4cb10f4a40 100644 --- a/packages/transactions/src/coders/transaction.test.ts +++ b/packages/transactions/src/coders/transaction.test.ts @@ -531,4 +531,40 @@ describe('TransactionCoder', () => { JSON.parse(JSON.stringify(transaction)) ); }); + + it('can encode/decode TransactionBlob', () => { + const transaction: Transaction = { + type: TransactionType.Blob, + blobId: B256, + witnessIndex: U16, + policyTypes: 5, + inputsCount: 0, + outputsCount: 0, + witnessesCount: 1, + policies: [ + { type: PolicyType.Tip, data: bn(U32) }, + { type: PolicyType.Maturity, data: U32 }, + ], + inputs: [], + outputs: [], + witnesses: [ + { + dataLength: 1, + data: '0x01', + }, + ], + }; + + const encoded = hexlify(new TransactionCoder().encode(transaction)); + expect(encoded).toEqual( + '0x0000000000000005d5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b0000000000000384000000000000000500000000000000000000000000000000000000000000000100000000000003e800000000000003e800000000000000010100000000000000' + ); + + const [decoded, offset] = new TransactionCoder().decode(arrayify(encoded), 0); + + expect(offset).toEqual((encoded.length - 2) / 2); + expect(JSON.parse(JSON.stringify(decoded))).toMatchObject( + JSON.parse(JSON.stringify(transaction)) + ); + }); }); diff --git a/packages/transactions/src/coders/transaction.ts b/packages/transactions/src/coders/transaction.ts index efca5d0e472..2b170b1e1df 100644 --- a/packages/transactions/src/coders/transaction.ts +++ b/packages/transactions/src/coders/transaction.ts @@ -26,6 +26,7 @@ export enum TransactionType /* u8 */ { Mint = 2, Upgrade = 3, Upload = 4, + Blob = 5, } export type TransactionScript = { @@ -568,12 +569,113 @@ export class TransactionUploadCoder extends Coder { + constructor() { + super('TransactionBlob', 'struct TransactionBlob', 0); + } + + encode(value: TransactionBlob): Uint8Array { + const parts: Uint8Array[] = []; + + parts.push(new B256Coder().encode(value.blobId)); + parts.push(new NumberCoder('u16', { padToWordSize: true }).encode(value.bytecodeWitnessIndex)); + parts.push(new NumberCoder('u32', { padToWordSize: true }).encode(value.policyTypes)); + parts.push(new NumberCoder('u16', { padToWordSize: true }).encode(value.inputsCount)); + parts.push(new NumberCoder('u16', { padToWordSize: true }).encode(value.outputsCount)); + parts.push(new NumberCoder('u16', { padToWordSize: true }).encode(value.witnessesCount)); + parts.push(new PoliciesCoder().encode(value.policies)); + parts.push(new ArrayCoder(new InputCoder(), value.inputsCount).encode(value.inputs)); + parts.push(new ArrayCoder(new OutputCoder(), value.outputsCount).encode(value.outputs)); + parts.push(new ArrayCoder(new WitnessCoder(), value.witnessesCount).encode(value.witnesses)); + + return concat(parts); + } + + decode(data: Uint8Array, offset: number): [TransactionBlob, number] { + let decoded; + let o = offset; + + [decoded, o] = new B256Coder().decode(data, o); + const blobId = decoded; + [decoded, o] = new NumberCoder('u16', { padToWordSize: true }).decode(data, o); + const bytecodeWitnessIndex = decoded; + [decoded, o] = new NumberCoder('u32', { padToWordSize: true }).decode(data, o); + const policyTypes = decoded; + [decoded, o] = new NumberCoder('u16', { padToWordSize: true }).decode(data, o); + const inputsCount = decoded; + [decoded, o] = new NumberCoder('u16', { padToWordSize: true }).decode(data, o); + const outputsCount = decoded; + [decoded, o] = new NumberCoder('u16', { padToWordSize: true }).decode(data, o); + const witnessesCount = decoded; + [decoded, o] = new PoliciesCoder().decode(data, o, policyTypes); + const policies = decoded; + [decoded, o] = new ArrayCoder(new InputCoder(), inputsCount).decode(data, o); + const inputs = decoded; + [decoded, o] = new ArrayCoder(new OutputCoder(), outputsCount).decode(data, o); + const outputs = decoded; + [decoded, o] = new ArrayCoder(new WitnessCoder(), witnessesCount).decode(data, o); + const witnesses = decoded; + + return [ + { + type: TransactionType.Blob, + blobId, + bytecodeWitnessIndex, + policyTypes, + inputsCount, + outputsCount, + witnessesCount, + policies, + inputs, + outputs, + witnesses, + }, + o, + ]; + } +} + type PossibleTransactions = | TransactionScript | TransactionCreate | TransactionMint | TransactionUpgrade - | TransactionUpload; + | TransactionUpload + | TransactionBlob; export type Transaction = TTransactionType extends TransactionType ? Extract @@ -581,7 +683,8 @@ export type Transaction = TTransactionType extends Tran Partial> & Partial> & Partial> & - Partial> & { + Partial> & + Partial> & { type: TransactionType; }; @@ -626,6 +729,10 @@ export class TransactionCoder extends Coder { ); break; } + case TransactionType.Blob: { + parts.push(new TransactionBlobCoder().encode(value as Transaction)); + break; + } default: { throw new FuelError( ErrorCode.UNSUPPORTED_TRANSACTION_TYPE, @@ -665,6 +772,10 @@ export class TransactionCoder extends Coder { [decoded, o] = new TransactionUploadCoder().decode(data, o); return [decoded, o]; } + case TransactionType.Blob: { + [decoded, o] = new TransactionBlobCoder().decode(data, o); + return [decoded, o]; + } default: { throw new FuelError( ErrorCode.UNSUPPORTED_TRANSACTION_TYPE, From 853a44260e4691908e01f19909f9b42100232252 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 24 Jul 2024 13:45:35 +0100 Subject: [PATCH 002/135] fix: blob import --- packages/account/src/providers/transaction-request/index.ts | 1 + packages/contract/src/contract-factory.ts | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/account/src/providers/transaction-request/index.ts b/packages/account/src/providers/transaction-request/index.ts index a710ed3857c..37116d65685 100644 --- a/packages/account/src/providers/transaction-request/index.ts +++ b/packages/account/src/providers/transaction-request/index.ts @@ -1,6 +1,7 @@ export * from './input'; export * from './output'; export * from './transaction-request'; +export * from './blob-transaction-request'; export * from './create-transaction-request'; export * from './script-transaction-request'; export * from './errors'; diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index fc540c0b861..c793adafb46 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -7,12 +7,10 @@ import type { TransactionResult, TransactionType, } from '@fuel-ts/account'; -import { CreateTransactionRequest } from '@fuel-ts/account'; -import { BlobTransactionRequest } from '@fuel-ts/account/dist/providers/transaction-request/blob-transaction-request'; +import { CreateTransactionRequest, BlobTransactionRequest } from '@fuel-ts/account'; import { randomBytes } from '@fuel-ts/crypto'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; import type { BytesLike } from '@fuel-ts/interfaces'; -import { SparseMerkleTree } from '@fuel-ts/merkle'; import { Contract } from '@fuel-ts/program'; import type { StorageSlot } from '@fuel-ts/transactions'; import { arrayify, isDefined } from '@fuel-ts/utils'; From 31732873c3f0d805dc70f14dc65e2f39f8bc0cbb Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 24 Jul 2024 16:56:32 +0100 Subject: [PATCH 003/135] feat: remove instanceof checks --- packages/account/src/providers/provider.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/account/src/providers/provider.ts b/packages/account/src/providers/provider.ts index 4d200368223..79dfdebb42e 100644 --- a/packages/account/src/providers/provider.ts +++ b/packages/account/src/providers/provider.ts @@ -46,8 +46,9 @@ import type { TransactionRequestInput, CoinTransactionRequestInput, JsonAbisFromAllCalls, + ScriptTransactionRequest, } from './transaction-request'; -import { ScriptTransactionRequest, transactionRequestify } from './transaction-request'; +import { transactionRequestify } from './transaction-request'; import type { TransactionResultReceipt } from './transaction-response'; import { TransactionResponse, getDecodedLogs } from './transaction-response'; import { processGqlReceipt } from './transaction-summary/receipt'; @@ -864,7 +865,7 @@ Supported fuel-core version: ${supportedVersion}.` const hasMissingOutputs = missingOutputVariables.length !== 0 || missingOutputContractIds.length !== 0; - if (hasMissingOutputs && transactionRequest instanceof ScriptTransactionRequest) { + if (hasMissingOutputs && transactionRequest.type === TransactionType.Script) { outputVariables += missingOutputVariables.length; transactionRequest.addVariableOutputs(missingOutputVariables.length); missingOutputContractIds.forEach(({ contractId }) => { @@ -950,7 +951,7 @@ Supported fuel-core version: ${supportedVersion}.` const hasMissingOutputs = missingOutputVariables.length > 0 || missingOutputContractIds.length > 0; const request = allRequests[requestIdx]; - if (hasMissingOutputs && request instanceof ScriptTransactionRequest) { + if (hasMissingOutputs && request.type === TransactionType.Script) { result.outputVariables += missingOutputVariables.length; request.addVariableOutputs(missingOutputVariables.length); missingOutputContractIds.forEach(({ contractId }) => { @@ -1030,7 +1031,7 @@ Supported fuel-core version: ${supportedVersion}.` let gasLimit = bn(0); // Only Script transactions consume gas - if (transactionRequest instanceof ScriptTransactionRequest) { + if (transactionRequest.type === TransactionType.Script) { // If the gasLimit is set to 0, it means we need to estimate it. gasLimit = transactionRequest.gasLimit; if (transactionRequest.gasLimit.eq(0)) { @@ -1127,7 +1128,7 @@ Supported fuel-core version: ${supportedVersion}.` { signatureCallback }: TransactionCostParams = {} ): Promise> { const txRequestClone = clone(transactionRequestify(transactionRequestLike)); - const isScriptTransaction = txRequestClone instanceof ScriptTransactionRequest; + const isScriptTransaction = txRequestClone.type === TransactionType.Script; const updateMaxFee = txRequestClone.maxFee.eq(0); // Remove gasLimit to avoid gasLimit when estimating predicates From 740745cd5704a34f75277af34d7d21735f884547 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 24 Jul 2024 16:57:10 +0100 Subject: [PATCH 004/135] feat: blob tx spec and validity fixes --- .../blob-transaction-request.ts | 25 +++++++++++++------ .../providers/transaction-request/utils.ts | 4 +++ .../transaction-summary/operations.test.ts | 1 + packages/account/src/providers/utils/gas.ts | 10 ++++++++ packages/contract/src/contract-factory.ts | 6 +++-- .../fuel-gauge/src/contract-factory.test.ts | 22 ++-------------- .../transactions/src/coders/transaction.ts | 20 +++++++-------- 7 files changed, 49 insertions(+), 39 deletions(-) diff --git a/packages/account/src/providers/transaction-request/blob-transaction-request.ts b/packages/account/src/providers/transaction-request/blob-transaction-request.ts index aaaf43556da..54af40d5eee 100644 --- a/packages/account/src/providers/transaction-request/blob-transaction-request.ts +++ b/packages/account/src/providers/transaction-request/blob-transaction-request.ts @@ -1,13 +1,17 @@ import { hash } from '@fuel-ts/hasher'; +import type { BN } from '@fuel-ts/math'; import type { TransactionBlob } from '@fuel-ts/transactions'; +import type { GasCosts } from '../provider'; +import { calculateMetadataGasForTxBlob } from '../utils'; + import { hashTransaction } from './hash-transaction'; import type { BaseTransactionRequestLike } from './transaction-request'; import { BaseTransactionRequest, TransactionType } from './transaction-request'; export interface BlobTransactionRequestLike extends BaseTransactionRequestLike { /** Witness index of contract bytecode to create */ - bytecodeWitnessIndex?: number; + witnessIndex?: number; } export class BlobTransactionRequest extends BaseTransactionRequest { @@ -21,16 +25,16 @@ export class BlobTransactionRequest extends BaseTransactionRequest { /** Type of the transaction */ type = TransactionType.Blob as const; /** Witness index of contract bytecode to create */ - bytecodeWitnessIndex: number; + witnessIndex: number; /** * Creates an instance `CreateTransactionRequest`. * * @param createTransactionRequestLike - The initial values for the instance */ - constructor({ bytecodeWitnessIndex, ...rest }: BlobTransactionRequestLike) { + constructor({ witnessIndex, ...rest }: BlobTransactionRequestLike) { super(rest); - this.bytecodeWitnessIndex = bytecodeWitnessIndex ?? 0; + this.witnessIndex = witnessIndex ?? 0; } /** @@ -40,12 +44,12 @@ export class BlobTransactionRequest extends BaseTransactionRequest { */ toTransaction(): TransactionBlob { const baseTransaction = this.getBaseTransaction(); - const bytecodeWitnessIndex = this.bytecodeWitnessIndex; + const witnessIndex = this.witnessIndex; return { type: TransactionType.Blob, ...baseTransaction, - blobId: hash(this.witnesses[bytecodeWitnessIndex]), - bytecodeWitnessIndex, + id: hash(this.witnesses[witnessIndex]), + witnessIndex, }; } @@ -59,4 +63,11 @@ export class BlobTransactionRequest extends BaseTransactionRequest { getTransactionId(chainId: number): string { return hashTransaction(this, chainId); } + + metadataGas(gasCosts: GasCosts): BN { + return calculateMetadataGasForTxBlob({ + gasCosts, + txBytesSize: this.byteSize(), + }); + } } diff --git a/packages/account/src/providers/transaction-request/utils.ts b/packages/account/src/providers/transaction-request/utils.ts index 3aeac32e56a..50c279540ff 100644 --- a/packages/account/src/providers/transaction-request/utils.ts +++ b/packages/account/src/providers/transaction-request/utils.ts @@ -1,6 +1,7 @@ import { ErrorCode, FuelError } from '@fuel-ts/errors'; import { TransactionType } from '@fuel-ts/transactions'; +import { BlobTransactionRequest } from './blob-transaction-request'; import { CreateTransactionRequest } from './create-transaction-request'; import { ScriptTransactionRequest } from './script-transaction-request'; import type { TransactionRequestLike, TransactionRequest } from './types'; @@ -20,6 +21,9 @@ export const transactionRequestify = (obj: TransactionRequestLike): TransactionR case TransactionType.Create: { return CreateTransactionRequest.from(obj); } + case TransactionType.Blob: { + return BlobTransactionRequest.from(obj); + } default: { throw new FuelError( ErrorCode.UNSUPPORTED_TRANSACTION_TYPE, diff --git a/packages/account/src/providers/transaction-summary/operations.test.ts b/packages/account/src/providers/transaction-summary/operations.test.ts index a7fe8c48d07..c724819da3c 100644 --- a/packages/account/src/providers/transaction-summary/operations.test.ts +++ b/packages/account/src/providers/transaction-summary/operations.test.ts @@ -940,6 +940,7 @@ describe('operations', () => { expect(getTransactionTypeName(TransactionType.Create)).toBe(TransactionTypeName.Create); expect(getTransactionTypeName(TransactionType.Mint)).toBe(TransactionTypeName.Mint); expect(getTransactionTypeName(TransactionType.Script)).toBe(TransactionTypeName.Script); + expect(getTransactionTypeName(TransactionType.Blob)).toBe(TransactionTypeName.Blob); expect(() => getTransactionTypeName('' as unknown as TransactionType)).toThrowError( 'Unsupported transaction type: ' diff --git a/packages/account/src/providers/utils/gas.ts b/packages/account/src/providers/utils/gas.ts index 9e9bd49a39d..48a219561e4 100644 --- a/packages/account/src/providers/utils/gas.ts +++ b/packages/account/src/providers/utils/gas.ts @@ -158,6 +158,16 @@ export function calculateMetadataGasForTxScript({ return resolveGasDependentCosts(txBytesSize, gasCosts.s256); } +export function calculateMetadataGasForTxBlob({ + gasCosts, + txBytesSize, +}: { + gasCosts: GasCosts; + txBytesSize: number; +}) { + return resolveGasDependentCosts(txBytesSize, gasCosts.s256); +} + export interface CalculateGasFeeParams { tip?: BN; gas: BN; diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index c793adafb46..d9332f50690 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -182,7 +182,8 @@ export default class ContractFactory { // Chunk the bytecode const chunks: ContractChunk[] = []; - const chunkSize = maxContractSize; + // Subtract 10,000 because max tx size === max contract size, come up with something better + const chunkSize = maxContractSize - 10_000; for (let offset = 0, index = 0; offset < this.bytecode.length; offset += chunkSize, index++) { const chunk = this.bytecode.slice(offset, offset + chunkSize); chunks.push({ id: index, size: chunk.length, bytecode: chunk }); @@ -193,7 +194,7 @@ export default class ContractFactory { const { maxFee: setMaxFee } = deployContractOptions; for (const { bytecode } of chunks) { const blobTxRequest = new BlobTransactionRequest({ - bytecodeWitnessIndex: 0, + witnessIndex: 0, witnesses: [bytecode], }); @@ -213,6 +214,7 @@ export default class ContractFactory { await account.fund(blobTxRequest, txCost); const response = await account.sendTransaction(blobTxRequest, { awaitExecution: true }); + const { id } = await response.waitForResult(); if (!id) { throw new Error('Blob ID not returned'); diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index 0489c3c4187..cbdf59f6cb4 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -254,32 +254,14 @@ describe('Contract Factory', () => { ); }); - it('should not deploy contracts greater than 100KB', async () => { - using launched = await launchTestNode(); - const { - wallets: [wallet], - } = launched; - - const largeByteCode = `0x${'00'.repeat(265144)}`; - const factory = new ContractFactory(largeByteCode, StorageTestContractAbi__factory.abi, wallet); - - await expectToThrowFuelError( - async () => factory.deployContract(), - new FuelError( - ErrorCode.CONTRACT_SIZE_EXCEEDS_LIMIT, - 'Contract bytecode is too large. Max contract size is 100KB' - ) - ); - }); - - it('should deploy contracts greater than 100KB in chunks', async () => { + it.only('should deploy contracts greater than 100KB in chunks', async () => { using launched = await launchTestNode(); const { wallets: [wallet], } = launched; const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); - + const { waitForResult: waitForDeployResult } = await factory.deployContract(); const { contract } = await waitForDeployResult(); diff --git a/packages/transactions/src/coders/transaction.ts b/packages/transactions/src/coders/transaction.ts index 2b170b1e1df..1630b0e12b5 100644 --- a/packages/transactions/src/coders/transaction.ts +++ b/packages/transactions/src/coders/transaction.ts @@ -199,7 +199,7 @@ export type TransactionCreate = { export class TransactionCreateCoder extends Coder { constructor() { super('TransactionCreate', 'struct TransactionCreate', 0); - } +} encode(value: TransactionCreate): Uint8Array { const parts: Uint8Array[] = []; @@ -573,10 +573,10 @@ export type TransactionBlob = { type: TransactionType.Blob; /** Hash of the bytecode. (b256) */ - blobId: string; + id: string; - /** Witness index of contract bytecode segment (u16) */ - bytecodeWitnessIndex: number; + /** Witness index of contract bytecode (u16) */ + witnessIndex: number; /** Bitfield of used policy types (u32) */ policyTypes: number; @@ -611,8 +611,8 @@ export class TransactionBlobCoder extends Coder Date: Wed, 24 Jul 2024 17:30:38 +0100 Subject: [PATCH 005/135] feat: correctly get blobIds --- .../blob-transaction-request.ts | 2 +- packages/contract/src/contract-factory.ts | 11 +++--- .../fuel-gauge/src/contract-factory.test.ts | 35 +++++++++++-------- .../transactions/src/coders/transaction.ts | 10 +++--- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/account/src/providers/transaction-request/blob-transaction-request.ts b/packages/account/src/providers/transaction-request/blob-transaction-request.ts index 54af40d5eee..9ea6c7b9221 100644 --- a/packages/account/src/providers/transaction-request/blob-transaction-request.ts +++ b/packages/account/src/providers/transaction-request/blob-transaction-request.ts @@ -48,7 +48,7 @@ export class BlobTransactionRequest extends BaseTransactionRequest { return { type: TransactionType.Blob, ...baseTransaction, - id: hash(this.witnesses[witnessIndex]), + blobId: hash(this.witnesses[witnessIndex]), witnessIndex, }; } diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index d9332f50690..0614f7cbd31 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -215,11 +215,12 @@ export default class ContractFactory { const response = await account.sendTransaction(blobTxRequest, { awaitExecution: true }); - const { id } = await response.waitForResult(); - if (!id) { - throw new Error('Blob ID not returned'); - } - blobIds.push(id); + const { + transaction: { blobId }, + } = await response.waitForResult(); + + // Todo: check status + blobIds.push(blobId); } // Deploy contract via loader contract diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index cbdf59f6cb4..2789d84f247 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -254,19 +254,24 @@ describe('Contract Factory', () => { ); }); - it.only('should deploy contracts greater than 100KB in chunks', async () => { - using launched = await launchTestNode(); - const { - wallets: [wallet], - } = launched; - - const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); - - const { waitForResult: waitForDeployResult } = await factory.deployContract(); - const { contract } = await waitForDeployResult(); - - const { waitForResult } = await contract.functions.gen().call(); - const { value } = await waitForResult(); - expect(value).toBe(true); - }); + it.only( + 'should deploy contracts greater than 100KB in chunks', + async () => { + using launched = await launchTestNode(); + const { + wallets: [wallet], + } = launched; + + const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); + + const { waitForResult: waitForDeployResult } = + await factory.deployContract(); + const { contract } = await waitForDeployResult(); + + const { waitForResult } = await contract.functions.gen().call(); + const { value } = await waitForResult(); + expect(value).toBe(true); + }, + { timeout: 10000 } + ); }); diff --git a/packages/transactions/src/coders/transaction.ts b/packages/transactions/src/coders/transaction.ts index 1630b0e12b5..dbb94fe9446 100644 --- a/packages/transactions/src/coders/transaction.ts +++ b/packages/transactions/src/coders/transaction.ts @@ -199,7 +199,7 @@ export type TransactionCreate = { export class TransactionCreateCoder extends Coder { constructor() { super('TransactionCreate', 'struct TransactionCreate', 0); -} + } encode(value: TransactionCreate): Uint8Array { const parts: Uint8Array[] = []; @@ -573,7 +573,7 @@ export type TransactionBlob = { type: TransactionType.Blob; /** Hash of the bytecode. (b256) */ - id: string; + blobId: string; /** Witness index of contract bytecode (u16) */ witnessIndex: number; @@ -611,7 +611,7 @@ export class TransactionBlobCoder extends Coder Date: Thu, 25 Jul 2024 12:55:33 +0100 Subject: [PATCH 006/135] feat: add loader instructions --- packages/contract/package.json | 1 + packages/contract/src/contract-factory.ts | 128 ++++++++++++++++++++-- pnpm-lock.yaml | 4 +- 3 files changed, 121 insertions(+), 12 deletions(-) diff --git a/packages/contract/package.json b/packages/contract/package.json index 34a26d060fc..327d1804e73 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -39,6 +39,7 @@ }, "license": "Apache-2.0", "dependencies": { + "@fuels/vm-asm": "0.55.0", "@fuel-ts/abi-coder": "workspace:*", "@fuel-ts/account": "workspace:*", "@fuel-ts/crypto": "workspace:*", diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index 0614f7cbd31..ebacdaa44fd 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -1,4 +1,4 @@ -import { Interface } from '@fuel-ts/abi-coder'; +import { BYTES_32, Interface } from '@fuel-ts/abi-coder'; import type { JsonAbi, InputValue } from '@fuel-ts/abi-coder'; import type { Account, @@ -11,9 +11,11 @@ import { CreateTransactionRequest, BlobTransactionRequest } from '@fuel-ts/accou import { randomBytes } from '@fuel-ts/crypto'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; import type { BytesLike } from '@fuel-ts/interfaces'; -import { Contract } from '@fuel-ts/program'; +import { Contract, InstructionSet } from '@fuel-ts/program'; import type { StorageSlot } from '@fuel-ts/transactions'; -import { arrayify, isDefined } from '@fuel-ts/utils'; +import { arrayify, concat, isDefined } from '@fuel-ts/utils'; +import * as asm from '@fuels/vm-asm'; +import { blob } from 'stream/consumers'; import { getContractId, getContractStorageRoot, hexlifyWithPrefix } from './util'; @@ -40,6 +42,7 @@ export type ContractChunk = { id: number; size: number; bytecode: Uint8Array; + blobId?: string; }; /** @@ -190,9 +193,8 @@ export default class ContractFactory { } // Deploy chunk as blob tx - const blobIds: string[] = []; const { maxFee: setMaxFee } = deployContractOptions; - for (const { bytecode } of chunks) { + for (const { id, bytecode } of chunks) { const blobTxRequest = new BlobTransactionRequest({ witnessIndex: 0, witnesses: [bytecode], @@ -220,26 +222,130 @@ export default class ContractFactory { } = await response.waitForResult(); // Todo: check status - blobIds.push(blobId); + chunks[id].blobId = blobId; } // Deploy contract via loader contract // 1. Get bytes for loader contract // 2. Encode byteIds as function arguments - // this.bytecode should be from encoded loader - const salt = deployContractOptions.salt || randomBytes(32); - const stateRoot = deployContractOptions.stateRoot || getContractStorageRoot([]); + // Destructure constants + const { RegId, Instruction } = asm; + + const instructionsPerBlob = 26; + const numberOfBlobs = chunks.length; + const numberOfInstructions = numberOfBlobs * instructionsPerBlob; + const blobIdSize = BYTES_32; + + // Btyes for the BSIZ opcode + const bsizBytes = () => new Uint8Array([186, 69, 0, 0]); + // Bytes for the BLDD opcode + const blddBytes = () => new Uint8Array([187, 81, 0, 17]); + + const instructionsBytes = concat([ + new InstructionSet( + // 0x12 is going to hold the total size of the contract + asm.move_(0x12, RegId.zero().to_u8()), + // find the start of the hardcoded blob ids, which are located after the code ends + asm.move_(0x10, RegId.is().to_u8()), + // 0x10 to hold the address of the current blob id + asm.addi(0x10, 0x10, numberOfInstructions * Instruction.size()), + // loop counter + asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs) + ).toBytes(), + // LOOP starts here + // 0x11 to hold the size of the current blob + bsizBytes(), + new InstructionSet( + // update the total size of the contract + asm.add(0x12, 0x12, 0x11), + // move on to the next blob + asm.addi(0x10, 0x10, blobIdSize), + // decrement the loop counter + asm.subi(0x13, 0x13, 1), + // Jump backwards 3 instructions if the counter has not reached 0 + asm.jneb(0x13, RegId.zero().to_u8(), RegId.zero().to_u8(), 3), + // move the stack pointer by the contract size since we need to write the contract on the stack since only that memory can be executed + asm.cfe(0x12), + // find the start of the hardcoded blob ids, which are located after the code ends + asm.move_(0x10, RegId.is().to_u8()), + // 0x10 to hold the address of the current blob id + asm.addi(0x10, 0x10, numberOfInstructions * Instruction.size()), + // 0x12 is going to hold the total bytes loaded of the contract + asm.move_(0x12, RegId.zero().to_u8()), + // loop counter + asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs) + ).toBytes(), + // LOOP starts here + // 0x11 to hold the size of the current blob + bsizBytes(), + new InstructionSet( + // the location where to load the current blob (start of stack) + asm.move_(0x14, RegId.spp().to_u8()), + // move to where this blob should be loaded by adding the total bytes loaded + asm.add(0x14, 0x14, 0x12) + ).toBytes(), + blddBytes(), + new InstructionSet( + // update the total bytes loaded + asm.add(0x12, 0x12, 0x11), + // move on to the next blob + asm.addi(0x10, 0x10, blobIdSize), + // decrement the loop counter + asm.subi(0x13, 0x13, 1), + // Jump backwards 6 instructions if the counter has not reached 0 + asm.jneb(0x13, RegId.zero().to_u8(), RegId.zero().to_u8(), 6), + // what follows is called _jmp_mem by the sway compiler + // move to the start of the stack (also the start of the contract we loaded) + asm.move_(0x16, RegId.spp().to_u8()), + // subtract the address contained in IS because jmp will add it back + asm.sub(0x16, 0x16, RegId.is().to_u8()), + // jmp will multiply by 4 so we need to divide to cancel that out + asm.divi(0x16, 0x16, 4), + // jump to the start of the contract we loaded + asm.jmp(0x16) + ).toBytes(), + ]); + + const storageSlots = deployContractOptions?.storageSlots + ?.map(({ key, value }) => ({ + key: hexlifyWithPrefix(key), + value: hexlifyWithPrefix(value), + })) + .sort(({ key: keyA }, { key: keyB }) => keyA.localeCompare(keyB)); + + const options = { + salt: randomBytes(32), + ...deployContractOptions, + storageSlots: storageSlots || [], + }; + + const stateRoot = options.stateRoot || getContractStorageRoot(options.storageSlots); - contractId = getContractId(this.bytecode, salt, stateRoot); + contractId = getContractId(instructionsBytes, options.salt, stateRoot); const createTxRequest = new CreateTransactionRequest({ bytecodeWitnessIndex: 0, - witnesses: [this.bytecode], + witnesses: [instructionsBytes], ...deployContractOptions, }); createTxRequest.addContractCreatedOutput(contractId, stateRoot); transactionRequest = createTxRequest; + + const txCost = await account.getTransactionCost(transactionRequest); + + if (isDefined(setMaxFee)) { + if (txCost.maxFee.gt(setMaxFee)) { + throw new FuelError( + ErrorCode.MAX_FEE_TOO_LOW, + `Max fee '${deployContractOptions.maxFee}' is lower than the required: '${txCost.maxFee}'.` + ); + } + } else { + transactionRequest.maxFee = txCost.maxFee; + } + + await account.fund(transactionRequest, txCost); } else { const { contractId: id, transactionRequest: req } = await this.prepareDeploy(deployContractOptions); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e76ddabdae..bf0bab036f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -810,6 +810,9 @@ importers: '@fuel-ts/versions': specifier: workspace:* version: link:../versions + '@fuels/vm-asm': + specifier: 0.55.0 + version: 0.55.0 ramda: specifier: ^0.29.0 version: 0.29.0 @@ -6542,7 +6545,6 @@ packages: bun@1.1.20: resolution: {integrity: sha512-aqLmvaz0/vLUiCrOXtAsf7pCSOS/qXieYDsq8COa3+fIgMK05CjZt9m9r7DC+tjKy7hH8uKSNTapQOr/kX8gIA==} - cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true From 0bbcfdc612c5eb663452da3de99d9ec37bdedd1c Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Thu, 25 Jul 2024 14:08:29 +0100 Subject: [PATCH 007/135] feat: append blob ids to loader --- internal/forc/VERSION | 2 +- .../transaction-request/blob-transaction-request.ts | 9 +++++++-- packages/contract/src/contract-factory.ts | 11 ++++------- .../fixtures/forc-projects/large-contract/src/main.sw | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/internal/forc/VERSION b/internal/forc/VERSION index 645b2e0a0c5..7e9253a37f6 100644 --- a/internal/forc/VERSION +++ b/internal/forc/VERSION @@ -1 +1 @@ -git:kayagokalp/proxy-with-chunks \ No newline at end of file +0.62.0 \ No newline at end of file diff --git a/packages/account/src/providers/transaction-request/blob-transaction-request.ts b/packages/account/src/providers/transaction-request/blob-transaction-request.ts index 9ea6c7b9221..fd73e410559 100644 --- a/packages/account/src/providers/transaction-request/blob-transaction-request.ts +++ b/packages/account/src/providers/transaction-request/blob-transaction-request.ts @@ -28,9 +28,9 @@ export class BlobTransactionRequest extends BaseTransactionRequest { witnessIndex: number; /** - * Creates an instance `CreateTransactionRequest`. + * Creates an instance `BlobTransactionRequest`. * - * @param createTransactionRequestLike - The initial values for the instance + * @param blobTransactionRequestLike - The initial values for the instance */ constructor({ witnessIndex, ...rest }: BlobTransactionRequestLike) { super(rest); @@ -43,6 +43,11 @@ export class BlobTransactionRequest extends BaseTransactionRequest { * @returns The transaction create object. */ toTransaction(): TransactionBlob { + const blobBytecode = this.witnesses[this.witnessIndex]; + if (!blobBytecode) { + throw new Error('NO BLOB BYTECODE'); + } + const baseTransaction = this.getBaseTransaction(); const witnessIndex = this.witnessIndex; return { diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index ebacdaa44fd..4958c1f287a 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -214,7 +214,6 @@ export default class ContractFactory { } await account.fund(blobTxRequest, txCost); - const response = await account.sendTransaction(blobTxRequest, { awaitExecution: true }); const { @@ -222,14 +221,9 @@ export default class ContractFactory { } = await response.waitForResult(); // Todo: check status - chunks[id].blobId = blobId; + chunks[id].blobId = blobId as string; } - // Deploy contract via loader contract - // 1. Get bytes for loader contract - // 2. Encode byteIds as function arguments - - // Destructure constants const { RegId, Instruction } = asm; const instructionsPerBlob = 26; @@ -237,6 +231,8 @@ export default class ContractFactory { const numberOfInstructions = numberOfBlobs * instructionsPerBlob; const blobIdSize = BYTES_32; + // Bytes for the Blob Ids + const blobIdBytes = () => concat(chunks.map(({ blobId }) => arrayify(blobId as string))); // Btyes for the BSIZ opcode const bsizBytes = () => new Uint8Array([186, 69, 0, 0]); // Bytes for the BLDD opcode @@ -305,6 +301,7 @@ export default class ContractFactory { // jump to the start of the contract we loaded asm.jmp(0x16) ).toBytes(), + blobIdBytes(), ]); const storageSlots = deployContractOptions?.storageSlots diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw index d6345f48436..7f5918d5aa2 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw +++ b/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw @@ -7,7 +7,7 @@ abi LargeContract { impl LargeContract for Contract { fn gen() -> bool { asm() { - blob i500000; + blob i450000; } true } From a0e761e4f7fbc97f36b6ccd58dd48e03b908eeee Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Thu, 25 Jul 2024 17:04:43 +0100 Subject: [PATCH 008/135] feat: lots of cleanup --- packages/contract/src/contract-factory.ts | 327 +++++++----------- packages/contract/src/loader-script.ts | 86 +++++ .../fuel-gauge/src/contract-factory.test.ts | 17 +- .../versions/src/lib/getBuiltinVersions.ts | 2 +- 4 files changed, 225 insertions(+), 207 deletions(-) create mode 100644 packages/contract/src/loader-script.ts diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index 4958c1f287a..e5825d06ed6 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -1,22 +1,26 @@ -import { BYTES_32, Interface } from '@fuel-ts/abi-coder'; +import { Interface } from '@fuel-ts/abi-coder'; import type { JsonAbi, InputValue } from '@fuel-ts/abi-coder'; import type { Account, CreateTransactionRequestLike, Provider, + TransactionRequest, TransactionResult, TransactionType, } from '@fuel-ts/account'; -import { CreateTransactionRequest, BlobTransactionRequest } from '@fuel-ts/account'; +import { + CreateTransactionRequest, + BlobTransactionRequest, + TransactionStatus, +} from '@fuel-ts/account'; import { randomBytes } from '@fuel-ts/crypto'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; import type { BytesLike } from '@fuel-ts/interfaces'; -import { Contract, InstructionSet } from '@fuel-ts/program'; +import { Contract } from '@fuel-ts/program'; import type { StorageSlot } from '@fuel-ts/transactions'; -import { arrayify, concat, isDefined } from '@fuel-ts/utils'; -import * as asm from '@fuels/vm-asm'; -import { blob } from 'stream/consumers'; +import { arrayify, isDefined } from '@fuel-ts/utils'; +import { getLoaderInstructions } from './loader-script'; import { getContractId, getContractStorageRoot, hexlifyWithPrefix } from './util'; /** @@ -113,7 +117,9 @@ export default class ContractFactory { * @param deployContractOptions - Options for deploying the contract. * @returns The CreateTransactionRequest object for deploying the contract. */ - createTransactionRequest(deployContractOptions?: DeployContractOptions) { + createTransactionRequest( + deployContractOptions?: DeployContractOptions & { bytecode?: Uint8Array } + ) { const storageSlots = deployContractOptions?.storageSlots ?.map(({ key, value }) => ({ key: hexlifyWithPrefix(key), @@ -134,11 +140,12 @@ export default class ContractFactory { ); } + const bytecode = deployContractOptions?.bytecode || this.bytecode; const stateRoot = options.stateRoot || getContractStorageRoot(options.storageSlots); - const contractId = getContractId(this.bytecode, options.salt, stateRoot); + const contractId = getContractId(bytecode, options.salt, stateRoot); const transactionRequest = new CreateTransactionRequest({ bytecodeWitnessIndex: 0, - witnesses: [this.bytecode], + witnesses: [bytecode], ...options, }); transactionRequest.addContractCreatedOutput(contractId, stateRoot); @@ -149,6 +156,37 @@ export default class ContractFactory { }; } + blobTransactionRequest(options: { blobBytecode: Uint8Array } & DeployContractOptions) { + const { blobBytecode } = options; + return new BlobTransactionRequest({ + witnessIndex: 0, + witnesses: [blobBytecode], + ...options, + }); + } + + async fundTransactionRequest(request: TransactionRequest, options: DeployContractOptions = {}) { + const account = this.getAccount(); + const { maxFee: setMaxFee } = options; + + const txCost = await account.getTransactionCost(request); + + if (isDefined(setMaxFee)) { + if (txCost.maxFee.gt(setMaxFee)) { + throw new FuelError( + ErrorCode.MAX_FEE_TOO_LOW, + `Max fee '${options.maxFee}' is lower than the required: '${txCost.maxFee}'.` + ); + } + } else { + request.maxFee = txCost.maxFee; + } + + await account.fund(request, txCost); + + return request; + } + /** * Deploy a contract with the specified options. * @@ -159,199 +197,101 @@ export default class ContractFactory { deployContractOptions: DeployContractOptions = {} ): Promise> { const account = this.getAccount(); - const { consensusParameters } = account.provider.getChain(); const maxContractSize = consensusParameters.contractParameters.contractMaxSize.toNumber(); - let contractId: string; - let transactionRequest: CreateTransactionRequest; - - // Checker whether the contract needs to be chunked if (this.bytecode.length > maxContractSize) { - // Ensure the max contract size is byte aligned (VM requirement) - if (maxContractSize % 8 !== 0) { - throw new FuelError( - ErrorCode.CONTRACT_SIZE_EXCEEDS_LIMIT, // Todo: change error - `Max contract size of ${maxContractSize} bytes is not byte aligned.` - ); - } + throw new FuelError( + ErrorCode.CONTRACT_SIZE_EXCEEDS_LIMIT, + 'Contract bytecode is too large. Max contract size is 100KB' // change error + ); + } - // Set configurables - const { configurableConstants } = deployContractOptions; - const hasConfigurable = Object.keys(this.interface.configurables).length; - if (hasConfigurable && configurableConstants) { - this.setConfigurableConstants(configurableConstants); - } + const { contractId, transactionRequest } = await this.prepareDeploy(deployContractOptions); - // Chunk the bytecode - const chunks: ContractChunk[] = []; - // Subtract 10,000 because max tx size === max contract size, come up with something better - const chunkSize = maxContractSize - 10_000; - for (let offset = 0, index = 0; offset < this.bytecode.length; offset += chunkSize, index++) { - const chunk = this.bytecode.slice(offset, offset + chunkSize); - chunks.push({ id: index, size: chunk.length, bytecode: chunk }); - } + const transactionResponse = await account.sendTransaction(transactionRequest); - // Deploy chunk as blob tx - const { maxFee: setMaxFee } = deployContractOptions; - for (const { id, bytecode } of chunks) { - const blobTxRequest = new BlobTransactionRequest({ - witnessIndex: 0, - witnesses: [bytecode], - }); - - const txCost = await account.getTransactionCost(blobTxRequest); - - if (isDefined(setMaxFee)) { - if (txCost.maxFee.gt(setMaxFee)) { - throw new FuelError( - ErrorCode.MAX_FEE_TOO_LOW, - `Max fee '${deployContractOptions.maxFee}' is lower than the required: '${txCost.maxFee}'.` - ); - } - } else { - blobTxRequest.maxFee = txCost.maxFee; - } + const waitForResult = async () => { + const transactionResult = await transactionResponse.waitForResult(); + const contract = new Contract(contractId, this.interface, account) as TContract; - await account.fund(blobTxRequest, txCost); - const response = await account.sendTransaction(blobTxRequest, { awaitExecution: true }); + return { contract, transactionResult }; + }; - const { - transaction: { blobId }, - } = await response.waitForResult(); + return { waitForResult, contractId, transactionId: transactionResponse.id }; + } - // Todo: check status - chunks[id].blobId = blobId as string; - } + /** + * Deploy a contract with the specified options. + * + * @param deployContractOptions - Options for deploying the contract. + * @returns A promise that resolves to the deployed contract instance. + */ + async deployContractLoader( + deployContractOptions: DeployContractOptions = {} + ): Promise> { + const account = this.getAccount(); + const { consensusParameters } = account.provider.getChain(); + const maxContractSize = consensusParameters.contractParameters.contractMaxSize.toNumber(); - const { RegId, Instruction } = asm; - - const instructionsPerBlob = 26; - const numberOfBlobs = chunks.length; - const numberOfInstructions = numberOfBlobs * instructionsPerBlob; - const blobIdSize = BYTES_32; - - // Bytes for the Blob Ids - const blobIdBytes = () => concat(chunks.map(({ blobId }) => arrayify(blobId as string))); - // Btyes for the BSIZ opcode - const bsizBytes = () => new Uint8Array([186, 69, 0, 0]); - // Bytes for the BLDD opcode - const blddBytes = () => new Uint8Array([187, 81, 0, 17]); - - const instructionsBytes = concat([ - new InstructionSet( - // 0x12 is going to hold the total size of the contract - asm.move_(0x12, RegId.zero().to_u8()), - // find the start of the hardcoded blob ids, which are located after the code ends - asm.move_(0x10, RegId.is().to_u8()), - // 0x10 to hold the address of the current blob id - asm.addi(0x10, 0x10, numberOfInstructions * Instruction.size()), - // loop counter - asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs) - ).toBytes(), - // LOOP starts here - // 0x11 to hold the size of the current blob - bsizBytes(), - new InstructionSet( - // update the total size of the contract - asm.add(0x12, 0x12, 0x11), - // move on to the next blob - asm.addi(0x10, 0x10, blobIdSize), - // decrement the loop counter - asm.subi(0x13, 0x13, 1), - // Jump backwards 3 instructions if the counter has not reached 0 - asm.jneb(0x13, RegId.zero().to_u8(), RegId.zero().to_u8(), 3), - // move the stack pointer by the contract size since we need to write the contract on the stack since only that memory can be executed - asm.cfe(0x12), - // find the start of the hardcoded blob ids, which are located after the code ends - asm.move_(0x10, RegId.is().to_u8()), - // 0x10 to hold the address of the current blob id - asm.addi(0x10, 0x10, numberOfInstructions * Instruction.size()), - // 0x12 is going to hold the total bytes loaded of the contract - asm.move_(0x12, RegId.zero().to_u8()), - // loop counter - asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs) - ).toBytes(), - // LOOP starts here - // 0x11 to hold the size of the current blob - bsizBytes(), - new InstructionSet( - // the location where to load the current blob (start of stack) - asm.move_(0x14, RegId.spp().to_u8()), - // move to where this blob should be loaded by adding the total bytes loaded - asm.add(0x14, 0x14, 0x12) - ).toBytes(), - blddBytes(), - new InstructionSet( - // update the total bytes loaded - asm.add(0x12, 0x12, 0x11), - // move on to the next blob - asm.addi(0x10, 0x10, blobIdSize), - // decrement the loop counter - asm.subi(0x13, 0x13, 1), - // Jump backwards 6 instructions if the counter has not reached 0 - asm.jneb(0x13, RegId.zero().to_u8(), RegId.zero().to_u8(), 6), - // what follows is called _jmp_mem by the sway compiler - // move to the start of the stack (also the start of the contract we loaded) - asm.move_(0x16, RegId.spp().to_u8()), - // subtract the address contained in IS because jmp will add it back - asm.sub(0x16, 0x16, RegId.is().to_u8()), - // jmp will multiply by 4 so we need to divide to cancel that out - asm.divi(0x16, 0x16, 4), - // jump to the start of the contract we loaded - asm.jmp(0x16) - ).toBytes(), - blobIdBytes(), - ]); - - const storageSlots = deployContractOptions?.storageSlots - ?.map(({ key, value }) => ({ - key: hexlifyWithPrefix(key), - value: hexlifyWithPrefix(value), - })) - .sort(({ key: keyA }, { key: keyB }) => keyA.localeCompare(keyB)); - - const options = { - salt: randomBytes(32), - ...deployContractOptions, - storageSlots: storageSlots || [], - }; - - const stateRoot = options.stateRoot || getContractStorageRoot(options.storageSlots); - - contractId = getContractId(instructionsBytes, options.salt, stateRoot); - const createTxRequest = new CreateTransactionRequest({ - bytecodeWitnessIndex: 0, - witnesses: [instructionsBytes], - ...deployContractOptions, - }); - createTxRequest.addContractCreatedOutput(contractId, stateRoot); + // Ensure the max contract size is byte aligned (VM requirement) + if (maxContractSize % 8 !== 0) { + throw new FuelError( + ErrorCode.CONTRACT_SIZE_EXCEEDS_LIMIT, // Todo: change error + `Max contract size of ${maxContractSize} bytes is not byte aligned.` + ); + } - transactionRequest = createTxRequest; + const { configurableConstants } = deployContractOptions; + if (configurableConstants) { + this.setConfigurableConstants(configurableConstants); + } - const txCost = await account.getTransactionCost(transactionRequest); + // Chunk the bytecode + const chunks: ContractChunk[] = []; + const chunkSize = maxContractSize - 10_000; // Come up with better max size calculation + for (let offset = 0, index = 0; offset < this.bytecode.length; offset += chunkSize, index++) { + const chunk = this.bytecode.slice(offset, offset + chunkSize); + chunks.push({ id: index, size: chunk.length, bytecode: chunk }); + } - if (isDefined(setMaxFee)) { - if (txCost.maxFee.gt(setMaxFee)) { - throw new FuelError( - ErrorCode.MAX_FEE_TOO_LOW, - `Max fee '${deployContractOptions.maxFee}' is lower than the required: '${txCost.maxFee}'.` - ); - } - } else { - transactionRequest.maxFee = txCost.maxFee; + // Deploy the chunks as blobs + for (const { id, bytecode } of chunks) { + const blobRequest = new BlobTransactionRequest({ + witnessIndex: 0, + witnesses: [bytecode], + }); + + const fundedBlobRequest = await this.fundTransactionRequest( + blobRequest, + deployContractOptions + ); + const response = await account.sendTransaction(fundedBlobRequest, { awaitExecution: true }); + const result = await response.waitForResult(); + + if (!result.status || result.status !== TransactionStatus.success) { + throw new FuelError(ErrorCode.TRANSACTION_FAILED, 'Failed to deploy contract chunk'); } - await account.fund(transactionRequest, txCost); - } else { - const { contractId: id, transactionRequest: req } = - await this.prepareDeploy(deployContractOptions); - contractId = id; - transactionRequest = req; + chunks[id].blobId = result.transaction.blobId; } - const transactionResponse = await account.sendTransaction(transactionRequest, { - awaitExecution: false, + // Get the loader bytecode + const blobIds = chunks.map((c) => c.blobId as string); + const loaderBytecode = getLoaderInstructions(blobIds); + + // Deploy the loader contract + const { contractId, transactionRequest: createRequest } = this.createTransactionRequest({ + bytecode: loaderBytecode, + ...deployContractOptions, + }); + const fundedCreateRequest = await this.fundTransactionRequest( + createRequest, + deployContractOptions + ); + + const transactionResponse = await account.sendTransaction(fundedCreateRequest, { + awaitExecution: true, }); const waitForResult = async () => { @@ -416,24 +356,7 @@ export default class ContractFactory { const { contractId, transactionRequest } = this.createTransactionRequest(deployContractOptions); - const account = this.getAccount(); - - const txCost = await account.getTransactionCost(transactionRequest); - - const { maxFee: setMaxFee } = deployContractOptions; - - if (isDefined(setMaxFee)) { - if (txCost.maxFee.gt(setMaxFee)) { - throw new FuelError( - ErrorCode.MAX_FEE_TOO_LOW, - `Max fee '${deployContractOptions.maxFee}' is lower than the required: '${txCost.maxFee}'.` - ); - } - } else { - transactionRequest.maxFee = txCost.maxFee; - } - - await account.fund(transactionRequest, txCost); + await this.fundTransactionRequest(transactionRequest, deployContractOptions); return { contractId, diff --git a/packages/contract/src/loader-script.ts b/packages/contract/src/loader-script.ts new file mode 100644 index 00000000000..53e3eac13c8 --- /dev/null +++ b/packages/contract/src/loader-script.ts @@ -0,0 +1,86 @@ +import { BYTES_32 } from '@fuel-ts/abi-coder'; +import { InstructionSet } from '@fuel-ts/program'; +import { arrayify, concat } from '@fuel-ts/utils'; +import * as asm from '@fuels/vm-asm'; + +export const getLoaderInstructions = (blobIds: string[]): Uint8Array => { + const { RegId, Instruction } = asm; + + const instructionsPerBlob = 26; + const numberOfBlobs = blobIds.length; + const numberOfInstructions = numberOfBlobs * instructionsPerBlob; + const blobIdSize = BYTES_32; + + // Btyes for the BSIZ opcode + const bsizBytes = () => new Uint8Array([186, 69, 0, 0]); + // Bytes for the BLDD opcode + const blddBytes = () => new Uint8Array([187, 81, 0, 17]); + // Bytes for the Blob Ids + const blobIdBytes = () => concat(blobIds.map((b) => arrayify(b))); + + return concat([ + new InstructionSet( + // 0x12 is going to hold the total size of the contract + asm.move_(0x12, RegId.zero().to_u8()), + // find the start of the hardcoded blob ids, which are located after the code ends + asm.move_(0x10, RegId.is().to_u8()), + // 0x10 to hold the address of the current blob id + asm.addi(0x10, 0x10, numberOfInstructions * Instruction.size()), + // loop counter + asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs) + ).toBytes(), + // LOOP starts here + // 0x11 to hold the size of the current blob + bsizBytes(), + new InstructionSet( + // update the total size of the contract + asm.add(0x12, 0x12, 0x11), + // move on to the next blob + asm.addi(0x10, 0x10, blobIdSize), + // decrement the loop counter + asm.subi(0x13, 0x13, 1), + // Jump backwards 3 instructions if the counter has not reached 0 + asm.jneb(0x13, RegId.zero().to_u8(), RegId.zero().to_u8(), 3), + // move the stack pointer by the contract size since we need to write the contract on the stack since only that memory can be executed + asm.cfe(0x12), + // find the start of the hardcoded blob ids, which are located after the code ends + asm.move_(0x10, RegId.is().to_u8()), + // 0x10 to hold the address of the current blob id + asm.addi(0x10, 0x10, numberOfInstructions * Instruction.size()), + // 0x12 is going to hold the total bytes loaded of the contract + asm.move_(0x12, RegId.zero().to_u8()), + // loop counter + asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs) + ).toBytes(), + // LOOP starts here + // 0x11 to hold the size of the current blob + bsizBytes(), + new InstructionSet( + // the location where to load the current blob (start of stack) + asm.move_(0x14, RegId.spp().to_u8()), + // move to where this blob should be loaded by adding the total bytes loaded + asm.add(0x14, 0x14, 0x12) + ).toBytes(), + blddBytes(), + new InstructionSet( + // update the total bytes loaded + asm.add(0x12, 0x12, 0x11), + // move on to the next blob + asm.addi(0x10, 0x10, blobIdSize), + // decrement the loop counter + asm.subi(0x13, 0x13, 1), + // Jump backwards 6 instructions if the counter has not reached 0 + asm.jneb(0x13, RegId.zero().to_u8(), RegId.zero().to_u8(), 6), + // what follows is called _jmp_mem by the sway compiler + // move to the start of the stack (also the start of the contract we loaded) + asm.move_(0x16, RegId.spp().to_u8()), + // subtract the address contained in IS because jmp will add it back + asm.sub(0x16, 0x16, RegId.is().to_u8()), + // jmp will multiply by 4 so we need to divide to cancel that out + asm.divi(0x16, 0x16, 4), + // jump to the start of the contract we loaded + asm.jmp(0x16) + ).toBytes(), + blobIdBytes(), + ]); +}; diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index 2789d84f247..82b64a27c39 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -1,6 +1,7 @@ import type { Account, TransactionResult } from '@fuel-ts/account'; import { FuelError, ErrorCode } from '@fuel-ts/errors'; import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; +import type { DeployContractOptions } from 'fuels'; import { BN, bn, toHex, Interface, ContractFactory } from 'fuels'; import { launchTestNode } from 'fuels/test-utils'; @@ -262,15 +263,23 @@ describe('Contract Factory', () => { wallets: [wallet], } = launched; + const salt = new Uint8Array([ + 166, 23, 175, 50, 185, 247, 229, 160, 32, 86, 191, 57, 44, 165, 193, 78, 134, 144, 54, 219, + 234, 246, 163, 190, 132, 237, 251, 228, 12, 13, 127, 193, + ]); + const options: DeployContractOptions = { salt }; + const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); const { waitForResult: waitForDeployResult } = - await factory.deployContract(); + await factory.deployContractLoader(options); + const { contract } = await waitForDeployResult(); + expect(contract.id).toBeDefined(); - const { waitForResult } = await contract.functions.gen().call(); - const { value } = await waitForResult(); - expect(value).toBe(true); + const { waitForResult: waitForCallResult } = await contract.functions.something().call(); + const { value } = await waitForCallResult(); + expect(value).toBe(1001); }, { timeout: 10000 } ); diff --git a/packages/versions/src/lib/getBuiltinVersions.ts b/packages/versions/src/lib/getBuiltinVersions.ts index e4cdeb47188..f9aaed33b4a 100644 --- a/packages/versions/src/lib/getBuiltinVersions.ts +++ b/packages/versions/src/lib/getBuiltinVersions.ts @@ -1,7 +1,7 @@ export function getBuiltinVersions() { return { FORC: '0.62.0', - FUEL_CORE: '0.31.0', + FUEL_CORE: 'git:dento/blob-tx', FUELS: '0.92.1', }; } From 3baba2329c2eefd24cc3a90485eb44c22b87e034 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Thu, 25 Jul 2024 17:44:33 +0100 Subject: [PATCH 009/135] feat: fix wait for result in blob tx --- packages/contract/src/contract-factory.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index cb6cf70c42f..204e9d7870a 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -266,8 +266,9 @@ export default class ContractFactory { blobRequest, deployContractOptions ); - const { waitForResult } = await account.sendTransaction(fundedBlobRequest); - const result = await waitForResult(); + + const check = await account.sendTransaction(fundedBlobRequest); + const result = await check.waitForResult(); if (!result.status || result.status !== TransactionStatus.success) { throw new FuelError(ErrorCode.TRANSACTION_FAILED, 'Failed to deploy contract chunk'); From 57e84103f75495ed221cb8546c6a8b63f0e2555d Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Thu, 25 Jul 2024 17:45:16 +0100 Subject: [PATCH 010/135] chore: linting --- internal/forc/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/forc/VERSION b/internal/forc/VERSION index 7e9253a37f6..4d74f323830 100644 --- a/internal/forc/VERSION +++ b/internal/forc/VERSION @@ -1 +1 @@ -0.62.0 \ No newline at end of file +0.62.0 From e04d773df95afe95f5c51ac0780176b017492154 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Thu, 25 Jul 2024 18:41:53 +0100 Subject: [PATCH 011/135] test: test mods --- .../fuel-gauge/src/contract-factory.test.ts | 24 ++++++++++--------- .../forc-projects/large-contract/src/main.sw | 10 ++++---- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index 82b64a27c39..202886165e8 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -1,8 +1,9 @@ import type { Account, TransactionResult } from '@fuel-ts/account'; +import { generateTestWallet } from '@fuel-ts/account/test-utils'; import { FuelError, ErrorCode } from '@fuel-ts/errors'; import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import type { DeployContractOptions } from 'fuels'; -import { BN, bn, toHex, Interface, ContractFactory } from 'fuels'; +import { BN, bn, toHex, Interface, ContractFactory, LOCAL_NETWORK_URL, Provider } from 'fuels'; import { launchTestNode } from 'fuels/test-utils'; import type { LargeContractAbi } from '../test/typegen/contracts'; @@ -258,10 +259,11 @@ describe('Contract Factory', () => { it.only( 'should deploy contracts greater than 100KB in chunks', async () => { - using launched = await launchTestNode(); - const { - wallets: [wallet], - } = launched; + // USE TEST NODE + + const provider = await Provider.create(LOCAL_NETWORK_URL); + const baseAssetId = provider.getBaseAssetId(); + const wallet = await generateTestWallet(provider, [[100_000_000, baseAssetId]]); const salt = new Uint8Array([ 166, 23, 175, 50, 185, 247, 229, 160, 32, 86, 191, 57, 44, 165, 193, 78, 134, 144, 54, 219, @@ -271,16 +273,16 @@ describe('Contract Factory', () => { const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); - const { waitForResult: waitForDeployResult } = - await factory.deployContractLoader(options); + const deploy = await factory.deployContractLoader(options); - const { contract } = await waitForDeployResult(); + const { contract } = await deploy.waitForResult(); expect(contract.id).toBeDefined(); - const { waitForResult: waitForCallResult } = await contract.functions.something().call(); - const { value } = await waitForCallResult(); + const call = await contract.functions.something().call(); + + const { value } = await call.waitForResult(); expect(value).toBe(1001); }, - { timeout: 10000 } + { timeout: 20000 } ); }); diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw index 7f5918d5aa2..3dc4af3d05c 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw +++ b/packages/fuel-gauge/test/fixtures/forc-projects/large-contract/src/main.sw @@ -1,14 +1,14 @@ contract; -abi LargeContract { - fn gen() -> bool; +abi MyContract { + fn something() -> u64; } -impl LargeContract for Contract { - fn gen() -> bool { +impl MyContract for Contract { + fn something() -> u64 { asm() { blob i450000; } - true + 1001 } } From 6829095ea3d70bf59cf7091ddcfe18f00af43edd Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Thu, 25 Jul 2024 18:42:12 +0100 Subject: [PATCH 012/135] chore: add demo package build filter --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e46705e64f..0970fa57db2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "dev": "nodemon --config nodemon.config.json -x 'pnpm build:packages'", "build": "turbo run build --cache-dir=.turbo", - "build:packages": "turbo run build --filter=!docs --filter=!template-*", + "build:packages": "turbo run build --filter=!docs --filter=!template-* --filter=!demo*", "ci:test": "./scripts/tests-ci.sh", "pretest": "turbo run pretest", "depcheck": "knip --dependencies --tags=-knipignore", From cfe40296d097e36375d28f49156c7f788d8af88a Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Fri, 26 Jul 2024 10:45:57 +0100 Subject: [PATCH 013/135] test: add max size test for initial deploy method --- .../fuel-gauge/src/contract-factory.test.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index 202886165e8..9afa63b7565 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -256,11 +256,27 @@ describe('Contract Factory', () => { ); }); + it('should not deploy contracts greater than MAX_CONTRACT_SIZE via deploy contract', async () => { + using launched = await launchTestNode(); + const { + wallets: [wallet], + } = launched; + + const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); + + await expectToThrowFuelError( + () => factory.deployContract(), + new FuelError( + ErrorCode.CONTRACT_SIZE_EXCEEDS_LIMIT, + 'Contract bytecode is too large. Max contract size is 100KB' + ) + ); + }); + it.only( - 'should deploy contracts greater than 100KB in chunks', + 'should deploy contracts greater than MAX_CONTRACT_SIZE via a loader contract', async () => { // USE TEST NODE - const provider = await Provider.create(LOCAL_NETWORK_URL); const baseAssetId = provider.getBaseAssetId(); const wallet = await generateTestWallet(provider, [[100_000_000, baseAssetId]]); From 7d93486563fba1350c988a09eba03167cd0a46c4 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Mon, 29 Jul 2024 14:00:23 +0100 Subject: [PATCH 014/135] feat: upgrade asm package --- apps/demo-nextjs/package.json | 2 +- apps/demo-react-cra/package.json | 2 +- apps/demo-react-vite/package.json | 2 +- packages/account/package.json | 2 +- packages/contract/package.json | 7 +++-- packages/contract/src/loader-script.ts | 27 ++++++++---------- .../fixtures/project/.fuels/stateConfig.json | 1 + .../utils/defaultSnapshots/stateConfig.json | 1 + pnpm-lock.yaml | 28 ++++++++++++------- 9 files changed, 39 insertions(+), 33 deletions(-) diff --git a/apps/demo-nextjs/package.json b/apps/demo-nextjs/package.json index fad3b1ebdb8..ec3dde0c6d0 100644 --- a/apps/demo-nextjs/package.json +++ b/apps/demo-nextjs/package.json @@ -10,7 +10,7 @@ "pretest": "pnpm original:build" }, "dependencies": { - "@fuels/vm-asm": "0.55.0", + "@fuels/vm-asm": "0.56.0", "@types/node": "20.14.11", "@types/react-dom": "18.3.0", "@types/react": "18.3.3", diff --git a/apps/demo-react-cra/package.json b/apps/demo-react-cra/package.json index cc3f870c3cc..5506fb0b2aa 100644 --- a/apps/demo-react-cra/package.json +++ b/apps/demo-react-cra/package.json @@ -3,7 +3,7 @@ "version": "0.1.29", "private": true, "dependencies": { - "@fuels/vm-asm": "0.55.0", + "@fuels/vm-asm": "0.56.0", "@testing-library/react": "^16.0.0", "@types/node": "^20.14.11", "@types/react": "^18.3.3", diff --git a/apps/demo-react-vite/package.json b/apps/demo-react-vite/package.json index 4082a4c0be3..f5d4075e2c5 100644 --- a/apps/demo-react-vite/package.json +++ b/apps/demo-react-vite/package.json @@ -11,7 +11,7 @@ "pretest": "pnpm original:build" }, "dependencies": { - "@fuels/vm-asm": "0.55.0", + "@fuels/vm-asm": "0.56.0", "fuels": "workspace:*", "react-dom": "^18.3.1", "react": "^18.3.1" diff --git a/packages/account/package.json b/packages/account/package.json index 8c9e856b2cc..a548ac57d88 100644 --- a/packages/account/package.json +++ b/packages/account/package.json @@ -59,7 +59,7 @@ "@fuel-ts/transactions": "workspace:*", "@fuel-ts/utils": "workspace:*", "@fuel-ts/versions": "workspace:*", - "@fuels/vm-asm": "0.55.0", + "@fuels/vm-asm": "0.56.0", "@noble/curves": "^1.4.2", "events": "^3.3.0", "graphql": "^16.9.0", diff --git a/packages/contract/package.json b/packages/contract/package.json index 327d1804e73..eb25d665ab6 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -39,23 +39,24 @@ }, "license": "Apache-2.0", "dependencies": { - "@fuels/vm-asm": "0.55.0", "@fuel-ts/abi-coder": "workspace:*", "@fuel-ts/account": "workspace:*", "@fuel-ts/crypto": "workspace:*", "@fuel-ts/errors": "workspace:*", "@fuel-ts/hasher": "workspace:*", "@fuel-ts/interfaces": "workspace:^", + "@fuel-ts/math": "workspace:^", "@fuel-ts/merkle": "workspace:*", "@fuel-ts/program": "workspace:*", "@fuel-ts/transactions": "workspace:*", "@fuel-ts/utils": "workspace:*", "@fuel-ts/versions": "workspace:*", + "@fuels/vm-asm": "0.56.0", "ramda": "^0.29.0" }, "devDependencies": { + "@fuel-ts/utils": "workspace:*", "@internal/forc": "workspace:*", - "@types/ramda": "^0.29.3", - "@fuel-ts/utils": "workspace:*" + "@types/ramda": "^0.29.3" } } diff --git a/packages/contract/src/loader-script.ts b/packages/contract/src/loader-script.ts index 53e3eac13c8..eda46dd9559 100644 --- a/packages/contract/src/loader-script.ts +++ b/packages/contract/src/loader-script.ts @@ -27,12 +27,10 @@ export const getLoaderInstructions = (blobIds: string[]): Uint8Array => { // 0x10 to hold the address of the current blob id asm.addi(0x10, 0x10, numberOfInstructions * Instruction.size()), // loop counter - asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs) - ).toBytes(), - // LOOP starts here - // 0x11 to hold the size of the current blob - bsizBytes(), - new InstructionSet( + asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs), + // LOOP starts here + // 0x11 to hold the size of the current blob + asm.bsiz(0x11, 0x10), // update the total size of the contract asm.add(0x12, 0x12, 0x11), // move on to the next blob @@ -50,19 +48,16 @@ export const getLoaderInstructions = (blobIds: string[]): Uint8Array => { // 0x12 is going to hold the total bytes loaded of the contract asm.move_(0x12, RegId.zero().to_u8()), // loop counter - asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs) - ).toBytes(), - // LOOP starts here - // 0x11 to hold the size of the current blob - bsizBytes(), - new InstructionSet( + asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs), + // LOOP starts here + // 0x11 to hold the size of the current blob + asm.bsiz(0x11, 0x10), // the location where to load the current blob (start of stack) asm.move_(0x14, RegId.spp().to_u8()), // move to where this blob should be loaded by adding the total bytes loaded - asm.add(0x14, 0x14, 0x12) - ).toBytes(), - blddBytes(), - new InstructionSet( + asm.add(0x14, 0x14, 0x12), + // load the current blob + asm.bldd(0x14, 0x10, RegId.zero().to_u8(), 0x11), // update the total bytes loaded asm.add(0x12, 0x12, 0x11), // move on to the next blob diff --git a/packages/fuels/test/fixtures/project/.fuels/stateConfig.json b/packages/fuels/test/fixtures/project/.fuels/stateConfig.json index d82415abb32..392b1337be9 100644 --- a/packages/fuels/test/fixtures/project/.fuels/stateConfig.json +++ b/packages/fuels/test/fixtures/project/.fuels/stateConfig.json @@ -461,6 +461,7 @@ } ], "contracts": [], + "blobs": [], "block_height": 0, "da_block_height": 0 } diff --git a/packages/utils/src/utils/defaultSnapshots/stateConfig.json b/packages/utils/src/utils/defaultSnapshots/stateConfig.json index b529d526570..47803660257 100644 --- a/packages/utils/src/utils/defaultSnapshots/stateConfig.json +++ b/packages/utils/src/utils/defaultSnapshots/stateConfig.json @@ -469,6 +469,7 @@ } ], "contracts": [], + "blobs": [], "block_height": 0, "da_block_height": 0 } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db61d315aae..d881ade1d47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -272,8 +272,8 @@ importers: apps/demo-nextjs: dependencies: '@fuels/vm-asm': - specifier: 0.55.0 - version: 0.55.0 + specifier: 0.56.0 + version: 0.56.0 '@types/node': specifier: 20.14.11 version: 20.14.11 @@ -308,8 +308,8 @@ importers: apps/demo-react-cra: dependencies: '@fuels/vm-asm': - specifier: 0.55.0 - version: 0.55.0 + specifier: 0.56.0 + version: 0.56.0 '@testing-library/react': specifier: ^16.0.0 version: 16.0.0(@testing-library/dom@8.20.1)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -351,8 +351,8 @@ importers: apps/demo-react-vite: dependencies: '@fuels/vm-asm': - specifier: 0.55.0 - version: 0.55.0 + specifier: 0.56.0 + version: 0.56.0 fuels: specifier: workspace:* version: link:../../packages/fuels @@ -702,8 +702,8 @@ importers: specifier: workspace:* version: link:../versions '@fuels/vm-asm': - specifier: 0.55.0 - version: 0.55.0 + specifier: 0.56.0 + version: 0.56.0 '@noble/curves': specifier: ^1.4.2 version: 1.4.2 @@ -795,6 +795,9 @@ importers: '@fuel-ts/interfaces': specifier: workspace:^ version: link:../interfaces + '@fuel-ts/math': + specifier: workspace:^ + version: link:../math '@fuel-ts/merkle': specifier: workspace:* version: link:../merkle @@ -811,8 +814,8 @@ importers: specifier: workspace:* version: link:../versions '@fuels/vm-asm': - specifier: 0.55.0 - version: 0.55.0 + specifier: 0.56.0 + version: 0.56.0 ramda: specifier: ^0.29.0 version: 0.29.0 @@ -3293,6 +3296,9 @@ packages: '@fuels/vm-asm@0.55.0': resolution: {integrity: sha512-k+n+ZF+s9ovPoURcRIUKxNHeniY3M8Fw6OBTdmc4jPtrNQnPb7BErGV7O6RnVGn/DEWp/zs6xHOmTjoZLR6UNg==} + '@fuels/vm-asm@0.56.0': + resolution: {integrity: sha512-oa9P55qkez/UASB8VNhTg3XflVWLHAC0JTKuCDLKZ+2ivxzLeHHcrIO+3AtJXLvUwTU9kC4rHMNUqXDaKz7HrA==} + '@graphql-codegen/add@5.0.3': resolution: {integrity: sha512-SxXPmramkth8XtBlAHu4H4jYcYXM/o3p01+psU+0NADQowA8jtYkK6MW5rV6T+CxkEaNZItfSmZRPgIuypcqnA==} peerDependencies: @@ -17611,6 +17617,8 @@ snapshots: '@fuels/vm-asm@0.55.0': {} + '@fuels/vm-asm@0.56.0': {} + '@graphql-codegen/add@5.0.3(graphql@16.9.0)': dependencies: '@graphql-codegen/plugin-helpers': 5.0.4(graphql@16.9.0) From b14c7536348729766d500d3c1b4a1c462acbd758 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 31 Jul 2024 15:40:39 +0100 Subject: [PATCH 015/135] feat: fix blob cost estimation, funding and blob id handling --- .fuel-core/configs/chainConfig.json | 12 +++++++ .fuel-core/configs/stateConfig.json | 1 + .../src/providers/fuel-core-schema.graphql | 4 ++- .../account/src/providers/operations.graphql | 10 +++++- .../blob-transaction-request.ts | 13 ++++--- .../transaction-response.ts | 7 ++++ packages/account/src/providers/utils/gas.ts | 8 ++++- packages/contract/src/contract-factory.ts | 27 +++++++++++---- packages/errors/src/error-codes.ts | 1 + .../fuel-gauge/src/contract-factory.test.ts | 34 ++++++++++++++++--- .../fixtures/project/.fuels/chainConfig.json | 12 +++++++ .../utils/defaultSnapshots/chainConfig.json | 12 +++++++ packages/utils/src/utils/types.ts | 2 ++ 13 files changed, 124 insertions(+), 19 deletions(-) diff --git a/.fuel-core/configs/chainConfig.json b/.fuel-core/configs/chainConfig.json index e4ea2be7c8d..bbdd787d914 100644 --- a/.fuel-core/configs/chainConfig.json +++ b/.fuel-core/configs/chainConfig.json @@ -135,6 +135,18 @@ "gasPerUnit": 0 } }, + "bsiz": { + "LightOperation": { + "base": 17, + "units_per_gas": 790 + } + }, + "bldd": { + "LightOperation": { + "base": 15, + "units_per_gas": 272 + } + }, "cfe": { "HeavyOperation": { "base": 2, diff --git a/.fuel-core/configs/stateConfig.json b/.fuel-core/configs/stateConfig.json index b529d526570..47803660257 100644 --- a/.fuel-core/configs/stateConfig.json +++ b/.fuel-core/configs/stateConfig.json @@ -469,6 +469,7 @@ } ], "contracts": [], + "blobs": [], "block_height": 0, "da_block_height": 0 } diff --git a/packages/account/src/providers/fuel-core-schema.graphql b/packages/account/src/providers/fuel-core-schema.graphql index b2a4b135b31..62b34c8ff15 100644 --- a/packages/account/src/providers/fuel-core-schema.graphql +++ b/packages/account/src/providers/fuel-core-schema.graphql @@ -389,7 +389,6 @@ type GasCosts { divi: U64! ecr1: U64! eck1: U64! - ed19: U64! eq: U64! exp: U64! expi: U64! @@ -462,12 +461,15 @@ type GasCosts { xor: U64! xori: U64! alocDependentCost: DependentCost! + bldd: DependentCost + bsiz: DependentCost cfe: DependentCost! cfeiDependentCost: DependentCost! call: DependentCost! ccp: DependentCost! croo: DependentCost! csiz: DependentCost! + ed19: DependentCost! k256: DependentCost! ldc: DependentCost! logd: DependentCost! diff --git a/packages/account/src/providers/operations.graphql b/packages/account/src/providers/operations.graphql index e65c99b3a1e..133998094a9 100644 --- a/packages/account/src/providers/operations.graphql +++ b/packages/account/src/providers/operations.graphql @@ -323,7 +323,6 @@ fragment GasCostsFragment on GasCosts { divi ecr1 eck1 - ed19 eq exp expi @@ -398,6 +397,12 @@ fragment GasCostsFragment on GasCosts { alocDependentCost { ...DependentCostFragment } + bldd { + ...DependentCostFragment + } + bsiz { + ...DependentCostFragment + } cfe { ...DependentCostFragment } @@ -416,6 +421,9 @@ fragment GasCostsFragment on GasCosts { csiz { ...DependentCostFragment } + ed19 { + ...DependentCostFragment + } k256 { ...DependentCostFragment } diff --git a/packages/account/src/providers/transaction-request/blob-transaction-request.ts b/packages/account/src/providers/transaction-request/blob-transaction-request.ts index fd73e410559..6015dbdd8e7 100644 --- a/packages/account/src/providers/transaction-request/blob-transaction-request.ts +++ b/packages/account/src/providers/transaction-request/blob-transaction-request.ts @@ -1,4 +1,3 @@ -import { hash } from '@fuel-ts/hasher'; import type { BN } from '@fuel-ts/math'; import type { TransactionBlob } from '@fuel-ts/transactions'; @@ -10,6 +9,8 @@ import type { BaseTransactionRequestLike } from './transaction-request'; import { BaseTransactionRequest, TransactionType } from './transaction-request'; export interface BlobTransactionRequestLike extends BaseTransactionRequestLike { + /** Blob ID */ + blobId: string; /** Witness index of contract bytecode to create */ witnessIndex?: number; } @@ -24,6 +25,8 @@ export class BlobTransactionRequest extends BaseTransactionRequest { /** Type of the transaction */ type = TransactionType.Blob as const; + /** Blob ID */ + blobId: string; /** Witness index of contract bytecode to create */ witnessIndex: number; @@ -32,8 +35,9 @@ export class BlobTransactionRequest extends BaseTransactionRequest { * * @param blobTransactionRequestLike - The initial values for the instance */ - constructor({ witnessIndex, ...rest }: BlobTransactionRequestLike) { + constructor({ witnessIndex, blobId, ...rest }: BlobTransactionRequestLike) { super(rest); + this.blobId = blobId; this.witnessIndex = witnessIndex ?? 0; } @@ -49,11 +53,11 @@ export class BlobTransactionRequest extends BaseTransactionRequest { } const baseTransaction = this.getBaseTransaction(); - const witnessIndex = this.witnessIndex; + const { witnessIndex, blobId } = this; return { type: TransactionType.Blob, ...baseTransaction, - blobId: hash(this.witnesses[witnessIndex]), + blobId, witnessIndex, }; } @@ -73,6 +77,7 @@ export class BlobTransactionRequest extends BaseTransactionRequest { return calculateMetadataGasForTxBlob({ gasCosts, txBytesSize: this.byteSize(), + witnessBytesSize: this.witnesses[this.witnessIndex].length, }); } } diff --git a/packages/account/src/providers/transaction-response/transaction-response.ts b/packages/account/src/providers/transaction-response/transaction-response.ts index 201e5b53dec..4ff88b0a7ab 100644 --- a/packages/account/src/providers/transaction-response/transaction-response.ts +++ b/packages/account/src/providers/transaction-response/transaction-response.ts @@ -228,6 +228,13 @@ export class TransactionResponse { for await (const { statusChange } of subscription) { if (statusChange.type === 'SqueezedOutStatus') { + if (/BlobIdAlreadyUploaded/i.exec(statusChange.reason)) { + throw new FuelError( + ErrorCode.BLOB_ID_ALREADY_UPLOADED, + `Blob ID already uploaded and is available for reuse.` + ); + } + throw new FuelError( ErrorCode.TRANSACTION_SQUEEZED_OUT, `Transaction Squeezed Out with reason: ${statusChange.reason}` diff --git a/packages/account/src/providers/utils/gas.ts b/packages/account/src/providers/utils/gas.ts index 48a219561e4..ef0bcc82fbd 100644 --- a/packages/account/src/providers/utils/gas.ts +++ b/packages/account/src/providers/utils/gas.ts @@ -2,6 +2,7 @@ import { bn } from '@fuel-ts/math'; import type { BN, BNInput } from '@fuel-ts/math'; import { ReceiptType, type Input } from '@fuel-ts/transactions'; import { arrayify } from '@fuel-ts/utils'; +import { blob } from 'stream/consumers'; import type { GqlDependentCost, @@ -103,6 +104,7 @@ export interface IGetMaxGasParams { gasPerByte: BN; minGas: BN; gasLimit?: BN; + blobSize?: BN; maxGasPerTx: BN; } @@ -161,11 +163,15 @@ export function calculateMetadataGasForTxScript({ export function calculateMetadataGasForTxBlob({ gasCosts, txBytesSize, + witnessBytesSize, }: { gasCosts: GasCosts; txBytesSize: number; + witnessBytesSize: number; }) { - return resolveGasDependentCosts(txBytesSize, gasCosts.s256); + const txId = resolveGasDependentCosts(txBytesSize, gasCosts.s256); + const blobLen = resolveGasDependentCosts(witnessBytesSize, gasCosts.s256); + return txId.add(blobLen); } export interface CalculateGasFeeParams { diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index 204e9d7870a..d0a945c9f8f 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -15,6 +15,7 @@ import { } from '@fuel-ts/account'; import { randomBytes } from '@fuel-ts/crypto'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; +import { hash } from '@fuel-ts/hasher'; import type { BytesLike } from '@fuel-ts/interfaces'; import { Contract } from '@fuel-ts/program'; import type { StorageSlot } from '@fuel-ts/transactions'; @@ -159,6 +160,7 @@ export default class ContractFactory { blobTransactionRequest(options: { blobBytecode: Uint8Array } & DeployContractOptions) { const { blobBytecode } = options; return new BlobTransactionRequest({ + blobId: hash(blobBytecode), witnessIndex: 0, witnesses: [blobBytecode], ...options, @@ -257,24 +259,35 @@ export default class ContractFactory { // Deploy the chunks as blobs for (const { id, bytecode } of chunks) { - const blobRequest = new BlobTransactionRequest({ - witnessIndex: 0, - witnesses: [bytecode], + const blobRequest = this.blobTransactionRequest({ + blobBytecode: bytecode, + ...deployContractOptions, }); + chunks[id].blobId = blobRequest.blobId; + const fundedBlobRequest = await this.fundTransactionRequest( blobRequest, deployContractOptions ); - const check = await account.sendTransaction(fundedBlobRequest); - const result = await check.waitForResult(); + let result: TransactionResult; + + try { + const blobTx = await account.sendTransaction(fundedBlobRequest); + result = await blobTx.waitForResult(); + } catch (err: unknown) { + if ((err).code === ErrorCode.BLOB_ID_ALREADY_UPLOADED) { + // eslint-disable-next-line no-continue + continue; + } - if (!result.status || result.status !== TransactionStatus.success) { throw new FuelError(ErrorCode.TRANSACTION_FAILED, 'Failed to deploy contract chunk'); } - chunks[id].blobId = result.transaction.blobId; + if (!result.status || result.status !== TransactionStatus.success) { + throw new FuelError(ErrorCode.TRANSACTION_FAILED, 'Failed to deploy contract chunk'); + } } // Get the loader bytecode diff --git a/packages/errors/src/error-codes.ts b/packages/errors/src/error-codes.ts index 4fc0c2f59d7..7ec0fc3c46f 100644 --- a/packages/errors/src/error-codes.ts +++ b/packages/errors/src/error-codes.ts @@ -73,6 +73,7 @@ export enum ErrorCode { DUPLICATED_POLICY = 'duplicated-policy', TRANSACTION_SQUEEZED_OUT = 'transaction-squeezed-out', CONTRACT_SIZE_EXCEEDS_LIMIT = 'contract-size-exceeds-limit', + BLOB_ID_ALREADY_UPLOADED = 'blob-id-already-uploaded', // receipt INVALID_RECEIPT_TYPE = 'invalid-receipt-type', diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index 38e080b8c41..19bad80917e 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -3,7 +3,16 @@ import { generateTestWallet } from '@fuel-ts/account/test-utils'; import { FuelError, ErrorCode } from '@fuel-ts/errors'; import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import type { DeployContractOptions } from 'fuels'; -import { BN, bn, toHex, Interface, ContractFactory, LOCAL_NETWORK_URL, Provider } from 'fuels'; +import { + BN, + bn, + toHex, + Interface, + ContractFactory, + LOCAL_NETWORK_URL, + Provider, + assets, +} from 'fuels'; import { launchTestNode } from 'fuels/test-utils'; import type { LargeContractAbi } from '../test/typegen/contracts'; @@ -278,9 +287,24 @@ describe('Contract Factory', () => { 'should deploy contracts greater than MAX_CONTRACT_SIZE via a loader contract', async () => { // USE TEST NODE - const provider = await Provider.create(LOCAL_NETWORK_URL); - const baseAssetId = provider.getBaseAssetId(); - const wallet = await generateTestWallet(provider, [[100_000_000, baseAssetId]]); + using launched = await launchTestNode({ + nodeOptions: { + args: ['--poa-instant', 'false', '--poa-interval-period', '1s'], + }, + providerOptions: { + cacheUtxo: -1, + }, + }); + + const { + wallets: [wallet], + provider, + } = launched; + + // USING NODE + // const provider = await Provider.create(LOCAL_NETWORK_URL, { cacheUtxo: -1 }); + // const baseAssetId = provider.getBaseAssetId(); + // const wallet = await generateTestWallet(provider, [[100_000_000, baseAssetId]]); const salt = new Uint8Array([ 166, 23, 175, 50, 185, 247, 229, 160, 32, 86, 191, 57, 44, 165, 193, 78, 134, 144, 54, 219, @@ -300,6 +324,6 @@ describe('Contract Factory', () => { const { value } = await call.waitForResult(); expect(value).toBe(1001); }, - { timeout: 20000 } + { timeout: 60000 } ); }); diff --git a/packages/fuels/test/fixtures/project/.fuels/chainConfig.json b/packages/fuels/test/fixtures/project/.fuels/chainConfig.json index 2977a19e76f..4f7bf446d80 100644 --- a/packages/fuels/test/fixtures/project/.fuels/chainConfig.json +++ b/packages/fuels/test/fixtures/project/.fuels/chainConfig.json @@ -135,6 +135,18 @@ "gasPerUnit": 0 } }, + "bsiz": { + "LightOperation": { + "base": 17, + "units_per_gas": 790 + } + }, + "bldd": { + "LightOperation": { + "base": 15, + "units_per_gas": 272 + } + }, "cfe": { "HeavyOperation": { "base": 2, diff --git a/packages/utils/src/utils/defaultSnapshots/chainConfig.json b/packages/utils/src/utils/defaultSnapshots/chainConfig.json index e4ea2be7c8d..bbdd787d914 100644 --- a/packages/utils/src/utils/defaultSnapshots/chainConfig.json +++ b/packages/utils/src/utils/defaultSnapshots/chainConfig.json @@ -135,6 +135,18 @@ "gasPerUnit": 0 } }, + "bsiz": { + "LightOperation": { + "base": 17, + "units_per_gas": 790 + } + }, + "bldd": { + "LightOperation": { + "base": 15, + "units_per_gas": 272 + } + }, "cfe": { "HeavyOperation": { "base": 2, diff --git a/packages/utils/src/utils/types.ts b/packages/utils/src/utils/types.ts index c0cef584131..fba5722181b 100644 --- a/packages/utils/src/utils/types.ts +++ b/packages/utils/src/utils/types.ts @@ -51,6 +51,8 @@ interface GasCosts { bhei: number; bhsh: number; burn: number; + bldd: Operation; + bsiz: Operation; cb: number; cfei: number; cfsi: number; From f68392356aa0c4f244b170f6b370ecb22117cb51 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 31 Jul 2024 19:41:46 +0100 Subject: [PATCH 016/135] feat: dynamic blob sizing --- .../blob-transaction-request.ts | 7 +--- .../transaction-request.ts | 4 ++ packages/contract/src/contract-factory.ts | 38 +++++++++++++++---- packages/contract/src/loader-script.ts | 4 -- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/account/src/providers/transaction-request/blob-transaction-request.ts b/packages/account/src/providers/transaction-request/blob-transaction-request.ts index 6015dbdd8e7..f66d19e7f29 100644 --- a/packages/account/src/providers/transaction-request/blob-transaction-request.ts +++ b/packages/account/src/providers/transaction-request/blob-transaction-request.ts @@ -1,3 +1,5 @@ +import { randomBytes } from '@fuel-ts/crypto'; +import { hash } from '@fuel-ts/hasher'; import type { BN } from '@fuel-ts/math'; import type { TransactionBlob } from '@fuel-ts/transactions'; @@ -47,11 +49,6 @@ export class BlobTransactionRequest extends BaseTransactionRequest { * @returns The transaction create object. */ toTransaction(): TransactionBlob { - const blobBytecode = this.witnesses[this.witnessIndex]; - if (!blobBytecode) { - throw new Error('NO BLOB BYTECODE'); - } - const baseTransaction = this.getBaseTransaction(); const { witnessIndex, blobId } = this; return { diff --git a/packages/account/src/providers/transaction-request/transaction-request.ts b/packages/account/src/providers/transaction-request/transaction-request.ts index df903f200da..66db4061ded 100644 --- a/packages/account/src/providers/transaction-request/transaction-request.ts +++ b/packages/account/src/providers/transaction-request/transaction-request.ts @@ -689,4 +689,8 @@ export abstract class BaseTransactionRequest implements BaseTransactionRequestLi } }); } + + byteLength(): number { + return this.toTransactionBytes().byteLength; + } } diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index d0a945c9f8f..16b93fa80b2 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -1,4 +1,4 @@ -import { Interface } from '@fuel-ts/abi-coder'; +import { Interface, WORD_SIZE } from '@fuel-ts/abi-coder'; import type { JsonAbi, InputValue } from '@fuel-ts/abi-coder'; import type { Account, @@ -7,6 +7,7 @@ import type { TransactionRequest, TransactionResult, TransactionType, + CoinQuantity, } from '@fuel-ts/account'; import { CreateTransactionRequest, @@ -17,9 +18,10 @@ import { randomBytes } from '@fuel-ts/crypto'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; import { hash } from '@fuel-ts/hasher'; import type { BytesLike } from '@fuel-ts/interfaces'; +import { bn } from '@fuel-ts/math'; import { Contract } from '@fuel-ts/program'; import type { StorageSlot } from '@fuel-ts/transactions'; -import { arrayify, isDefined } from '@fuel-ts/utils'; +import { arrayify, isDefined, concat } from '@fuel-ts/utils'; import { getLoaderInstructions } from './loader-script'; import { getContractId, getContractStorageRoot, hexlifyWithPrefix } from './util'; @@ -236,8 +238,8 @@ export default class ContractFactory { const { consensusParameters } = account.provider.getChain(); const maxContractSize = consensusParameters.contractParameters.contractMaxSize.toNumber(); - // Ensure the max contract size is byte aligned (VM requirement) - if (maxContractSize % 8 !== 0) { + // Ensure the max contract size is byte aligned before chunking + if (maxContractSize % WORD_SIZE !== 0) { throw new FuelError( ErrorCode.CONTRACT_SIZE_EXCEEDS_LIMIT, // Todo: change error `Max contract size of ${maxContractSize} bytes is not byte aligned.` @@ -249,21 +251,39 @@ export default class ContractFactory { this.setConfigurableConstants(configurableConstants); } + // Find the max chunk size (again, ensure is word aligned) + const baseAssetId = account.provider.getBaseAssetId(); + const fakeFunding: CoinQuantity[] = [{ assetId: baseAssetId, amount: bn(100_000) }]; + const baseBlobRequest = this.blobTransactionRequest({ blobBytecode: randomBytes(32) }); + baseBlobRequest.fundWithFakeUtxos(fakeFunding, baseAssetId); + const maxChunkSize = maxContractSize - baseBlobRequest.byteLength() - WORD_SIZE; + const padding = maxChunkSize % WORD_SIZE; + const chunkSize = padding + ((WORD_SIZE - (padding % WORD_SIZE)) % WORD_SIZE) + maxChunkSize; + // Chunk the bytecode const chunks: ContractChunk[] = []; - const chunkSize = maxContractSize - 10_000; // Come up with better max size calculation for (let offset = 0, index = 0; offset < this.bytecode.length; offset += chunkSize, index++) { const chunk = this.bytecode.slice(offset, offset + chunkSize); + + // Chunks must be byte aligned + if (chunk.length % WORD_SIZE !== 0) { + const paddingLength = chunk.length % WORD_SIZE; + const paddedChunk = concat([chunk, new Uint8Array(paddingLength)]); + chunks.push({ id: index, size: paddedChunk.length, bytecode: paddedChunk }); + // eslint-disable-next-line no-continue + continue; + } chunks.push({ id: index, size: chunk.length, bytecode: chunk }); } - // Deploy the chunks as blobs + // Deploy the chunks as blob txs for (const { id, bytecode } of chunks) { const blobRequest = this.blobTransactionRequest({ blobBytecode: bytecode, ...deployContractOptions, }); + // Store the blobIds for the loader contract chunks[id].blobId = blobRequest.blobId; const fundedBlobRequest = await this.fundTransactionRequest( @@ -277,7 +297,11 @@ export default class ContractFactory { const blobTx = await account.sendTransaction(fundedBlobRequest); result = await blobTx.waitForResult(); } catch (err: unknown) { + // Core will throw for blobs that have already been uploaded, but the blobId + // is still valid so we can use this for the loader contract if ((err).code === ErrorCode.BLOB_ID_ALREADY_UPLOADED) { + // TODO: We need to unset the cached utxo as it can be reused + // this.account?.provider.cache?.del(UTXO_ID); // eslint-disable-next-line no-continue continue; } @@ -294,7 +318,7 @@ export default class ContractFactory { const blobIds = chunks.map((c) => c.blobId as string); const loaderBytecode = getLoaderInstructions(blobIds); - // Deploy the loader contract + // Deploy the loader contract via create tx const { contractId, transactionRequest: createRequest } = this.createTransactionRequest({ bytecode: loaderBytecode, ...deployContractOptions, diff --git a/packages/contract/src/loader-script.ts b/packages/contract/src/loader-script.ts index eda46dd9559..eabb64173aa 100644 --- a/packages/contract/src/loader-script.ts +++ b/packages/contract/src/loader-script.ts @@ -11,10 +11,6 @@ export const getLoaderInstructions = (blobIds: string[]): Uint8Array => { const numberOfInstructions = numberOfBlobs * instructionsPerBlob; const blobIdSize = BYTES_32; - // Btyes for the BSIZ opcode - const bsizBytes = () => new Uint8Array([186, 69, 0, 0]); - // Bytes for the BLDD opcode - const blddBytes = () => new Uint8Array([187, 81, 0, 17]); // Bytes for the Blob Ids const blobIdBytes = () => concat(blobIds.map((b) => arrayify(b))); From fd95a20d87038b30aa1d03fc356648027606ff57 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 31 Jul 2024 19:42:34 +0100 Subject: [PATCH 017/135] chore: lint --- .../providers/transaction-request/blob-transaction-request.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/account/src/providers/transaction-request/blob-transaction-request.ts b/packages/account/src/providers/transaction-request/blob-transaction-request.ts index f66d19e7f29..24d7b2ccfa7 100644 --- a/packages/account/src/providers/transaction-request/blob-transaction-request.ts +++ b/packages/account/src/providers/transaction-request/blob-transaction-request.ts @@ -1,5 +1,3 @@ -import { randomBytes } from '@fuel-ts/crypto'; -import { hash } from '@fuel-ts/hasher'; import type { BN } from '@fuel-ts/math'; import type { TransactionBlob } from '@fuel-ts/transactions'; From 60e7db70a306e3771b8ff2079bf3e5812f75aa31 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Thu, 1 Aug 2024 21:38:17 +0100 Subject: [PATCH 018/135] feat: fuel-core with ed19 --- .fuel-core/configs/chainConfig.json | 6 ++++++ packages/account/src/providers/fuel-core-schema.graphql | 3 ++- packages/account/src/providers/operations.graphql | 3 ++- packages/utils/src/utils/defaultSnapshots/chainConfig.json | 6 ++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.fuel-core/configs/chainConfig.json b/.fuel-core/configs/chainConfig.json index bbdd787d914..0591f78bad8 100644 --- a/.fuel-core/configs/chainConfig.json +++ b/.fuel-core/configs/chainConfig.json @@ -183,6 +183,12 @@ "units_per_gas": 237 } }, + "ed19DependentCost": { + "LightOperation": { + "base": 45, + "units_per_gas": 237 + } + }, "k256": { "LightOperation": { "base": 37, diff --git a/packages/account/src/providers/fuel-core-schema.graphql b/packages/account/src/providers/fuel-core-schema.graphql index 62b34c8ff15..d01dfe75f50 100644 --- a/packages/account/src/providers/fuel-core-schema.graphql +++ b/packages/account/src/providers/fuel-core-schema.graphql @@ -389,6 +389,7 @@ type GasCosts { divi: U64! ecr1: U64! eck1: U64! + ed19: U64! eq: U64! exp: U64! expi: U64! @@ -469,7 +470,7 @@ type GasCosts { ccp: DependentCost! croo: DependentCost! csiz: DependentCost! - ed19: DependentCost! + ed19DependentCost: DependentCost! k256: DependentCost! ldc: DependentCost! logd: DependentCost! diff --git a/packages/account/src/providers/operations.graphql b/packages/account/src/providers/operations.graphql index 133998094a9..0419221e412 100644 --- a/packages/account/src/providers/operations.graphql +++ b/packages/account/src/providers/operations.graphql @@ -323,6 +323,7 @@ fragment GasCostsFragment on GasCosts { divi ecr1 eck1 + ed19 eq exp expi @@ -421,7 +422,7 @@ fragment GasCostsFragment on GasCosts { csiz { ...DependentCostFragment } - ed19 { + ed19DependentCost { ...DependentCostFragment } k256 { diff --git a/packages/utils/src/utils/defaultSnapshots/chainConfig.json b/packages/utils/src/utils/defaultSnapshots/chainConfig.json index bbdd787d914..0591f78bad8 100644 --- a/packages/utils/src/utils/defaultSnapshots/chainConfig.json +++ b/packages/utils/src/utils/defaultSnapshots/chainConfig.json @@ -183,6 +183,12 @@ "units_per_gas": 237 } }, + "ed19DependentCost": { + "LightOperation": { + "base": 45, + "units_per_gas": 237 + } + }, "k256": { "LightOperation": { "base": 37, From 2335817b9889d941d4bb07761d7f2b1570a74646 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Fri, 2 Aug 2024 07:37:29 +0100 Subject: [PATCH 019/135] feat: v4 gas costs, loader fixes and better blob id handling --- .fuel-core/configs/chainConfig.json | 87 +++++++++---------- packages/contract/src/contract-factory.ts | 54 +++++++----- packages/contract/src/loader-script.ts | 57 ++++-------- .../fuel-gauge/src/contract-factory.test.ts | 28 +++--- .../utils/src/utils/defaultSnapshotConfigs.ts | 1 + .../utils/defaultSnapshots/chainConfig.json | 87 +++++++++---------- 6 files changed, 152 insertions(+), 162 deletions(-) diff --git a/.fuel-core/configs/chainConfig.json b/.fuel-core/configs/chainConfig.json index 0591f78bad8..13d9a1adc5f 100644 --- a/.fuel-core/configs/chainConfig.json +++ b/.fuel-core/configs/chainConfig.json @@ -40,51 +40,47 @@ }, "chain_id": 0, "gas_costs": { - "V1": { + "V4": { "add": 2, "addi": 2, - "aloc": 2, "and": 2, "andi": 2, - "bal": 86, + "bal": 29, "bhei": 2, "bhsh": 2, - "burn": 25770, + "burn": 19976, "cb": 2, - "cfei": 2, "cfsi": 2, "div": 2, "divi": 2, - "eck1": 3114, - "ecr1": 42270, - "ed19": 2878, + "eck1": 1907, + "ecr1": 26135, "eq": 2, "exp": 2, "expi": 2, - "flag": 1, + "flag": 2, "gm": 2, "gt": 2, - "gtf": 12, + "gtf": 13, "ji": 2, "jmp": 2, "jne": 2, "jnei": 2, "jnzi": 2, - "jmpf": 1, - "jmpb": 1, - "jnzf": 1, - "jnzb": 1, - "jnef": 1, - "jneb": 1, + "jmpf": 2, + "jmpb": 2, + "jnzf": 2, + "jnzb": 2, + "jnef": 2, + "jneb": 2, "lb": 2, - "log": 165, + "log": 102, "lt": 2, "lw": 2, - "mint": 29024, + "mint": 18042, "mlog": 2, - "mod_op": 2, "modi": 2, - "move_op": 2, + "mod_op": 2, "movi": 2, "mroo": 4, "mul": 2, @@ -96,43 +92,44 @@ "ori": 2, "poph": 3, "popl": 3, - "pshh": 4, - "pshl": 4, - "ret": 134, - "rvrt": 153, + "pshh": 5, + "pshl": 5, + "move_op": 2, + "ret": 53, "sb": 2, "sll": 2, "slli": 2, "srl": 2, "srli": 2, - "srw": 209, + "srw": 177, "sub": 2, "subi": 2, "sw": 2, - "sww": 22501, - "time": 50, - "tr": 33912, - "tro": 24294, + "sww": 17302, + "time": 35, + "tr": 27852, + "tro": 19718, "wdcm": 2, - "wqcm": 3, + "wqcm": 2, "wdop": 3, "wqop": 3, "wdml": 3, - "wqml": 4, - "wddv": 5, - "wqdv": 6, - "wdmd": 10, - "wqmd": 17, - "wdam": 9, - "wqam": 11, - "wdmm": 10, - "wqmm": 10, + "wqml": 3, + "wddv": 4, + "wqdv": 5, + "wdmd": 8, + "wqmd": 12, + "wdam": 7, + "wqam": 8, + "wdmm": 8, + "wqmm": 8, "xor": 2, "xori": 2, - "alocDependentCost": { + "rvrt": 2, + "aloc": { "HeavyOperation": { "base": 2, - "gasPerUnit": 0 + "gas_per_unit": 0 } }, "bsiz": { @@ -150,13 +147,13 @@ "cfe": { "HeavyOperation": { "base": 2, - "gasPerUnit": 0 + "gas_per_unit": 0 } }, - "cfeiDependentCost": { + "cfei": { "HeavyOperation": { "base": 2, - "gasPerUnit": 0 + "gas_per_unit": 0 } }, "call": { @@ -183,7 +180,7 @@ "units_per_gas": 237 } }, - "ed19DependentCost": { + "ed19": { "LightOperation": { "base": 45, "units_per_gas": 237 diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index 16b93fa80b2..defe2f70fe9 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -276,6 +276,8 @@ export default class ContractFactory { chunks.push({ id: index, size: chunk.length, bytecode: chunk }); } + const uploadedBlobIds: string[] = []; + // Deploy the chunks as blob txs for (const { id, bytecode } of chunks) { const blobRequest = this.blobTransactionRequest({ @@ -284,33 +286,39 @@ export default class ContractFactory { }); // Store the blobIds for the loader contract - chunks[id].blobId = blobRequest.blobId; - - const fundedBlobRequest = await this.fundTransactionRequest( - blobRequest, - deployContractOptions - ); + const { blobId } = blobRequest; + chunks[id].blobId = blobId; + + // Upload the blob if it hasn't been uploaded yet. Duplicate blob IDs will fail gracefully. + if (!uploadedBlobIds.includes(blobId)) { + const fundedBlobRequest = await this.fundTransactionRequest( + blobRequest, + deployContractOptions + ); - let result: TransactionResult; - - try { - const blobTx = await account.sendTransaction(fundedBlobRequest); - result = await blobTx.waitForResult(); - } catch (err: unknown) { - // Core will throw for blobs that have already been uploaded, but the blobId - // is still valid so we can use this for the loader contract - if ((err).code === ErrorCode.BLOB_ID_ALREADY_UPLOADED) { - // TODO: We need to unset the cached utxo as it can be reused - // this.account?.provider.cache?.del(UTXO_ID); - // eslint-disable-next-line no-continue - continue; + let result: TransactionResult; + + try { + const blobTx = await account.sendTransaction(fundedBlobRequest); + result = await blobTx.waitForResult(); + } catch (err: unknown) { + // Core will throw for blobs that have already been uploaded, but the blobId + // is still valid so we can use this for the loader contract + if ((err).code === ErrorCode.BLOB_ID_ALREADY_UPLOADED) { + // TODO: We need to unset the cached utxo as it can be reused + // this.account?.provider.cache?.del(UTXO_ID); + // eslint-disable-next-line no-continue + continue; + } + + throw new FuelError(ErrorCode.TRANSACTION_FAILED, 'Failed to deploy contract chunk'); } - throw new FuelError(ErrorCode.TRANSACTION_FAILED, 'Failed to deploy contract chunk'); - } + if (!result.status || result.status !== TransactionStatus.success) { + throw new FuelError(ErrorCode.TRANSACTION_FAILED, 'Failed to deploy contract chunk'); + } - if (!result.status || result.status !== TransactionStatus.success) { - throw new FuelError(ErrorCode.TRANSACTION_FAILED, 'Failed to deploy contract chunk'); + uploadedBlobIds.push(blobId); } } diff --git a/packages/contract/src/loader-script.ts b/packages/contract/src/loader-script.ts index eabb64173aa..7df48942224 100644 --- a/packages/contract/src/loader-script.ts +++ b/packages/contract/src/loader-script.ts @@ -6,7 +6,7 @@ import * as asm from '@fuels/vm-asm'; export const getLoaderInstructions = (blobIds: string[]): Uint8Array => { const { RegId, Instruction } = asm; - const instructionsPerBlob = 26; + const instructionsPerBlob = 12; const numberOfBlobs = blobIds.length; const numberOfInstructions = numberOfBlobs * instructionsPerBlob; const blobIdSize = BYTES_32; @@ -14,57 +14,36 @@ export const getLoaderInstructions = (blobIds: string[]): Uint8Array => { // Bytes for the Blob Ids const blobIdBytes = () => concat(blobIds.map((b) => arrayify(b))); + // There are 2 main steps: + // 1. Load the blob contents into memory + // 2. Jump to the beginning of the memory where the blobs were loaded + // After that the execution continues normally with the loaded contract reading our + // prepared fn selector and jumps to the selected contract method. return concat([ new InstructionSet( - // 0x12 is going to hold the total size of the contract - asm.move_(0x12, RegId.zero().to_u8()), - // find the start of the hardcoded blob ids, which are located after the code ends - asm.move_(0x10, RegId.is().to_u8()), + // 1. load the blob contents into memory + // find the start of the hardcoded blob ids, which are located after the code ends, + asm.move_(0x10, RegId.zero().to_u8()), // 0x10 to hold the address of the current blob id asm.addi(0x10, 0x10, numberOfInstructions * Instruction.size()), + // The contract is going to be loaded from the current value of SP onwards, save + // the location into 0x16 so we can jump into it later on + asm.move_(0x16, RegId.sp().to_u8()), // loop counter - asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs), + asm.movi(0x13, numberOfBlobs), // LOOP starts here // 0x11 to hold the size of the current blob asm.bsiz(0x11, 0x10), - // update the total size of the contract - asm.add(0x12, 0x12, 0x11), + // push the blob contents onto the stack + asm.ldc(0x10, 0, 0x11, 1), // move on to the next blob asm.addi(0x10, 0x10, blobIdSize), // decrement the loop counter asm.subi(0x13, 0x13, 1), - // Jump backwards 3 instructions if the counter has not reached 0 - asm.jneb(0x13, RegId.zero().to_u8(), RegId.zero().to_u8(), 3), - // move the stack pointer by the contract size since we need to write the contract on the stack since only that memory can be executed - asm.cfe(0x12), - // find the start of the hardcoded blob ids, which are located after the code ends - asm.move_(0x10, RegId.is().to_u8()), - // 0x10 to hold the address of the current blob id - asm.addi(0x10, 0x10, numberOfInstructions * Instruction.size()), - // 0x12 is going to hold the total bytes loaded of the contract - asm.move_(0x12, RegId.zero().to_u8()), - // loop counter - asm.addi(0x13, RegId.zero().to_u8(), numberOfBlobs), - // LOOP starts here - // 0x11 to hold the size of the current blob - asm.bsiz(0x11, 0x10), - // the location where to load the current blob (start of stack) - asm.move_(0x14, RegId.spp().to_u8()), - // move to where this blob should be loaded by adding the total bytes loaded - asm.add(0x14, 0x14, 0x12), - // load the current blob - asm.bldd(0x14, 0x10, RegId.zero().to_u8(), 0x11), - // update the total bytes loaded - asm.add(0x12, 0x12, 0x11), - // move on to the next blob - asm.addi(0x10, 0x10, blobIdSize), - // decrement the loop counter - asm.subi(0x13, 0x13, 1), - // Jump backwards 6 instructions if the counter has not reached 0 - asm.jneb(0x13, RegId.zero().to_u8(), RegId.zero().to_u8(), 6), + // Jump backwards (3+1) instructions if the counter has not reached 0 + asm.jnzb(0x13, RegId.zero().to_u8(), 3), + // Jump into the memory where the contract is loaded // what follows is called _jmp_mem by the sway compiler - // move to the start of the stack (also the start of the contract we loaded) - asm.move_(0x16, RegId.spp().to_u8()), // subtract the address contained in IS because jmp will add it back asm.sub(0x16, 0x16, RegId.is().to_u8()), // jmp will multiply by 4 so we need to divide to cancel that out diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index 19bad80917e..a75b3a3551d 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -288,9 +288,6 @@ describe('Contract Factory', () => { async () => { // USE TEST NODE using launched = await launchTestNode({ - nodeOptions: { - args: ['--poa-instant', 'false', '--poa-interval-period', '1s'], - }, providerOptions: { cacheUtxo: -1, }, @@ -306,24 +303,35 @@ describe('Contract Factory', () => { // const baseAssetId = provider.getBaseAssetId(); // const wallet = await generateTestWallet(provider, [[100_000_000, baseAssetId]]); - const salt = new Uint8Array([ - 166, 23, 175, 50, 185, 247, 229, 160, 32, 86, 191, 57, 44, 165, 193, 78, 134, 144, 54, 219, - 234, 246, 163, 190, 132, 237, 251, 228, 12, 13, 127, 193, - ]); - const options: DeployContractOptions = { salt }; + // SMALL CONTRACT WITH LOADER + // const factory = new ContractFactory( + // StorageTestContractAbiHex, + // StorageTestContractAbi__factory.abi, + // wallet + // ); + + // const deploy = await factory.deployContractLoader(); + // const { contract } = await deploy.waitForResult(); + // const call2 = await contract.functions.return_var2().call(); + // const { value: var2 } = await call2.waitForResult(); + // expect(var2).toEqual(20); + + // lARGE CONTRACT WITH LOADER const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); - const deploy = await factory.deployContractLoader(options); + const deploy = await factory.deployContractLoader(); const { contract } = await deploy.waitForResult(); expect(contract.id).toBeDefined(); + console.log('contract', contract.id); + const call = await contract.functions.something().call(); const { value } = await call.waitForResult(); expect(value).toBe(1001); }, - { timeout: 60000 } + { timeout: 15000 } ); }); diff --git a/packages/utils/src/utils/defaultSnapshotConfigs.ts b/packages/utils/src/utils/defaultSnapshotConfigs.ts index 5d362e1b46d..9f26ed117f2 100644 --- a/packages/utils/src/utils/defaultSnapshotConfigs.ts +++ b/packages/utils/src/utils/defaultSnapshotConfigs.ts @@ -4,6 +4,7 @@ import stateConfigJson from './defaultSnapshots/stateConfig.json'; import type { SnapshotConfigs } from './types'; export const defaultSnapshotConfigs: SnapshotConfigs = { + // @ts-expect-error Getting type errors as we can't gen the fuel schema for v4 gas costs chainConfig: chainConfigJson, metadata: metadataJson, stateConfig: stateConfigJson, diff --git a/packages/utils/src/utils/defaultSnapshots/chainConfig.json b/packages/utils/src/utils/defaultSnapshots/chainConfig.json index 0591f78bad8..13d9a1adc5f 100644 --- a/packages/utils/src/utils/defaultSnapshots/chainConfig.json +++ b/packages/utils/src/utils/defaultSnapshots/chainConfig.json @@ -40,51 +40,47 @@ }, "chain_id": 0, "gas_costs": { - "V1": { + "V4": { "add": 2, "addi": 2, - "aloc": 2, "and": 2, "andi": 2, - "bal": 86, + "bal": 29, "bhei": 2, "bhsh": 2, - "burn": 25770, + "burn": 19976, "cb": 2, - "cfei": 2, "cfsi": 2, "div": 2, "divi": 2, - "eck1": 3114, - "ecr1": 42270, - "ed19": 2878, + "eck1": 1907, + "ecr1": 26135, "eq": 2, "exp": 2, "expi": 2, - "flag": 1, + "flag": 2, "gm": 2, "gt": 2, - "gtf": 12, + "gtf": 13, "ji": 2, "jmp": 2, "jne": 2, "jnei": 2, "jnzi": 2, - "jmpf": 1, - "jmpb": 1, - "jnzf": 1, - "jnzb": 1, - "jnef": 1, - "jneb": 1, + "jmpf": 2, + "jmpb": 2, + "jnzf": 2, + "jnzb": 2, + "jnef": 2, + "jneb": 2, "lb": 2, - "log": 165, + "log": 102, "lt": 2, "lw": 2, - "mint": 29024, + "mint": 18042, "mlog": 2, - "mod_op": 2, "modi": 2, - "move_op": 2, + "mod_op": 2, "movi": 2, "mroo": 4, "mul": 2, @@ -96,43 +92,44 @@ "ori": 2, "poph": 3, "popl": 3, - "pshh": 4, - "pshl": 4, - "ret": 134, - "rvrt": 153, + "pshh": 5, + "pshl": 5, + "move_op": 2, + "ret": 53, "sb": 2, "sll": 2, "slli": 2, "srl": 2, "srli": 2, - "srw": 209, + "srw": 177, "sub": 2, "subi": 2, "sw": 2, - "sww": 22501, - "time": 50, - "tr": 33912, - "tro": 24294, + "sww": 17302, + "time": 35, + "tr": 27852, + "tro": 19718, "wdcm": 2, - "wqcm": 3, + "wqcm": 2, "wdop": 3, "wqop": 3, "wdml": 3, - "wqml": 4, - "wddv": 5, - "wqdv": 6, - "wdmd": 10, - "wqmd": 17, - "wdam": 9, - "wqam": 11, - "wdmm": 10, - "wqmm": 10, + "wqml": 3, + "wddv": 4, + "wqdv": 5, + "wdmd": 8, + "wqmd": 12, + "wdam": 7, + "wqam": 8, + "wdmm": 8, + "wqmm": 8, "xor": 2, "xori": 2, - "alocDependentCost": { + "rvrt": 2, + "aloc": { "HeavyOperation": { "base": 2, - "gasPerUnit": 0 + "gas_per_unit": 0 } }, "bsiz": { @@ -150,13 +147,13 @@ "cfe": { "HeavyOperation": { "base": 2, - "gasPerUnit": 0 + "gas_per_unit": 0 } }, - "cfeiDependentCost": { + "cfei": { "HeavyOperation": { "base": 2, - "gasPerUnit": 0 + "gas_per_unit": 0 } }, "call": { @@ -183,7 +180,7 @@ "units_per_gas": 237 } }, - "ed19DependentCost": { + "ed19": { "LightOperation": { "base": 45, "units_per_gas": 237 From b725b29330c56e2497c113f98b1a19e8ae225c0c Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Fri, 2 Aug 2024 09:30:12 +0100 Subject: [PATCH 020/135] feat: simplify funding --- packages/contract/src/contract-factory.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index defe2f70fe9..cce93e41f23 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -252,10 +252,8 @@ export default class ContractFactory { } // Find the max chunk size (again, ensure is word aligned) - const baseAssetId = account.provider.getBaseAssetId(); - const fakeFunding: CoinQuantity[] = [{ assetId: baseAssetId, amount: bn(100_000) }]; const baseBlobRequest = this.blobTransactionRequest({ blobBytecode: randomBytes(32) }); - baseBlobRequest.fundWithFakeUtxos(fakeFunding, baseAssetId); + baseBlobRequest.fundWithFakeUtxos([], account.provider.getBaseAssetId()); const maxChunkSize = maxContractSize - baseBlobRequest.byteLength() - WORD_SIZE; const padding = maxChunkSize % WORD_SIZE; const chunkSize = padding + ((WORD_SIZE - (padding % WORD_SIZE)) % WORD_SIZE) + maxChunkSize; From e60ef9bb2f31f75a2e518716fa1f338928dfef08 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Fri, 2 Aug 2024 13:25:43 +0100 Subject: [PATCH 021/135] feat: loader contract fixes --- packages/contract/src/contract-factory.ts | 2 - packages/contract/src/loader-script.ts | 72 +++++++++--------- .../fuel-gauge/src/contract-factory.test.ts | 76 ++++++------------- 3 files changed, 60 insertions(+), 90 deletions(-) diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index cce93e41f23..fb753218fca 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -7,7 +7,6 @@ import type { TransactionRequest, TransactionResult, TransactionType, - CoinQuantity, } from '@fuel-ts/account'; import { CreateTransactionRequest, @@ -18,7 +17,6 @@ import { randomBytes } from '@fuel-ts/crypto'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; import { hash } from '@fuel-ts/hasher'; import type { BytesLike } from '@fuel-ts/interfaces'; -import { bn } from '@fuel-ts/math'; import { Contract } from '@fuel-ts/program'; import type { StorageSlot } from '@fuel-ts/transactions'; import { arrayify, isDefined, concat } from '@fuel-ts/utils'; diff --git a/packages/contract/src/loader-script.ts b/packages/contract/src/loader-script.ts index 7df48942224..9de3270bd51 100644 --- a/packages/contract/src/loader-script.ts +++ b/packages/contract/src/loader-script.ts @@ -6,51 +6,49 @@ import * as asm from '@fuels/vm-asm'; export const getLoaderInstructions = (blobIds: string[]): Uint8Array => { const { RegId, Instruction } = asm; - const instructionsPerBlob = 12; + const numberOfInstructions = 12; const numberOfBlobs = blobIds.length; - const numberOfInstructions = numberOfBlobs * instructionsPerBlob; const blobIdSize = BYTES_32; // Bytes for the Blob Ids - const blobIdBytes = () => concat(blobIds.map((b) => arrayify(b))); + const blobIdBytes = concat(blobIds.map((b) => arrayify(b))); // There are 2 main steps: // 1. Load the blob contents into memory // 2. Jump to the beginning of the memory where the blobs were loaded // After that the execution continues normally with the loaded contract reading our // prepared fn selector and jumps to the selected contract method. - return concat([ - new InstructionSet( - // 1. load the blob contents into memory - // find the start of the hardcoded blob ids, which are located after the code ends, - asm.move_(0x10, RegId.zero().to_u8()), - // 0x10 to hold the address of the current blob id - asm.addi(0x10, 0x10, numberOfInstructions * Instruction.size()), - // The contract is going to be loaded from the current value of SP onwards, save - // the location into 0x16 so we can jump into it later on - asm.move_(0x16, RegId.sp().to_u8()), - // loop counter - asm.movi(0x13, numberOfBlobs), - // LOOP starts here - // 0x11 to hold the size of the current blob - asm.bsiz(0x11, 0x10), - // push the blob contents onto the stack - asm.ldc(0x10, 0, 0x11, 1), - // move on to the next blob - asm.addi(0x10, 0x10, blobIdSize), - // decrement the loop counter - asm.subi(0x13, 0x13, 1), - // Jump backwards (3+1) instructions if the counter has not reached 0 - asm.jnzb(0x13, RegId.zero().to_u8(), 3), - // Jump into the memory where the contract is loaded - // what follows is called _jmp_mem by the sway compiler - // subtract the address contained in IS because jmp will add it back - asm.sub(0x16, 0x16, RegId.is().to_u8()), - // jmp will multiply by 4 so we need to divide to cancel that out - asm.divi(0x16, 0x16, 4), - // jump to the start of the contract we loaded - asm.jmp(0x16) - ).toBytes(), - blobIdBytes(), - ]); + const instructionBytes = new InstructionSet( + // 1. load the blob contents into memory + // find the start of the hardcoded blob ids, which are located after the code ends + asm.move_(0x10, RegId.is().to_u8()), + // 0x10 to hold the address of the current blob id + asm.addi(0x10, 0x10, numberOfInstructions * Instruction.size()), + // The contract is going to be loaded from the current value of SP onwards, save + // the location into 0x16 so we can jump into it later on + asm.move_(0x16, RegId.sp().to_u8()), + // loop counter + asm.movi(0x13, numberOfBlobs), + // LOOP starts here + // 0x11 to hold the size of the current blob + asm.bsiz(0x11, 0x10), + // push the blob contents onto the stack + asm.ldc(0x10, 0, 0x11, 1), + // move on to the next blob + asm.addi(0x10, 0x10, blobIdSize), + // decrement the loop counter + asm.subi(0x13, 0x13, 1), + // Jump backwards (3+1) instructions if the counter has not reached 0 + asm.jnzb(0x13, RegId.zero().to_u8(), 3), + // Jump into the memory where the contract is loaded + // what follows is called _jmp_mem by the sway compiler + // subtract the address contained in IS because jmp will add it back + asm.sub(0x16, 0x16, RegId.is().to_u8()), + // jmp will multiply by 4 so we need to divide to cancel that out + asm.divi(0x16, 0x16, 4), + // jump to the start of the contract we loaded + asm.jmp(0x16) + ).toBytes(); + + return concat([instructionBytes, blobIdBytes]); }; diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index a75b3a3551d..2c604878cd4 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -12,6 +12,9 @@ import { LOCAL_NETWORK_URL, Provider, assets, + arrayify, + chunkAndPadBytes, + hexlify, } from 'fuels'; import { launchTestNode } from 'fuels/test-utils'; @@ -283,55 +286,26 @@ describe('Contract Factory', () => { ); }); - it.only( - 'should deploy contracts greater than MAX_CONTRACT_SIZE via a loader contract', - async () => { - // USE TEST NODE - using launched = await launchTestNode({ - providerOptions: { - cacheUtxo: -1, - }, - }); - - const { - wallets: [wallet], - provider, - } = launched; - - // USING NODE - // const provider = await Provider.create(LOCAL_NETWORK_URL, { cacheUtxo: -1 }); - // const baseAssetId = provider.getBaseAssetId(); - // const wallet = await generateTestWallet(provider, [[100_000_000, baseAssetId]]); - - // SMALL CONTRACT WITH LOADER - // const factory = new ContractFactory( - // StorageTestContractAbiHex, - // StorageTestContractAbi__factory.abi, - // wallet - // ); - - // const deploy = await factory.deployContractLoader(); - // const { contract } = await deploy.waitForResult(); - - // const call2 = await contract.functions.return_var2().call(); - // const { value: var2 } = await call2.waitForResult(); - // expect(var2).toEqual(20); - - // lARGE CONTRACT WITH LOADER - const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); - - const deploy = await factory.deployContractLoader(); - - const { contract } = await deploy.waitForResult(); - expect(contract.id).toBeDefined(); - - console.log('contract', contract.id); - - const call = await contract.functions.something().call(); - - const { value } = await call.waitForResult(); - expect(value).toBe(1001); - }, - { timeout: 15000 } - ); + it('should deploy contracts greater than MAX_CONTRACT_SIZE via a loader contract', async () => { + using launched = await launchTestNode({ + providerOptions: { + cacheUtxo: -1, + }, + }); + + const { + wallets: [wallet], + } = launched; + const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); + + const deploy = await factory.deployContractLoader(); + + const { contract } = await deploy.waitForResult(); + expect(contract.id).toBeDefined(); + + const call = await contract.functions.something().call(); + + const { value } = await call.waitForResult(); + expect(value.toNumber()).toBe(1001); + }); }); From 22864f39f92cc321c89714250afd2fc515534e24 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Fri, 2 Aug 2024 14:43:57 +0100 Subject: [PATCH 022/135] chore: remove redundant code Co-authored-by: Peter Smith --- packages/account/src/providers/utils/gas.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/account/src/providers/utils/gas.ts b/packages/account/src/providers/utils/gas.ts index ef0bcc82fbd..dbd851d5d3d 100644 --- a/packages/account/src/providers/utils/gas.ts +++ b/packages/account/src/providers/utils/gas.ts @@ -2,7 +2,6 @@ import { bn } from '@fuel-ts/math'; import type { BN, BNInput } from '@fuel-ts/math'; import { ReceiptType, type Input } from '@fuel-ts/transactions'; import { arrayify } from '@fuel-ts/utils'; -import { blob } from 'stream/consumers'; import type { GqlDependentCost, From 7d3b0fbaba03560515dd88340b8939ac5bf38b10 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Mon, 5 Aug 2024 12:44:51 +0100 Subject: [PATCH 023/135] feat: isTransactionType helper --- packages/account/src/providers/provider.ts | 25 +++++++++-------- .../transaction-request/utils.test.ts | 27 +++++++++++++++++++ .../providers/transaction-request/utils.ts | 16 ++++++++++- 3 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 packages/account/src/providers/transaction-request/utils.test.ts diff --git a/packages/account/src/providers/provider.ts b/packages/account/src/providers/provider.ts index c178495febd..1a79485ab07 100644 --- a/packages/account/src/providers/provider.ts +++ b/packages/account/src/providers/provider.ts @@ -48,7 +48,11 @@ import type { JsonAbisFromAllCalls, ScriptTransactionRequest, } from './transaction-request'; -import { transactionRequestify } from './transaction-request'; +import { + isTransactionTypeCreate, + isTransactionTypeScript, + transactionRequestify, +} from './transaction-request'; import type { TransactionResultReceipt } from './transaction-response'; import { TransactionResponse, getDecodedLogs } from './transaction-response'; import { processGqlReceipt } from './transaction-summary/receipt'; @@ -720,7 +724,7 @@ Supported fuel-core version: ${supportedVersion}.` let abis: JsonAbisFromAllCalls | undefined; - if (transactionRequest.type === TransactionType.Script) { + if (isTransactionTypeScript(transactionRequest)) { abis = transactionRequest.abis; } @@ -816,7 +820,7 @@ Supported fuel-core version: ${supportedVersion}.` async estimateTxDependencies( transactionRequest: TransactionRequest ): Promise { - if (transactionRequest.type === TransactionType.Create) { + if (isTransactionTypeCreate(transactionRequest)) { return { receipts: [], outputVariables: 0, @@ -846,7 +850,7 @@ Supported fuel-core version: ${supportedVersion}.` const hasMissingOutputs = missingOutputVariables.length !== 0 || missingOutputContractIds.length !== 0; - if (hasMissingOutputs && transactionRequest.type === TransactionType.Script) { + if (hasMissingOutputs && isTransactionTypeScript(transactionRequest)) { outputVariables += missingOutputVariables.length; transactionRequest.addVariableOutputs(missingOutputVariables.length); missingOutputContractIds.forEach(({ contractId }) => { @@ -900,7 +904,7 @@ Supported fuel-core version: ${supportedVersion}.` // Prepare ScriptTransactionRequests and their indices allRequests.forEach((req, index) => { - if (req.type === TransactionType.Script) { + if (isTransactionTypeScript(req)) { serializedTransactionsMap.set(index, hexlify(req.toTransactionBytes())); } }); @@ -932,7 +936,7 @@ Supported fuel-core version: ${supportedVersion}.` const hasMissingOutputs = missingOutputVariables.length > 0 || missingOutputContractIds.length > 0; const request = allRequests[requestIdx]; - if (hasMissingOutputs && request.type === TransactionType.Script) { + if (hasMissingOutputs && isTransactionTypeScript(request)) { result.outputVariables += missingOutputVariables.length; request.addVariableOutputs(missingOutputVariables.length); missingOutputContractIds.forEach(({ contractId }) => { @@ -1012,7 +1016,7 @@ Supported fuel-core version: ${supportedVersion}.` let gasLimit = bn(0); // Only Script transactions consume gas - if (transactionRequest.type === TransactionType.Script) { + if (isTransactionTypeScript(transactionRequest)) { // If the gasLimit is set to 0, it means we need to estimate it. gasLimit = transactionRequest.gasLimit; if (transactionRequest.gasLimit.eq(0)) { @@ -1109,17 +1113,16 @@ Supported fuel-core version: ${supportedVersion}.` { signatureCallback }: TransactionCostParams = {} ): Promise> { const txRequestClone = clone(transactionRequestify(transactionRequestLike)); - const isScriptTransaction = txRequestClone.type === TransactionType.Script; const updateMaxFee = txRequestClone.maxFee.eq(0); // Remove gasLimit to avoid gasLimit when estimating predicates - if (isScriptTransaction) { + if (isTransactionTypeScript(txRequestClone)) { txRequestClone.gasLimit = bn(0); } const signedRequest = clone(txRequestClone) as ScriptTransactionRequest; let addedSignatures = 0; - if (signatureCallback && isScriptTransaction) { + if (signatureCallback && isTransactionTypeScript(signedRequest)) { const lengthBefore = signedRequest.witnesses.length; await signatureCallback(signedRequest); addedSignatures = signedRequest.witnesses.length - lengthBefore; @@ -1143,7 +1146,7 @@ Supported fuel-core version: ${supportedVersion}.` let gasUsed = bn(0); txRequestClone.maxFee = maxFee; - if (isScriptTransaction) { + if (isTransactionTypeScript(txRequestClone)) { txRequestClone.gasLimit = gasLimit; if (signatureCallback) { await signatureCallback(txRequestClone); diff --git a/packages/account/src/providers/transaction-request/utils.test.ts b/packages/account/src/providers/transaction-request/utils.test.ts new file mode 100644 index 00000000000..19d77ed2a55 --- /dev/null +++ b/packages/account/src/providers/transaction-request/utils.test.ts @@ -0,0 +1,27 @@ +import { CreateTransactionRequest } from './create-transaction-request'; +import { ScriptTransactionRequest } from './script-transaction-request'; +import { isTransactionTypeCreate, isTransactionTypeScript } from './utils'; + +describe('isTransactionTypeScript', () => { + it('should return true if the request is a script transaction', () => { + const request = new ScriptTransactionRequest(); + expect(isTransactionTypeScript(request)).toBe(true); + }); + + it('should return false if the request is not a script transaction', () => { + const request = new CreateTransactionRequest({}); + expect(isTransactionTypeScript(request)).toBe(false); + }); +}); + +describe('isTransactionTypeCreate', () => { + it('should return true if the request is a create transaction', () => { + const request = new CreateTransactionRequest({}); + expect(isTransactionTypeCreate(request)).toBe(true); + }); + + it('should return false if the request is not a create transaction', () => { + const request = new ScriptTransactionRequest(); + expect(isTransactionTypeCreate(request)).toBe(false); + }); +}); diff --git a/packages/account/src/providers/transaction-request/utils.ts b/packages/account/src/providers/transaction-request/utils.ts index 50c279540ff..2a17a851be2 100644 --- a/packages/account/src/providers/transaction-request/utils.ts +++ b/packages/account/src/providers/transaction-request/utils.ts @@ -8,7 +8,11 @@ import type { TransactionRequestLike, TransactionRequest } from './types'; /** @hidden */ export const transactionRequestify = (obj: TransactionRequestLike): TransactionRequest => { - if (obj instanceof ScriptTransactionRequest || obj instanceof CreateTransactionRequest) { + if ( + obj instanceof ScriptTransactionRequest || + obj instanceof CreateTransactionRequest || + obj instanceof BlobTransactionRequest + ) { return obj; } @@ -32,3 +36,13 @@ export const transactionRequestify = (obj: TransactionRequestLike): TransactionR } } }; + +/** @hidden */ +export const isTransactionTypeScript = ( + request: TransactionRequestLike +): request is ScriptTransactionRequest => request.type === TransactionType.Script; + +/** @hidden */ +export const isTransactionTypeCreate = ( + request: TransactionRequestLike +): request is CreateTransactionRequest => request.type === TransactionType.Create; From 3bafa2bfce2f804c6b50118fb95422b9076be1fd Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Mon, 5 Aug 2024 13:42:15 +0100 Subject: [PATCH 024/135] chore: doc blocks and pr refactors --- packages/contract/src/contract-factory.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index fb753218fca..fce0217c21f 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -157,7 +157,13 @@ export default class ContractFactory { }; } - blobTransactionRequest(options: { blobBytecode: Uint8Array } & DeployContractOptions) { + /** + * Create a blob transaction request, used for deploying contract chunks. + * + * @param options - options for creating a blob transaction request. + * @returns a populated BlobTransactionRequest. + */ + private blobTransactionRequest(options: { blobBytecode: Uint8Array } & DeployContractOptions) { const { blobBytecode } = options; return new BlobTransactionRequest({ blobId: hash(blobBytecode), @@ -167,7 +173,17 @@ export default class ContractFactory { }); } - async fundTransactionRequest(request: TransactionRequest, options: DeployContractOptions = {}) { + /** + * Takes a transaction request, estimates it and funds it. + * + * @param request - the request to fund. + * @param options - options for funding the request. + * @returns a funded transaction request. + */ + private async fundTransactionRequest( + request: TransactionRequest, + options: DeployContractOptions = {} + ) { const account = this.getAccount(); const { maxFee: setMaxFee } = options; @@ -224,7 +240,7 @@ export default class ContractFactory { } /** - * Deploy a contract with the specified options. + * Chunks and deploys a contract via a loader contract. Suitable for deploying contracts larger than the max contract size. * * @param deployContractOptions - Options for deploying the contract. * @returns A promise that resolves to the deployed contract instance. From 7c63591dc342d0fc24bd6aea099385ce14d37b09 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Mon, 5 Aug 2024 13:42:32 +0100 Subject: [PATCH 025/135] chore: remove math from contract --- packages/contract/package.json | 1 - pnpm-lock.yaml | 4 ---- 2 files changed, 5 deletions(-) diff --git a/packages/contract/package.json b/packages/contract/package.json index 7173501572d..546de9abc72 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -45,7 +45,6 @@ "@fuel-ts/errors": "workspace:*", "@fuel-ts/hasher": "workspace:*", "@fuel-ts/interfaces": "workspace:^", - "@fuel-ts/math": "workspace:^", "@fuel-ts/merkle": "workspace:*", "@fuel-ts/program": "workspace:*", "@fuel-ts/transactions": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72b2dc22f3d..23c9950b5db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -795,9 +795,6 @@ importers: '@fuel-ts/interfaces': specifier: workspace:^ version: link:../interfaces - '@fuel-ts/math': - specifier: workspace:^ - version: link:../math '@fuel-ts/merkle': specifier: workspace:* version: link:../merkle @@ -6549,7 +6546,6 @@ packages: bun@1.1.21: resolution: {integrity: sha512-mvqYEvafGskIVTjlftbKvsXtyR6z/SQnhJsVw0xCU46pc56oX1sAGvaemWKOy/sy/gGMHcgLE0KUidDQQzqXWQ==} - cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true From 0a97c56f3cbed9f407a0ae4d56d975fb3a09e817 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Tue, 6 Aug 2024 10:18:13 +0100 Subject: [PATCH 026/135] feat: use fuel-core release --- internal/fuel-core/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/fuel-core/VERSION b/internal/fuel-core/VERSION index 6256560ecdb..1b8605b913d 100644 --- a/internal/fuel-core/VERSION +++ b/internal/fuel-core/VERSION @@ -1 +1 @@ -git:dento/blob-tx \ No newline at end of file +git:release/0.32.0 \ No newline at end of file From ebd92fc1a343a265547e5f225f671b05f0dd0ff6 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Tue, 6 Aug 2024 10:18:48 +0100 Subject: [PATCH 027/135] chore: isTransactionType cleanups --- packages/account/src/providers/provider.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/account/src/providers/provider.ts b/packages/account/src/providers/provider.ts index 1a79485ab07..058657664ec 100644 --- a/packages/account/src/providers/provider.ts +++ b/packages/account/src/providers/provider.ts @@ -1114,13 +1114,14 @@ Supported fuel-core version: ${supportedVersion}.` ): Promise> { const txRequestClone = clone(transactionRequestify(transactionRequestLike)); const updateMaxFee = txRequestClone.maxFee.eq(0); + const isScriptTransaction = isTransactionTypeScript(txRequestClone); // Remove gasLimit to avoid gasLimit when estimating predicates - if (isTransactionTypeScript(txRequestClone)) { + if (isScriptTransaction) { txRequestClone.gasLimit = bn(0); } - const signedRequest = clone(txRequestClone) as ScriptTransactionRequest; + const signedRequest = clone(txRequestClone); let addedSignatures = 0; if (signatureCallback && isTransactionTypeScript(signedRequest)) { const lengthBefore = signedRequest.witnesses.length; @@ -1146,7 +1147,7 @@ Supported fuel-core version: ${supportedVersion}.` let gasUsed = bn(0); txRequestClone.maxFee = maxFee; - if (isTransactionTypeScript(txRequestClone)) { + if (isScriptTransaction) { txRequestClone.gasLimit = gasLimit; if (signatureCallback) { await signatureCallback(txRequestClone); From 7ecfc8cc4c20937c3072283a7a3f19f9b77a0e15 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Tue, 6 Aug 2024 11:07:07 +0100 Subject: [PATCH 028/135] feat: use regex for response id check --- .../src/providers/transaction-response/transaction-response.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account/src/providers/transaction-response/transaction-response.ts b/packages/account/src/providers/transaction-response/transaction-response.ts index 4ff88b0a7ab..3e88e065513 100644 --- a/packages/account/src/providers/transaction-response/transaction-response.ts +++ b/packages/account/src/providers/transaction-response/transaction-response.ts @@ -228,7 +228,7 @@ export class TransactionResponse { for await (const { statusChange } of subscription) { if (statusChange.type === 'SqueezedOutStatus') { - if (/BlobIdAlreadyUploaded/i.exec(statusChange.reason)) { + if (statusChange.reason.indexOf('BlobIdAlreadyUploaded') > -1) { throw new FuelError( ErrorCode.BLOB_ID_ALREADY_UPLOADED, `Blob ID already uploaded and is available for reuse.` From bb24c4161db4eac09d785e3dc67985d063bed07f Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Tue, 6 Aug 2024 12:20:21 +0100 Subject: [PATCH 029/135] chore: cleanups --- .../blob-transaction-request.ts | 6 + packages/contract/src/contract-factory.ts | 117 +++++++----------- packages/contract/src/loader/index.ts | 3 + .../contract/src/loader/loader-script.test.ts | 22 ++++ .../src/{ => loader}/loader-script.ts | 0 packages/contract/src/loader/types.ts | 6 + packages/contract/src/loader/utils.test.ts | 30 +++++ packages/contract/src/loader/utils.ts | 23 ++++ .../fuel-gauge/src/contract-factory.test.ts | 2 +- 9 files changed, 133 insertions(+), 76 deletions(-) create mode 100644 packages/contract/src/loader/index.ts create mode 100644 packages/contract/src/loader/loader-script.test.ts rename packages/contract/src/{ => loader}/loader-script.ts (100%) create mode 100644 packages/contract/src/loader/types.ts create mode 100644 packages/contract/src/loader/utils.test.ts create mode 100644 packages/contract/src/loader/utils.ts diff --git a/packages/account/src/providers/transaction-request/blob-transaction-request.ts b/packages/account/src/providers/transaction-request/blob-transaction-request.ts index 24d7b2ccfa7..4008a8766bf 100644 --- a/packages/account/src/providers/transaction-request/blob-transaction-request.ts +++ b/packages/account/src/providers/transaction-request/blob-transaction-request.ts @@ -68,6 +68,12 @@ export class BlobTransactionRequest extends BaseTransactionRequest { return hashTransaction(this, chainId); } + /** + * Calculates the metadata gas cost for a blob transaction. + * + * @param gasCosts - gas costs passed from the chain. + * @returns metadata gas cost for the blob transaction. + */ metadataGas(gasCosts: GasCosts): BN { return calculateMetadataGasForTxBlob({ gasCosts, diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index fce0217c21f..b1aed295d55 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -19,9 +19,9 @@ import { hash } from '@fuel-ts/hasher'; import type { BytesLike } from '@fuel-ts/interfaces'; import { Contract } from '@fuel-ts/program'; import type { StorageSlot } from '@fuel-ts/transactions'; -import { arrayify, isDefined, concat } from '@fuel-ts/utils'; +import { arrayify, isDefined } from '@fuel-ts/utils'; -import { getLoaderInstructions } from './loader-script'; +import { getLoaderInstructions, getContractChunks } from './loader'; import { getContractId, getContractStorageRoot, hexlifyWithPrefix } from './util'; /** @@ -43,13 +43,6 @@ export type DeployContractResult = { }>; }; -export type ContractChunk = { - id: number; - size: number; - bytecode: Uint8Array; - blobId?: string; -}; - /** * `ContractFactory` provides utilities for deploying and configuring contracts. */ @@ -157,22 +150,6 @@ export default class ContractFactory { }; } - /** - * Create a blob transaction request, used for deploying contract chunks. - * - * @param options - options for creating a blob transaction request. - * @returns a populated BlobTransactionRequest. - */ - private blobTransactionRequest(options: { blobBytecode: Uint8Array } & DeployContractOptions) { - const { blobBytecode } = options; - return new BlobTransactionRequest({ - blobId: hash(blobBytecode), - witnessIndex: 0, - witnesses: [blobBytecode], - ...options, - }); - } - /** * Takes a transaction request, estimates it and funds it. * @@ -245,64 +222,31 @@ export default class ContractFactory { * @param deployContractOptions - Options for deploying the contract. * @returns A promise that resolves to the deployed contract instance. */ - async deployContractLoader( + async deployContractAsBlobs( deployContractOptions: DeployContractOptions = {} ): Promise> { const account = this.getAccount(); - const { consensusParameters } = account.provider.getChain(); - const maxContractSize = consensusParameters.contractParameters.contractMaxSize.toNumber(); - - // Ensure the max contract size is byte aligned before chunking - if (maxContractSize % WORD_SIZE !== 0) { - throw new FuelError( - ErrorCode.CONTRACT_SIZE_EXCEEDS_LIMIT, // Todo: change error - `Max contract size of ${maxContractSize} bytes is not byte aligned.` - ); - } - const { configurableConstants } = deployContractOptions; if (configurableConstants) { this.setConfigurableConstants(configurableConstants); } - // Find the max chunk size (again, ensure is word aligned) - const baseBlobRequest = this.blobTransactionRequest({ blobBytecode: randomBytes(32) }); - baseBlobRequest.fundWithFakeUtxos([], account.provider.getBaseAssetId()); - const maxChunkSize = maxContractSize - baseBlobRequest.byteLength() - WORD_SIZE; - const padding = maxChunkSize % WORD_SIZE; - const chunkSize = padding + ((WORD_SIZE - (padding % WORD_SIZE)) % WORD_SIZE) + maxChunkSize; - - // Chunk the bytecode - const chunks: ContractChunk[] = []; - for (let offset = 0, index = 0; offset < this.bytecode.length; offset += chunkSize, index++) { - const chunk = this.bytecode.slice(offset, offset + chunkSize); - - // Chunks must be byte aligned - if (chunk.length % WORD_SIZE !== 0) { - const paddingLength = chunk.length % WORD_SIZE; - const paddedChunk = concat([chunk, new Uint8Array(paddingLength)]); - chunks.push({ id: index, size: paddedChunk.length, bytecode: paddedChunk }); - // eslint-disable-next-line no-continue - continue; - } - chunks.push({ id: index, size: chunk.length, bytecode: chunk }); - } - - const uploadedBlobIds: string[] = []; + const chunkSize = this.getMaxChunkSize(); + const chunks = getContractChunks(this.bytecode, chunkSize); // Deploy the chunks as blob txs for (const { id, bytecode } of chunks) { const blobRequest = this.blobTransactionRequest({ - blobBytecode: bytecode, + bytecode, ...deployContractOptions, }); - - // Store the blobIds for the loader contract + // Store the already uploaded blobs and blobIds for the loader contract + const uploadedBlobs = chunks.map((c) => c.blobId).filter((c) => c); const { blobId } = blobRequest; chunks[id].blobId = blobId; // Upload the blob if it hasn't been uploaded yet. Duplicate blob IDs will fail gracefully. - if (!uploadedBlobIds.includes(blobId)) { + if (!uploadedBlobs.includes(blobId)) { const fundedBlobRequest = await this.fundTransactionRequest( blobRequest, deployContractOptions @@ -317,8 +261,6 @@ export default class ContractFactory { // Core will throw for blobs that have already been uploaded, but the blobId // is still valid so we can use this for the loader contract if ((err).code === ErrorCode.BLOB_ID_ALREADY_UPLOADED) { - // TODO: We need to unset the cached utxo as it can be reused - // this.account?.provider.cache?.del(UTXO_ID); // eslint-disable-next-line no-continue continue; } @@ -329,8 +271,6 @@ export default class ContractFactory { if (!result.status || result.status !== TransactionStatus.success) { throw new FuelError(ErrorCode.TRANSACTION_FAILED, 'Failed to deploy contract chunk'); } - - uploadedBlobIds.push(blobId); } } @@ -343,12 +283,8 @@ export default class ContractFactory { bytecode: loaderBytecode, ...deployContractOptions, }); - const fundedCreateRequest = await this.fundTransactionRequest( - createRequest, - deployContractOptions - ); - - const transactionResponse = await account.sendTransaction(fundedCreateRequest); + await this.fundTransactionRequest(createRequest, deployContractOptions); + const transactionResponse = await account.sendTransaction(createRequest); const waitForResult = async () => { const transactionResult = await transactionResponse.waitForResult(); @@ -419,4 +355,35 @@ export default class ContractFactory { transactionRequest, }; } + + /** + * Create a blob transaction request, used for deploying contract chunks. + * + * @param options - options for creating a blob transaction request. + * @returns a populated BlobTransactionRequest. + */ + private blobTransactionRequest(options: { bytecode: Uint8Array } & DeployContractOptions) { + const { bytecode } = options; + return new BlobTransactionRequest({ + blobId: hash(bytecode), + witnessIndex: 0, + witnesses: [bytecode], + ...options, + }); + } + + /** + * Get the maximum chunk size for deploying a contract by chunks. + */ + private getMaxChunkSize() { + const { provider } = this.getAccount(); + const { consensusParameters } = provider.getChain(); + const contractSizeLimit = consensusParameters.contractParameters.contractMaxSize.toNumber(); + // Get the base tx length + const blobTx = this.blobTransactionRequest({ bytecode: randomBytes(32) }); + blobTx.fundWithFakeUtxos([], provider.getBaseAssetId()); + const maxChunkSize = contractSizeLimit - blobTx.byteLength() - WORD_SIZE; + // Ensure chunksize is byte aligned + return Math.round(maxChunkSize / WORD_SIZE) * WORD_SIZE; + } } diff --git a/packages/contract/src/loader/index.ts b/packages/contract/src/loader/index.ts new file mode 100644 index 00000000000..6dee5f1765d --- /dev/null +++ b/packages/contract/src/loader/index.ts @@ -0,0 +1,3 @@ +export * from './loader-script'; +export * from './types'; +export * from './utils'; diff --git a/packages/contract/src/loader/loader-script.test.ts b/packages/contract/src/loader/loader-script.test.ts new file mode 100644 index 00000000000..a855746a1be --- /dev/null +++ b/packages/contract/src/loader/loader-script.test.ts @@ -0,0 +1,22 @@ +import { WORD_SIZE } from '@fuel-ts/abi-coder'; +import { arrayify } from '@fuel-ts/utils'; + +import { getLoaderInstructions } from './loader-script'; + +describe('Loader Script', () => { + it('generates loader bytecode for a list of blob ids', () => { + const blobIds = ['0x01', '0x02', '0x03']; + const actual = getLoaderInstructions(blobIds); + + const expected = new Uint8Array([ + 26, 64, 192, 0, 80, 65, 0, 48, 26, 88, 80, 0, 114, 76, 0, 3, 186, 69, 0, 0, 50, 64, 4, 65, 80, + 65, 0, 32, 89, 77, 48, 1, 119, 76, 0, 3, 32, 89, 99, 0, 82, 89, 96, 4, 74, 88, 0, 0, 1, 2, 3, + ]); + expect(actual).toStrictEqual(expected); + + // Checking loader bytes remains word sized + const blobIdBytes = blobIds.map((b) => arrayify(b)); + const loaderBytes = actual.slice(0, actual.length - blobIdBytes.length); + expect(loaderBytes.length % WORD_SIZE !== 0).toBe(false); + }); +}); diff --git a/packages/contract/src/loader-script.ts b/packages/contract/src/loader/loader-script.ts similarity index 100% rename from packages/contract/src/loader-script.ts rename to packages/contract/src/loader/loader-script.ts diff --git a/packages/contract/src/loader/types.ts b/packages/contract/src/loader/types.ts new file mode 100644 index 00000000000..b0a587d78e4 --- /dev/null +++ b/packages/contract/src/loader/types.ts @@ -0,0 +1,6 @@ +export type ContractChunk = { + id: number; + size: number; + bytecode: Uint8Array; + blobId?: string; +}; diff --git a/packages/contract/src/loader/utils.test.ts b/packages/contract/src/loader/utils.test.ts new file mode 100644 index 00000000000..1cb5ef129e4 --- /dev/null +++ b/packages/contract/src/loader/utils.test.ts @@ -0,0 +1,30 @@ +import { WORD_SIZE } from '@fuel-ts/abi-coder'; + +import { getContractChunks } from './utils'; + +describe('getContractChunks', () => { + it('splits bytecode into chunks', () => { + const bytecode = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); + const chunkSize = WORD_SIZE; + const actual = getContractChunks(bytecode, chunkSize); + + const expected = [ + { id: 0, size: 8, bytecode: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]) }, + { id: 1, size: 8, bytecode: new Uint8Array([9, 10, 11, 12, 13, 14, 15, 16]) }, + ]; + expect(actual).toStrictEqual(expected); + }); + + it('pads chunks with zero bytes', () => { + const bytecode = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]); + const chunkSize = WORD_SIZE; + const actual = getContractChunks(bytecode, chunkSize); + + const expected = [ + { id: 0, size: 8, bytecode: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]) }, + { id: 1, size: 8, bytecode: new Uint8Array([9, 0, 0, 0, 0, 0, 0, 0]) }, + ]; + + expect(actual).toStrictEqual(expected); + }); +}); diff --git a/packages/contract/src/loader/utils.ts b/packages/contract/src/loader/utils.ts new file mode 100644 index 00000000000..b5b8cea23f4 --- /dev/null +++ b/packages/contract/src/loader/utils.ts @@ -0,0 +1,23 @@ +import { WORD_SIZE } from '@fuel-ts/abi-coder'; +import { concat } from '@fuel-ts/utils'; + +import type { ContractChunk } from './types'; + +export const getContractChunks = (bytecode: Uint8Array, chunkSize: number): ContractChunk[] => { + const chunks: ContractChunk[] = []; + + for (let offset = 0, index = 0; offset < bytecode.length; offset += chunkSize, index++) { + const chunk = bytecode.slice(offset, offset + chunkSize); + + // Align chunks by word size + if (chunk.length % WORD_SIZE !== 0) { + const paddedChunk = concat([chunk, new Uint8Array(chunkSize - chunk.length)]); + chunks.push({ id: index, size: paddedChunk.length, bytecode: paddedChunk }); + // eslint-disable-next-line no-continue + continue; + } + chunks.push({ id: index, size: chunk.length, bytecode: chunk }); + } + + return chunks; +}; diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index 2c604878cd4..f9772c049a5 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -298,7 +298,7 @@ describe('Contract Factory', () => { } = launched; const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); - const deploy = await factory.deployContractLoader(); + const deploy = await factory.deployContractAsBlobs(); const { contract } = await deploy.waitForResult(); expect(contract.id).toBeDefined(); From 96b6e0d9802cc4989448fd58907998166d6f3a1f Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Tue, 6 Aug 2024 14:11:33 +0100 Subject: [PATCH 030/135] feat: chunk size tolerance --- packages/contract/src/contract-factory.ts | 46 +++++++++++-- packages/errors/src/error-codes.ts | 1 + .../fuel-gauge/src/contract-factory.test.ts | 65 ++++++++++++++----- 3 files changed, 90 insertions(+), 22 deletions(-) diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index b1aed295d55..e7aa8b44985 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -24,6 +24,9 @@ import { arrayify, isDefined } from '@fuel-ts/utils'; import { getLoaderInstructions, getContractChunks } from './loader'; import { getContractId, getContractStorageRoot, hexlifyWithPrefix } from './util'; +/** Amount of tolerance for chunk sizes in blob transactions */ +export const CHUNK_SIZE_TOLERANCE = 0.05; + /** * Options for deploying a contract. */ @@ -32,6 +35,7 @@ export type DeployContractOptions = { storageSlots?: StorageSlot[]; stateRoot?: BytesLike; configurableConstants?: { [name: string]: unknown }; + chunkSizeTolerance?: number; } & CreateTransactionRequestLike; export type DeployContractResult = { @@ -182,6 +186,25 @@ export default class ContractFactory { return request; } + /** + * Deploy a contract of any length with the specified options. + * + * @param deployContractOptions - Options for deploying the contract. + * @returns A promise that resolves to the deployed contract instance. + */ + async deploy( + deployContractOptions: DeployContractOptions = {} + ): Promise> { + const account = this.getAccount(); + const { consensusParameters } = account.provider.getChain(); + const maxContractSize = consensusParameters.contractParameters.contractMaxSize.toNumber(); + + if (this.bytecode.length > maxContractSize) { + return this.deployContractAsBlobs(deployContractOptions); + } + return this.deployContract(deployContractOptions); + } + /** * Deploy a contract with the specified options. * @@ -223,15 +246,17 @@ export default class ContractFactory { * @returns A promise that resolves to the deployed contract instance. */ async deployContractAsBlobs( - deployContractOptions: DeployContractOptions = {} + deployContractOptions: DeployContractOptions = { + chunkSizeTolerance: 0.05, + } ): Promise> { const account = this.getAccount(); - const { configurableConstants } = deployContractOptions; + const { configurableConstants, chunkSizeTolerance } = deployContractOptions; if (configurableConstants) { this.setConfigurableConstants(configurableConstants); } - const chunkSize = this.getMaxChunkSize(); + const chunkSize = this.getMaxChunkSize(chunkSizeTolerance); const chunks = getContractChunks(this.bytecode, chunkSize); // Deploy the chunks as blob txs @@ -375,14 +400,25 @@ export default class ContractFactory { /** * Get the maximum chunk size for deploying a contract by chunks. */ - private getMaxChunkSize() { + private getMaxChunkSize(chunkSizeTolerance: number = CHUNK_SIZE_TOLERANCE) { + if (chunkSizeTolerance < 0 || chunkSizeTolerance > 1) { + throw new FuelError( + ErrorCode.INVALID_CHUNK_SIZE_TOLERANCE, + 'Chunk size tolerance must be between 0 and 1' + ); + } + const { provider } = this.getAccount(); const { consensusParameters } = provider.getChain(); const contractSizeLimit = consensusParameters.contractParameters.contractMaxSize.toNumber(); // Get the base tx length const blobTx = this.blobTransactionRequest({ bytecode: randomBytes(32) }); blobTx.fundWithFakeUtxos([], provider.getBaseAssetId()); - const maxChunkSize = contractSizeLimit - blobTx.byteLength() - WORD_SIZE; + // Allow tolerance for fluctuating fees / inputs / outputs + const toleranceMultiplier = 1 - chunkSizeTolerance; + const maxChunkSize = + (contractSizeLimit - blobTx.byteLength() - WORD_SIZE) * toleranceMultiplier; + // Ensure chunksize is byte aligned return Math.round(maxChunkSize / WORD_SIZE) * WORD_SIZE; } diff --git a/packages/errors/src/error-codes.ts b/packages/errors/src/error-codes.ts index 7ec0fc3c46f..28343ee0893 100644 --- a/packages/errors/src/error-codes.ts +++ b/packages/errors/src/error-codes.ts @@ -74,6 +74,7 @@ export enum ErrorCode { TRANSACTION_SQUEEZED_OUT = 'transaction-squeezed-out', CONTRACT_SIZE_EXCEEDS_LIMIT = 'contract-size-exceeds-limit', BLOB_ID_ALREADY_UPLOADED = 'blob-id-already-uploaded', + INVALID_CHUNK_SIZE_TOLERANCE = 'invalid-chunk-size-tolerance', // receipt INVALID_RECEIPT_TYPE = 'invalid-receipt-type', diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index f9772c049a5..43db7d381df 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -1,21 +1,7 @@ import type { Account, TransactionResult } from '@fuel-ts/account'; -import { generateTestWallet } from '@fuel-ts/account/test-utils'; import { FuelError, ErrorCode } from '@fuel-ts/errors'; import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; -import type { DeployContractOptions } from 'fuels'; -import { - BN, - bn, - toHex, - Interface, - ContractFactory, - LOCAL_NETWORK_URL, - Provider, - assets, - arrayify, - chunkAndPadBytes, - hexlify, -} from 'fuels'; +import { BN, bn, toHex, Interface, ContractFactory, arrayify, concat } from 'fuels'; import { launchTestNode } from 'fuels/test-utils'; import type { LargeContractAbi } from '../test/typegen/contracts'; @@ -269,7 +255,7 @@ describe('Contract Factory', () => { ); }); - it('should not deploy contracts greater than MAX_CONTRACT_SIZE via deploy contract', async () => { + it('should not deploy large contracts via create', async () => { using launched = await launchTestNode(); const { wallets: [wallet], @@ -286,7 +272,7 @@ describe('Contract Factory', () => { ); }); - it('should deploy contracts greater than MAX_CONTRACT_SIZE via a loader contract', async () => { + it('deploys large contracts via blobs [byte aligned]', async () => { using launched = await launchTestNode({ providerOptions: { cacheUtxo: -1, @@ -297,7 +283,33 @@ describe('Contract Factory', () => { wallets: [wallet], } = launched; const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); + expect(factory.bytecode.length % 8 === 0).toBe(true); + + const deploy = await factory.deployContractAsBlobs(); + + const { contract } = await deploy.waitForResult(); + expect(contract.id).toBeDefined(); + + const call = await contract.functions.something().call(); + + const { value } = await call.waitForResult(); + expect(value.toNumber()).toBe(1001); + }); + + it('deploys large contracts via blobs [padded]', async () => { + using launched = await launchTestNode({ + providerOptions: { + cacheUtxo: -1, + }, + }); + const { + wallets: [wallet], + } = launched; + + const bytecode = concat([arrayify(largeContractHex), new Uint8Array(3)]); + const factory = new ContractFactory(bytecode, LargeContractAbi__factory.abi, wallet); + expect(bytecode.length % 8 === 0).toBe(false); const deploy = await factory.deployContractAsBlobs(); const { contract } = await deploy.waitForResult(); @@ -308,4 +320,23 @@ describe('Contract Factory', () => { const { value } = await call.waitForResult(); expect(value.toNumber()).toBe(1001); }); + + it('should not deploy large contracts via blobs [invalid chunk size tolerance]', async () => { + using launched = await launchTestNode(); + + const { + wallets: [wallet], + } = launched; + + const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); + const chunkSizeTolerance = 2; + + await expectToThrowFuelError( + () => factory.deployContractAsBlobs({ chunkSizeTolerance }), + new FuelError( + ErrorCode.INVALID_CHUNK_SIZE_TOLERANCE, + 'Chunk size tolerance must be between 0 and 1' + ) + ); + }); }); From 0ce01e268d379bb3ce167cd0dbeb90bf316a6ef8 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Tue, 6 Aug 2024 14:26:59 +0100 Subject: [PATCH 031/135] test: deploy test cases --- .../fuel-gauge/src/contract-factory.test.ts | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index 43db7d381df..f8eefd48fd5 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -8,8 +8,11 @@ import type { LargeContractAbi } from '../test/typegen/contracts'; import { StorageTestContractAbi__factory, LargeContractAbi__factory, + ConfigurableContractAbi__factory, } from '../test/typegen/contracts'; +import ConfigurableContractAbiHex from '../test/typegen/contracts/ConfigurableContractAbi.hex'; import largeContractHex from '../test/typegen/contracts/LargeContractAbi.hex'; +import LargeContractAbiHex from '../test/typegen/contracts/LargeContractAbi.hex'; import StorageTestContractAbiHex from '../test/typegen/contracts/StorageTestContractAbi.hex'; import { launchTestContract } from './utils'; @@ -288,7 +291,6 @@ describe('Contract Factory', () => { const deploy = await factory.deployContractAsBlobs(); const { contract } = await deploy.waitForResult(); - expect(contract.id).toBeDefined(); const call = await contract.functions.something().call(); @@ -339,4 +341,63 @@ describe('Contract Factory', () => { ) ); }); + + it('deploys a small contract via blobs', async () => { + using launched = await launchTestNode(); + + const { + wallets: [wallet], + } = launched; + + const factory = new ContractFactory( + ConfigurableContractAbiHex, + ConfigurableContractAbi__factory.abi, + wallet + ); + + const deploy = await factory.deployContractAsBlobs(); + const { contract } = await deploy.waitForResult(); + + const call = await contract.functions.echo_u8().call(); + const { value } = await call.waitForResult(); + expect(value).toBe(10); + }); + + it('deploys a small contract via deploy entrypoint', async () => { + using launched = await launchTestNode(); + + const { + wallets: [wallet], + } = launched; + + const factory = new ContractFactory( + ConfigurableContractAbiHex, + ConfigurableContractAbi__factory.abi, + wallet + ); + + const deploy = await factory.deploy(); + const { contract } = await deploy.waitForResult(); + + const call = await contract.functions.echo_u8().call(); + const { value } = await call.waitForResult(); + expect(value).toBe(10); + }); + + it('deploys a large contract via deploy entrypoint', async () => { + using launched = await launchTestNode(); + + const { + wallets: [wallet], + } = launched; + + const factory = new ContractFactory(LargeContractAbiHex, LargeContractAbi__factory.abi, wallet); + + const deploy = await factory.deploy(); + const { contract } = await deploy.waitForResult(); + + const call = await contract.functions.something().call(); + const { value } = await call.waitForResult(); + expect(value.toNumber()).toBe(1001); + }); }); From 57adf3d625a69f6d741c9709738b2ca390d83055 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Tue, 6 Aug 2024 14:27:16 +0100 Subject: [PATCH 032/135] feat: add ed19 dependent cost to chain --- packages/account/test/fixtures/chain.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/account/test/fixtures/chain.ts b/packages/account/test/fixtures/chain.ts index 19ba34ae30f..d76c1d41004 100644 --- a/packages/account/test/fixtures/chain.ts +++ b/packages/account/test/fixtures/chain.ts @@ -174,6 +174,11 @@ export const MOCK_CHAIN: GqlChainInfoFragment = { base: '17', unitsPerGas: '790', }, + ed19DependentCost: { + type: 'HeavyOperation', + base: '2', + gasPerUnit: '0', + }, k256: { type: 'LightOperation', base: '11', From d2ebf947276fa9fd0f20f3e36a52030fd94c48b9 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Tue, 6 Aug 2024 14:56:48 +0100 Subject: [PATCH 033/135] feat: getBytecodeSize --- packages/contract/src/contract-factory.ts | 10 ++++++++++ packages/fuel-gauge/src/contract-factory.test.ts | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index e7aa8b44985..eec6281eaaa 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -357,6 +357,16 @@ export default class ContractFactory { } } + /** + * Helper function to get the size of the contract's bytecode. Useful for informing + * which deployment method to use. + * + * @returns The size of the contract's bytecode. + */ + getBytecodeSize(): number { + return this.bytecode.length; + } + private getAccount(): Account { if (!this.account) { throw new FuelError(ErrorCode.ACCOUNT_REQUIRED, 'Account not assigned to contract.'); diff --git a/packages/fuel-gauge/src/contract-factory.test.ts b/packages/fuel-gauge/src/contract-factory.test.ts index f8eefd48fd5..a1fc24b5f7f 100644 --- a/packages/fuel-gauge/src/contract-factory.test.ts +++ b/packages/fuel-gauge/src/contract-factory.test.ts @@ -286,7 +286,7 @@ describe('Contract Factory', () => { wallets: [wallet], } = launched; const factory = new ContractFactory(largeContractHex, LargeContractAbi__factory.abi, wallet); - expect(factory.bytecode.length % 8 === 0).toBe(true); + expect(factory.getBytecodeSize() % 8 === 0).toBe(true); const deploy = await factory.deployContractAsBlobs(); @@ -311,7 +311,7 @@ describe('Contract Factory', () => { const bytecode = concat([arrayify(largeContractHex), new Uint8Array(3)]); const factory = new ContractFactory(bytecode, LargeContractAbi__factory.abi, wallet); - expect(bytecode.length % 8 === 0).toBe(false); + expect(factory.getBytecodeSize() % 8 === 0).toBe(false); const deploy = await factory.deployContractAsBlobs(); const { contract } = await deploy.waitForResult(); From dda9dc9220b3f9ebc53a19cc0d466831f742edfb Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Tue, 6 Aug 2024 16:30:04 +0100 Subject: [PATCH 034/135] docs: documentation for deploy methods --- .../contracts/deploying-contracts.test.ts | 120 +++++++++--------- .../guide/contracts/deploying-contracts.md | 42 +++--- 2 files changed, 84 insertions(+), 78 deletions(-) diff --git a/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts b/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts index 207ea7a7e70..9b40e3d31c8 100644 --- a/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts +++ b/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts @@ -1,65 +1,67 @@ -import { readFileSync } from 'fs'; -import { Provider, FUEL_NETWORK_URL, Wallet, ContractFactory } from 'fuels'; -import { join } from 'path'; - -import { DocSnippetProjectsEnum } from '../../../test/fixtures/forc-projects'; -import { getTestWallet } from '../../utils'; - -/** - * @group node - */ -describe(__filename, () => { - let PRIVATE_KEY: string; - let projectsPath: string; - let contractName: string; - - beforeAll(async () => { - const wallet = await getTestWallet(); - PRIVATE_KEY = wallet.privateKey; - projectsPath = join(__dirname, '../../../test/fixtures/forc-projects'); - - contractName = DocSnippetProjectsEnum.ECHO_VALUES; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Provider, TESTNET_NETWORK_URL, Wallet, ContractFactory, hexlify } from 'fuels'; +import { launchTestNode } from 'fuels/test-utils'; + +import { CounterAbi__factory } from '../../../test/typegen'; +import bytecode from '../../../test/typegen/contracts/CounterAbi.hex'; + +describe('Deploying Contracts', () => { + it('gets the max contract size for the chain', async () => { + using launched = await launchTestNode(); + + const { provider: testProvider } = launched; + // eslint-disable-next-line @typescript-eslint/no-shadow + const TESTNET_NETWORK_URL = testProvider.url; + + // #region get-contract-max-size + // #import { Provider, TESTNET_NETWORK_URL }; + const provider = await Provider.create(TESTNET_NETWORK_URL); + const { consensusParameters } = provider.getChain(); + const contractSizeLimit = consensusParameters.contractParameters.contractMaxSize; + // #endregion get-contract-max-size + expect(contractSizeLimit).toBeDefined(); }); - it('should successfully deploy and execute contract function', async () => { - // #region contract-setup-1 - // #context const PRIVATE_KEY = "..." - - const provider = await Provider.create(FUEL_NETWORK_URL); - - const wallet = Wallet.fromPrivateKey(PRIVATE_KEY, provider); - // #endregion contract-setup-1 - - // #region contract-setup-2 - // #context const contractsDir = join(__dirname, '../path/to/contracts/dir') - // #context const contractName = "contract-name" - - const byteCodePath = join(projectsPath, `${contractName}/out/release/${contractName}.bin`); - const byteCode = readFileSync(byteCodePath); - - const abiJsonPath = join(projectsPath, `${contractName}/out/release/${contractName}-abi.json`); - const abi = JSON.parse(readFileSync(abiJsonPath, 'utf8')); - // #endregion contract-setup-2 - - // #region contract-setup-3 - const factory = new ContractFactory(byteCode, abi, wallet); - - const { contractId, transactionId, waitForResult } = await factory.deployContract(); - // #endregion contract-setup-3 - - // #region contract-setup-4 + it('deploys a contract', async () => { + using launched = await launchTestNode(); + + const { + provider: testProvider, + wallets: [testWallet], + } = launched; + // eslint-disable-next-line @typescript-eslint/no-shadow + const TESTNET_NETWORK_URL = testProvider.url; + const WALLET_PVT_KEY = testWallet.privateKey; + const abi = CounterAbi__factory.abi; + + // #region setup + // #import { Provider, TESTNET_NETWORK_URL, Wallet, ContractFactory }; + // #context import { WALLET_PVT_KEY } from 'path/to/my/env/file'; + // #context import bytecode from 'path/to/typegen/hex/output'; + // #context import { abi } from 'path/to/typegen/outputs'; + const provider = await Provider.create(TESTNET_NETWORK_URL); + const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); + const factory = new ContractFactory(bytecode, abi, wallet); + // #endregion setup + expect(hexlify(factory.bytecode)).toBe(bytecode); + + // #region deploy + // Deploy the contract + const { waitForResult, contractId, transactionId } = await factory.deploy(); + // Await it's deployment const { contract, transactionResult } = await waitForResult(); - // #endregion contract-setup-4 - - // #region contract-setup-5 - const call = await contract.functions.echo_u8(15).call(); - - const { value } = await call.waitForResult(); - // #endregion contract-setup-5 - + // #endregion deploy + expect(contract).toBeDefined(); expect(transactionId).toBeDefined(); - expect(contractId).toBeDefined(); - expect(transactionResult.isStatusSuccess).toBeTruthy(); - expect(value).toBe(15); + expect(transactionResult.status).toBeTruthy(); + expect(contractId).toBe(contract.id.toB256()); + + // #region call + // Call the contract + const { waitForResult: waitForCallResult } = await contract.functions.get_count().call(); + // Await the result of the call + const { value } = await waitForCallResult(); + // #endregion call + expect(value.toNumber()).toBe(0); }); }); diff --git a/apps/docs/src/guide/contracts/deploying-contracts.md b/apps/docs/src/guide/contracts/deploying-contracts.md index 9e00beac37a..3fbd7ae27ae 100644 --- a/apps/docs/src/guide/contracts/deploying-contracts.md +++ b/apps/docs/src/guide/contracts/deploying-contracts.md @@ -7,40 +7,44 @@ # Deploying Contracts -This guide walks you through deploying a contract using the SDK, covering loading contract artifacts, initializing a contract factory, and deploying the contract. +Deploying contracts using the SDK is handled by the `ContractFactory`. The process involves collecting the contract artifacts, initializing the contract factory, and deploying the contract. -> **Note:** The maximum contract deployment size supported by the SDK is 100 KB. The SDK will `throw` an [error](../errors/index.md) for contracts larger than this size. +The SDK utilizes two different deployment processes. Either simply using a single create transaction to deploy the entire contract bytecode. Or, by splitting the contract into multiple chunks, deploying them as blobs (on chain data accessible to the VM) and then generating a contract from the associated blob IDs and deploying that as a create transaction. -## 1. Obtaining Contract Artifacts +The `ContractFactory` offers the following methods for the different processes: -After writing a contract in Sway and compiling it with `forc build` (read more on how to work with Sway), you will obtain two important artifacts: the compiled binary file and the JSON ABI file. These files are required for deploying a contract using the SDK. +- `deploy` for deploying contacts of any size (will automatically choose the appropriate deployment process). +- `deployContract` for deploying the entire contract bytecode in a single create transaction. +- `deployContractAsBlobs` for deploying the contract in chunks as blobs, and then deploying the contract as a create transaction. -## 2. Setting up the SDK Environment +> **Note:** Due to the nature of blob deployments, both `deploy` and `deployContractAsBlobs` may require multiple transactions to deploy the contract. -Before deploying a contract, set up the necessary environment by importing the required SDK components and initializing a wallet and a provider. +The deployment process used by the `ContractFactory` is dependent on the size of the contract you are attempting to deploy. The threshold for the contract size is dictated by the chain. You can find the maximum contract size supported by the chain by querying the chains consensus parameters. -<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#contract-setup-1{ts:line-numbers} +<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#get-contract-max-size{ts:line-numbers} -## 3. Loading Contract Artifacts +This guide will cover the process of deploying a contract using the `deploy` method, however all these methods can be used interchangeably dependent on the contract size. -Load the contract bytecode and JSON ABI, generated from the Sway source, into the SDK. +## 1. Setup -<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#contract-setup-2{ts:line-numbers} +After writing a contract in Sway you can build the necessary deployment artifacts either by running `forc build` (read more on how to work with Sway) or by using the [Fuels CLI](../fuels-cli/index.md) and running `fuels build` using your chosen package manager. We recommend using the Fuels CLI as it provides a more comprehensive usage including end to end type support. -## 4. Deploying the Contract +Once you have the contract artifacts, it can be passed to the `ContractFactory` for deployment, like so: -To deploy the contract, instantiate the [`ContractFactory`](../../api/Contract/ContractFactory.md) with the bytecode, ABI, and wallet. Then, call the `deployContract` method. This call resolves as soon as the transaction to deploy the contract is submitted and returns three items: the `contractId`, the `transactionId` and a `waitForResult` function. +<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#setup{ts:line-numbers} -<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#contract-setup-3{ts:line-numbers} +## 2. Contract Deployment -The `contract` instance will be returned only after calling `waitForResult` and waiting for it to resolve. To avoid blocking the rest of your code, you can attach this promise to a hook or listener that will use the contract only after it is fully deployed. +As mentioned earlier, there are two different processes for contract deployment handled by the `ContractFactory`. These can be used interchangeably, however, the `deploy` method is recommended as it will automatically choose the appropriate deployment process based on the contract size. + +This call resolves as soon as the transaction to deploy the contract is submitted and returns three items: the `contractId`, the `transactionId` and a `waitForResult` function. -<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#contract-setup-4{ts:line-numbers} +<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#deploy{ts:line-numbers} -## 5. Executing a Contract Call +The `contract` instance will be returned only after calling `waitForResult` and waiting for it to resolve. To avoid blocking the rest of your code, you can attach this promise to a hook or listener that will use the contract only after it is fully deployed. -Now that the contract is deployed, you can interact with it. In the following steps, you'll learn how to execute contract calls. +## 3. Executing a Contract Call -<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#contract-setup-5{ts:line-numbers} +Now that the contract is deployed, you can interact with it by submitting a contract call: -For a more comprehensive TypeScript-backed Fuel usage, learn how to [generate types from ABI](../fuels-cli/generating-types.md) +<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#call{ts:line-numbers} From aac2745f25dfeb9ee8e1ebc83c11f2268ff94ef4 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 7 Aug 2024 09:19:37 +0100 Subject: [PATCH 035/135] chore: use tolerance const Co-authored-by: Peter Smith --- packages/contract/src/contract-factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index eec6281eaaa..bbb32c0371e 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -247,7 +247,7 @@ export default class ContractFactory { */ async deployContractAsBlobs( deployContractOptions: DeployContractOptions = { - chunkSizeTolerance: 0.05, + chunkSizeTolerance: CHUNK_SIZE_TOLERANCE, } ): Promise> { const account = this.getAccount(); From 36358fa37ca2bd032c88e0baa02ddab732a682a3 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 7 Aug 2024 09:29:53 +0100 Subject: [PATCH 036/135] chore: update docs Co-authored-by: Chad Nehemiah --- apps/docs/src/guide/contracts/deploying-contracts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/src/guide/contracts/deploying-contracts.md b/apps/docs/src/guide/contracts/deploying-contracts.md index 3fbd7ae27ae..2a2cc1ad728 100644 --- a/apps/docs/src/guide/contracts/deploying-contracts.md +++ b/apps/docs/src/guide/contracts/deploying-contracts.md @@ -9,7 +9,7 @@ Deploying contracts using the SDK is handled by the `ContractFactory`. The process involves collecting the contract artifacts, initializing the contract factory, and deploying the contract. -The SDK utilizes two different deployment processes. Either simply using a single create transaction to deploy the entire contract bytecode. Or, by splitting the contract into multiple chunks, deploying them as blobs (on chain data accessible to the VM) and then generating a contract from the associated blob IDs and deploying that as a create transaction. +The SDK utilizes two different deployment processes. Either simply using a single create transaction to deploy the entire contract bytecode, or, by splitting the contract into multiple chunks, deploying them as blobs (on chain data accessible to the VM) and then generating a contract from the associated blob IDs. That generated contract is then deployed as a create transaction. The `ContractFactory` offers the following methods for the different processes: From 41947514fba754043110a12df0542dc0e7aa84d3 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 7 Aug 2024 09:30:32 +0100 Subject: [PATCH 037/135] chore: update docs Co-authored-by: Chad Nehemiah --- apps/docs/src/guide/contracts/deploying-contracts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/src/guide/contracts/deploying-contracts.md b/apps/docs/src/guide/contracts/deploying-contracts.md index 2a2cc1ad728..05f768b42a3 100644 --- a/apps/docs/src/guide/contracts/deploying-contracts.md +++ b/apps/docs/src/guide/contracts/deploying-contracts.md @@ -7,7 +7,7 @@ # Deploying Contracts -Deploying contracts using the SDK is handled by the `ContractFactory`. The process involves collecting the contract artifacts, initializing the contract factory, and deploying the contract. +To deploy a contract using the SDK, you can use the `ContractFactory`. This process involves collecting the contract artifacts, initializing the contract factory, and deploying the contract. The SDK utilizes two different deployment processes. Either simply using a single create transaction to deploy the entire contract bytecode, or, by splitting the contract into multiple chunks, deploying them as blobs (on chain data accessible to the VM) and then generating a contract from the associated blob IDs. That generated contract is then deployed as a create transaction. From bf7ba3bcdde4da977ae10f6f45c21d50e076c0b1 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 7 Aug 2024 09:56:08 +0100 Subject: [PATCH 038/135] feat: use fue-core@0.32.0 --- internal/fuel-core/VERSION | 2 +- packages/account/src/providers/provider.test.ts | 2 +- templates/nextjs/sway-programs/fuel-toolchain.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/fuel-core/VERSION b/internal/fuel-core/VERSION index 1b8605b913d..9eb2aa3f109 100644 --- a/internal/fuel-core/VERSION +++ b/internal/fuel-core/VERSION @@ -1 +1 @@ -git:release/0.32.0 \ No newline at end of file +0.32.0 diff --git a/packages/account/src/providers/provider.test.ts b/packages/account/src/providers/provider.test.ts index 167a58308e9..9a277d290bd 100644 --- a/packages/account/src/providers/provider.test.ts +++ b/packages/account/src/providers/provider.test.ts @@ -72,7 +72,7 @@ describe('Provider', () => { const version = await provider.getVersion(); - expect(version).toEqual('0.31.0'); + expect(version).toEqual('0.32.0'); }); it('can call()', async () => { diff --git a/templates/nextjs/sway-programs/fuel-toolchain.toml b/templates/nextjs/sway-programs/fuel-toolchain.toml index 5a563c4aded..53ca5433140 100644 --- a/templates/nextjs/sway-programs/fuel-toolchain.toml +++ b/templates/nextjs/sway-programs/fuel-toolchain.toml @@ -3,4 +3,4 @@ channel = "testnet" [components] forc = "0.62.0" -fuel-core = "0.31.0" +fuel-core = "0.32.0" From 2aa77354779fc7f2da8859387ab2e4f9801c75e7 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 7 Aug 2024 10:08:55 +0100 Subject: [PATCH 039/135] chore: add test groups --- .../guide/contracts/deploying-contracts.test.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts b/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts index 9b40e3d31c8..0f8659f4d59 100644 --- a/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts +++ b/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts @@ -2,9 +2,13 @@ import { Provider, TESTNET_NETWORK_URL, Wallet, ContractFactory, hexlify } from 'fuels'; import { launchTestNode } from 'fuels/test-utils'; -import { CounterAbi__factory } from '../../../test/typegen'; -import bytecode from '../../../test/typegen/contracts/CounterAbi.hex'; +import { EchoValuesAbi__factory } from '../../../test/typegen'; +import bytecode from '../../../test/typegen/contracts/EchoValuesAbi.hex'; +/** + * @group node + * @group browser + */ describe('Deploying Contracts', () => { it('gets the max contract size for the chain', async () => { using launched = await launchTestNode(); @@ -32,7 +36,7 @@ describe('Deploying Contracts', () => { // eslint-disable-next-line @typescript-eslint/no-shadow const TESTNET_NETWORK_URL = testProvider.url; const WALLET_PVT_KEY = testWallet.privateKey; - const abi = CounterAbi__factory.abi; + const abi = EchoValuesAbi__factory.abi; // #region setup // #import { Provider, TESTNET_NETWORK_URL, Wallet, ContractFactory }; @@ -58,10 +62,10 @@ describe('Deploying Contracts', () => { // #region call // Call the contract - const { waitForResult: waitForCallResult } = await contract.functions.get_count().call(); + const { waitForResult: waitForCallResult } = await contract.functions.echo_u8(10).call(); // Await the result of the call const { value } = await waitForCallResult(); // #endregion call - expect(value.toNumber()).toBe(0); + expect(value).toBe(10); }); }); From cf5832c4d3e69086c1089e208448cb6193f0c48f Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Wed, 7 Aug 2024 11:25:31 +0100 Subject: [PATCH 040/135] docs: add chunk tolerance documentation --- .../contracts/deploying-contracts.test.ts | 42 ++++++++++++++++++- .../guide/contracts/deploying-contracts.md | 18 ++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts b/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts index 50c6a52a8cf..b2acd8505a0 100644 --- a/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts +++ b/apps/docs-snippets/src/guide/contracts/deploying-contracts.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-shadow */ // eslint-disable-next-line @typescript-eslint/no-unused-vars import { Provider, TESTNET_NETWORK_URL, Wallet, ContractFactory, hexlify } from 'fuels'; import { launchTestNode } from 'fuels/test-utils'; @@ -13,7 +14,6 @@ describe('Deploying Contracts', () => { using launched = await launchTestNode(); const { provider: testProvider } = launched; - // eslint-disable-next-line @typescript-eslint/no-shadow const TESTNET_NETWORK_URL = testProvider.url; // #region get-contract-max-size @@ -32,7 +32,6 @@ describe('Deploying Contracts', () => { provider: testProvider, wallets: [testWallet], } = launched; - // eslint-disable-next-line @typescript-eslint/no-shadow const TESTNET_NETWORK_URL = testProvider.url; const WALLET_PVT_KEY = testWallet.privateKey; const abi = EchoValues.abi; @@ -67,4 +66,43 @@ describe('Deploying Contracts', () => { // #endregion call expect(value).toBe(10); }); + + it('deploys a large contract as blobs', async () => { + using launched = await launchTestNode(); + + const { + provider: testProvider, + wallets: [testWallet], + } = launched; + const TESTNET_NETWORK_URL = testProvider.url; + const WALLET_PVT_KEY = testWallet.privateKey; + const abi = EchoValues.abi; + const bytecode = EchoValuesFactory.bytecode; + + // #region blobs + // #import { Provider, TESTNET_NETWORK_URL, Wallet, ContractFactory }; + // #context import { WALLET_PVT_KEY } from 'path/to/my/env/file'; + // #context import { bytecode, abi } from 'path/to/typegen/outputs'; + const provider = await Provider.create(TESTNET_NETWORK_URL); + const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); + const factory = new ContractFactory(bytecode, abi, wallet); + + // Deploy the contract as blobs + const { waitForResult, contractId, transactionId } = await factory.deployContractAsBlobs({ + // Increasing chunk size tolerance to 10% incase of fee fluctuations + chunkSizeTolerance: 0.1, + }); + // Await it's deployment + const { contract, transactionResult } = await waitForResult(); + // #endregion blobs + + expect(contract).toBeDefined(); + expect(transactionId).toBeDefined(); + expect(transactionResult.status).toBeTruthy(); + expect(contractId).toBe(contract.id.toB256()); + + const { waitForResult: waitForCallResult } = await contract.functions.echo_u8(10).call(); + const { value } = await waitForCallResult(); + expect(value).toBe(10); + }); }); diff --git a/apps/docs/src/guide/contracts/deploying-contracts.md b/apps/docs/src/guide/contracts/deploying-contracts.md index 05f768b42a3..f16908c6fd4 100644 --- a/apps/docs/src/guide/contracts/deploying-contracts.md +++ b/apps/docs/src/guide/contracts/deploying-contracts.md @@ -1,4 +1,4 @@ -