Skip to content

Commit

Permalink
feat!: non-blocking blob deployment (#2929)
Browse files Browse the repository at this point in the history
* feat: intitial support for deploying large contract via blob tx

* fix: blob import

* feat: remove instanceof checks

* feat: blob tx spec and validity fixes

* feat: correctly get blobIds

* feat: add loader instructions

* feat: append blob ids to loader

* feat: lots of cleanup

* feat: fix wait for result in blob tx

* chore: linting

* test: test mods

* chore: add demo package build filter

* test: add max size test for initial deploy method

* feat: upgrade asm package

* feat: fix blob cost estimation, funding and blob id handling

* feat: dynamic blob sizing

* chore: lint

* feat: fuel-core with ed19

* feat: v4 gas costs, loader fixes and better blob id handling

* feat: simplify funding

* feat: loader contract fixes

* chore: remove redundant code

Co-authored-by: Peter Smith <[email protected]>

* feat: isTransactionType helper

* chore: doc blocks and pr refactors

* chore: remove math from contract

* feat: use fuel-core release

* chore: isTransactionType cleanups

* feat: use regex for response id check

* chore: cleanups

* feat: chunk size tolerance

* test: deploy test cases

* feat: add ed19 dependent cost to chain

* feat: getBytecodeSize

* docs: documentation for deploy methods

* chore: use tolerance const

Co-authored-by: Peter Smith <[email protected]>

* chore: update docs

Co-authored-by: Chad Nehemiah <[email protected]>

* chore: update docs

Co-authored-by: Chad Nehemiah <[email protected]>

* feat: use [email protected]

* chore: add test groups

* docs: add chunk tolerance documentation

* chore: fix doc

* chore: remove redundant method

* chore: deployContractOptions -> deployOptions

* feat: isTransactionTypeBlob

* feat: fail blob deploys for invalid funds

* chore: fix casing in gas config

* chore: linting

* test: add e2e

* chore: enable e2e

* chore: update e2e timeout

* chore: add networkUrl to e2e log

* ajust fuel core version

* update fuel core schema

* fixing test case

* fix maxFee tests

* simplify some tests

* ajust some tests

* chore: add missing test groups

* chore: enable devnet test

* chore: enable only devnet

* add group test

* fix test

* feat: optimise cost estimation

Co-authored-by: Sérgio Torres <[email protected]>

* add missing import from suggestion

* make linter happy

* increate test timeout

* increase blob tx tests timeout

* remove .only

* feat: pass deploy options to size estimation

* chore: disable testnet

* test: update isTypeBlob test

Co-authored-by: Peter Smith <[email protected]>

* test: use typegend factories in some factory tests

* test: add missing properties to test chain config

* chore: lint

* docs: add loader script rference

* chore: improve chunk fuc

* chore: remove redundant cast

* docs: update deploying contracts intro

* feat: update max size error message

* chore: small refactor for factoryt

* chore: refactor

* chore: nit

* docs: update errors docs

* test: transactionRequestify tests

* test: add another devnet test

* doc: add further info around chunk size tolerance

* chore: return e2e to defualts

* chore: add spell check words

* chore: remove breaking change

* chore: changeset

* fix: arrayify bytecode

* fix: use workspace version

Co-authored-by: Peter Smith <[email protected]>

* chore: lock file

* fix: changeset

* chore: depsync

* chore: update lock

* docs: fix links

* feat: BytesLike in contract factory

* chore: disable testnet

* test: fix docs assertion

* feat: account for max tx size consensus param

* test: blob configurable test

* test: fix assertion

* test: deploy via blobs with storage

* feat: chunkSizeTolerance -> chunkSizeOverride

* feat: manually use v4 gas types

* chore: update snippet

* feat: user patch fuel-core, fixes for devnet, blob ID already uploaded fails earleir

* chore: chunkSizeTolerance ->chunkSizeOverride

* test: e2e updates

* chore: merge conflict

* chore: restore fuel-core version

* chore: fix built in version

* chore: 0.32.1 upgrade

* chore: changeset

* choer: e2e

* chore: lint

* chore: enable testnet

* chore: lint

* chore: update changeset

Co-authored-by: Anderson Arboleya <[email protected]>

* docs: add typegen to snippet

* chore: dont export chunk size constant

Co-authored-by: Peter Smith <[email protected]>

* undo test modification after fuel-core patch

* docs: update doc

Co-authored-by: Peter Smith <[email protected]>

* feat: override -> multiplier

* test: add bytecode size check

* test: fix assertion

* chore: follow ups

* increase funding attempts to 5

* create error code

* throw error if funding attempts are exceed

* add gasPrice to fund method params to avoid re-fetch

* chore: remove blob e2e

* chore: remove script infinite

* chore: lint

* chore: reset the bare

* feat: getTransactionId promise

* chore: further conflicts

* chore: reintroduce test groups in e2e

* chore: fix tests and merge conflictt

* chore: fix test group

* feat: improve polling and docs

* chore: changeset

* chore: getTransactionId -> waitForTransactionId

* doc: make blob note more verbose

---------

Co-authored-by: Peter Smith <[email protected]>
Co-authored-by: Chad Nehemiah <[email protected]>
Co-authored-by: Sérgio Torres <[email protected]>
Co-authored-by: Anderson Arboleya <[email protected]>
  • Loading branch information
5 people authored Aug 14, 2024
1 parent f306933 commit 48ec97c
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-apes-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/contract": minor
---

feat!: non-blocking blob deployment
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ describe('Deploying Contracts', () => {

// #region deploy
// Deploy the contract
const { waitForResult, contractId, transactionId } = await factory.deploy();
const { waitForResult, contractId, waitForTransactionId } = await factory.deploy();
// Retrieve the transactionId
const transactionId = await waitForTransactionId();
// Await it's deployment
const { contract, transactionResult } = await waitForResult();
// #endregion deploy
Expand Down Expand Up @@ -90,14 +92,15 @@ describe('Deploying Contracts', () => {
const factory = new ContractFactory(bytecode, abi, wallet);

// Deploy the contract as blobs
const { waitForResult, contractId, transactionId } = await factory.deployAsBlobTx({
const { waitForResult, contractId, waitForTransactionId } = await factory.deployAsBlobTx({
// Increasing chunk size multiplier to be 90% of the max chunk size
chunkSizeMultiplier: 0.9,
});
// Await it's deployment
const { contract, transactionResult } = await waitForResult();
// #endregion blobs

const transactionId = await waitForTransactionId();
expect(contract).toBeDefined();
expect(transactionId).toBeDefined();
expect(transactionResult.status).toBeTruthy();
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/src/guide/contracts/deploying-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ Once you have the contract artifacts, it can be passed to the `ContractFactory`

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.
This call resolves as soon as the transaction to deploy the contract is submitted and returns three items: the `contractId`, a `waitForTransactionId` function and a `waitForResult` function.

<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#deploy{ts:line-numbers}

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.
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. Similarly, the transaction ID is only available once the underlying transaction has been funded. To avoid blocking the code until the ID is ready, you can use the `waitForTransactionId` function to await it's retrieval.

### 3. Executing a Contract Call

Expand All @@ -59,4 +59,4 @@ In the above guide we use the recommended `deploy` method. If you are working wi

In the above example, we also pass a `chunkSizeMultiplier` option to the deployment method. The SDK will attempt to chunk the contract to the most optimal about, however the transaction size can fluctuate and you can also be limited by request size limits against the node. By default we set a multiplier of 0.95, meaning the chunk size will be 95% of the potential maximum size, however you can adjust this to suit your needs and ensure the transaction passes. It must be set to a value between 0 and 1.

> **Note:** Blob deployments contain multiple dependent transactions, so you will need to wait longer than usual for the contract to be fully deployed and can be interacted with.
> **Note:** Deploying large contracts using blob transactions will take more time. Each transaction is dependent and has to wait for a block to be produced before it gets mined. Then a create transaction is submitted as normal. So you will need to wait longer than usual for the contract to be fully deployed and can be interacted with.
111 changes: 65 additions & 46 deletions packages/contract/src/contract-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export type DeployContractOptions = {
} & CreateTransactionRequestLike;

export type DeployContractResult<TContract extends Contract = Contract> = {
transactionId: string;
contractId: string;
waitForTransactionId: () => Promise<string>;
waitForResult: () => Promise<{
contract: TContract;
transactionResult: TransactionResult<TransactionType.Create>;
Expand Down Expand Up @@ -235,7 +235,11 @@ export default class ContractFactory {
return { contract, transactionResult };
};

return { waitForResult, contractId, transactionId: transactionResponse.id };
return {
contractId,
waitForTransactionId: () => Promise.resolve(transactionResponse.id),
waitForResult,
};
}

/**
Expand All @@ -255,7 +259,7 @@ export default class ContractFactory {
this.setConfigurableConstants(configurableConstants);
}

// Generate the chunks based on the maximum chunk size
// Generate the chunks based on the maximum chunk size and create blob txs
const chunkSize = this.getMaxChunkSize(deployOptions, chunkSizeMultiplier);
const chunks = getContractChunks(arrayify(this.bytecode), chunkSize).map((c) => {
const transactionRequest = this.blobTransactionRequest({
Expand All @@ -269,10 +273,19 @@ export default class ContractFactory {
};
});

// Check the account can afford to deploy all chunks
// Generate the associated create tx for the loader contract
const blobIds = chunks.map(({ blobId }) => blobId);
const loaderBytecode = getLoaderInstructions(blobIds);
const { contractId, transactionRequest: createRequest } = this.createTransactionRequest({
bytecode: loaderBytecode,
...deployOptions,
});

// Check the account can afford to deploy all chunks and loader
let totalCost = bn(0);
const chainInfo = account.provider.getChain();
const gasPrice = await account.provider.estimateGasPrice(10);
const priceFactor = chainInfo.consensusParameters.feeParameters.gasPriceFactor;
const estimatedBlobIds: string[] = [];

for (const { transactionRequest, blobId } of chunks) {
Expand All @@ -281,73 +294,79 @@ export default class ContractFactory {
const minFee = calculateGasFee({
gasPrice,
gas: minGas,
priceFactor: chainInfo.consensusParameters.feeParameters.gasPriceFactor,
priceFactor,
tip: transactionRequest.tip,
}).add(1);

totalCost = totalCost.add(minFee);
estimatedBlobIds.push(blobId);
}
const createMinGas = createRequest.calculateMinGas(chainInfo);
const createMinFee = calculateGasFee({
gasPrice,
gas: createMinGas,
priceFactor,
tip: createRequest.tip,
}).add(1);
totalCost = totalCost.add(createMinFee);
}
if (totalCost.gt(await account.getBalance())) {
throw new FuelError(ErrorCode.FUNDS_TOO_LOW, 'Insufficient balance to deploy contract.');
}

// Upload the blob if it hasn't been uploaded yet. Duplicate blob IDs will fail gracefully.
const uploadedBlobs: string[] = [];
// Transaction id is unset until we have funded the create tx, which is dependent on the blob txs
let txIdResolver: (value: string | PromiseLike<string>) => void;
const txIdPromise = new Promise<string>((resolve) => {
txIdResolver = resolve;
});

// Deploy the chunks as blob txs
for (const { blobId, transactionRequest } of chunks) {
if (!uploadedBlobs.includes(blobId)) {
const fundedBlobRequest = await this.fundTransactionRequest(
transactionRequest,
deployOptions
);
const waitForResult = async () => {
// Upload the blob if it hasn't been uploaded yet. Duplicate blob IDs will fail gracefully.
const uploadedBlobs: string[] = [];
// Deploy the chunks as blob txs
for (const { blobId, transactionRequest } of chunks) {
if (!uploadedBlobs.includes(blobId)) {
const fundedBlobRequest = await this.fundTransactionRequest(
transactionRequest,
deployOptions
);

let result: TransactionResult<TransactionType.Blob>;

let result: TransactionResult<TransactionType.Blob>;

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 ((<Error>err).message.indexOf(`BlobId is already taken ${blobId}`) > -1) {
// eslint-disable-next-line no-continue
continue;
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 ((<Error>err).message.indexOf(`BlobId is already taken ${blobId}`) > -1) {
// 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');
uploadedBlobs.push(blobId);
}

uploadedBlobs.push(blobId);
}
}

// Get the loader bytecode
const blobIds = chunks.map(({ blobId }) => blobId);
const loaderBytecode = getLoaderInstructions(blobIds);

// Deploy the loader contract via create tx
const { contractId, transactionRequest: createRequest } = this.createTransactionRequest({
bytecode: loaderBytecode,
...deployOptions,
});
await this.fundTransactionRequest(createRequest, deployOptions);
const transactionResponse = await account.sendTransaction(createRequest);

const waitForResult = async () => {
await this.fundTransactionRequest(createRequest, deployOptions);
txIdResolver(createRequest.getTransactionId(account.provider.getChainId()));
const transactionResponse = await account.sendTransaction(createRequest);
const transactionResult = await transactionResponse.waitForResult<TransactionType.Create>();
const contract = new Contract(contractId, this.interface, account) as TContract;

return { contract, transactionResult };
};

return { waitForResult, contractId, transactionId: transactionResponse.id };
const waitForTransactionId = () => txIdPromise;

return { waitForResult, contractId, waitForTransactionId };
}

/**
Expand Down
28 changes: 28 additions & 0 deletions packages/fuel-gauge/src/contract-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,34 @@ describe('Contract Factory', () => {
expect(value.toNumber()).toBe(1001);
}, 15000);

it('deploys large contracts via blobs and awaits transaction id', async () => {
using launched = await launchTestNode({
nodeOptions: {
args: ['--tx-pool-ttl', '1s'],
},
providerOptions: {
resourceCacheTTL: -1,
},
});

const {
wallets: [wallet],
} = launched;

const factory = new ContractFactory(LargeContractFactory.bytecode, LargeContract.abi, wallet);
const deploy = await factory.deployAsBlobTx<LargeContract>();
const initTxId = deploy.waitForTransactionId();
expect(initTxId).toStrictEqual(new Promise(() => {}));
const { contract } = await deploy.waitForResult();
expect(contract.id).toBeDefined();
const awaitTxId = await deploy.waitForTransactionId();
expect(awaitTxId).toBeTruthy();

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: {
Expand Down

0 comments on commit 48ec97c

Please sign in to comment.