Skip to content

Commit

Permalink
feat: validate blob IDs against chain in chunk deploys (#3047)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielbate authored Sep 3, 2024
1 parent 90a1d0f commit 6bef838
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 5 deletions.
6 changes: 6 additions & 0 deletions .changeset/gorgeous-laws-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/contract": patch
"@fuel-ts/account": patch
---

feat: validate blob IDs against chain in chunk deploys
49 changes: 48 additions & 1 deletion packages/account/src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { equalBytes } from '@noble/curves/abstract/utils';
import type { DocumentNode } from 'graphql';
import { GraphQLClient } from 'graphql-request';
import type { GraphQLResponse } from 'graphql-request/src/types';
import gql from 'graphql-tag';
import { clone } from 'ramda';

import { getSdk as getOperationsSdk } from './__generated__/operations';
Expand All @@ -27,6 +28,7 @@ import type {
GqlPageInfo,
GqlRelayedTransactionFailed,
GqlMessage,
Requester,
} from './__generated__/operations';
import type { Coin } from './coin';
import type { CoinQuantity, CoinQuantityLike } from './coin-quantity';
Expand Down Expand Up @@ -376,6 +378,7 @@ type SdkOperations = Omit<Operations, 'submitAndAwait' | 'statusChange'> & {
statusChange: (
...args: Parameters<Operations['statusChange']>
) => Promise<ReturnType<Operations['statusChange']>>;
getBlobs: (variables: { blobIds: string[] }) => Promise<{ blob: { id: string } | null }[]>;
};

/**
Expand Down Expand Up @@ -629,8 +632,33 @@ Supported fuel-core version: ${supportedVersion}.`
return gqlClient.request(query, vars);
};

const customOperations = (requester: Requester) => ({
getBlobs(variables: { blobIds: string[] }) {
const queryParams = variables.blobIds.map((_, i) => `$blobId${i}: BlobId!`).join(', ');
const blobParams = variables.blobIds
.map((_, i) => `blob${i}: blob(id: $blobId${i}) { id }`)
.join('\n');

const updatedVariables = variables.blobIds.reduce(
(acc, blobId, i) => {
acc[`blobId${i}`] = blobId;
return acc;
},
{} as Record<string, string>
);

const document = gql`
query getBlobs(${queryParams}) {
${blobParams}
}
`;

return requester(document, updatedVariables);
},
});

// @ts-expect-error This is due to this function being generic. Its type is specified when calling a specific operation via provider.operations.xyz.
return getOperationsSdk(executeQuery);
return { ...getOperationsSdk(executeQuery), ...customOperations(executeQuery) };
}

/**
Expand Down Expand Up @@ -1356,6 +1384,25 @@ Supported fuel-core version: ${supportedVersion}.`
return coins;
}

/**
* Returns an array of blobIds that exist on chain, for a given array of blobIds.
*
* @param blobIds - blobIds to check.
* @returns - A promise that resolves to an array of blobIds that exist on chain.
*/
async getBlobs(blobIds: string[]): Promise<string[]> {
const res = await this.operations.getBlobs({ blobIds });
const blobs: (string | null)[] = [];

Object.keys(res).forEach((key) => {
// @ts-expect-error keys are strings
const val = res[key];
blobs.push(val?.id ?? null);
});

return blobs.filter((v) => v) as string[];
}

/**
* Returns block matching the given ID or height.
*
Expand Down
12 changes: 8 additions & 4 deletions packages/contract/src/contract-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,15 +281,19 @@ export default class ContractFactory {
...deployOptions,
});

// BlobIDs only need to be uploaded once and we can check if they exist on chain
const uniqueBlobIds = [...new Set(blobIds)];
const uploadedBlobIds = await account.provider.getBlobs(uniqueBlobIds);
const blobIdsToUpload = uniqueBlobIds.filter((id) => !uploadedBlobIds.includes(id));

// 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) {
if (!estimatedBlobIds.includes(blobId)) {
if (blobIdsToUpload.includes(blobId)) {
const minGas = transactionRequest.calculateMinGas(chainInfo);
const minFee = calculateGasFee({
gasPrice,
Expand All @@ -299,7 +303,6 @@ export default class ContractFactory {
}).add(1);

totalCost = totalCost.add(minFee);
estimatedBlobIds.push(blobId);
}
const createMinGas = createRequest.calculateMinGas(chainInfo);
const createMinFee = calculateGasFee({
Expand All @@ -325,7 +328,7 @@ export default class ContractFactory {
const uploadedBlobs: string[] = [];
// Deploy the chunks as blob txs
for (const { blobId, transactionRequest } of chunks) {
if (!uploadedBlobs.includes(blobId)) {
if (!uploadedBlobs.includes(blobId) && blobIdsToUpload.includes(blobId)) {
const fundedBlobRequest = await this.fundTransactionRequest(
transactionRequest,
deployOptions
Expand All @@ -340,6 +343,7 @@ 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 ((<Error>err).message.indexOf(`BlobId is already taken ${blobId}`) > -1) {
uploadedBlobs.push(blobId);
// eslint-disable-next-line no-continue
continue;
}
Expand Down
31 changes: 31 additions & 0 deletions packages/fuel-gauge/src/contract-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,4 +518,35 @@ describe('Contract Factory', () => {
})
);
});

it('deploys large contract via blobs twice and only uploads blobs once', async () => {
using launched = await launchTestNode();

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

const sendTransactionSpy = vi.spyOn(wallet, 'sendTransaction');
const factory = new ContractFactory(LargeContractFactory.bytecode, LargeContract.abi, wallet);

const firstDeploy = await factory.deployAsBlobTx<LargeContract>({
salt: concat(['0x01', new Uint8Array(31)]),
});
const { contract: firstContract } = await firstDeploy.waitForResult();
const firstDeployCalls = sendTransactionSpy.mock.calls.length;
const secondDeploy = await factory.deployAsBlobTx<LargeContract>({
salt: concat(['0x02', new Uint8Array(31)]),
});
const { contract: secondContract } = await secondDeploy.waitForResult();
const secondDeployCalls = sendTransactionSpy.mock.calls.length;
expect(secondDeployCalls - firstDeployCalls).toBeLessThan(firstDeployCalls);

const firstCall = await firstContract.functions.something().call();
const { value: firstValue } = await firstCall.waitForResult();
expect(firstValue.toNumber()).toBe(1001);

const secondCall = await secondContract.functions.something().call();
const { value: secondValue } = await secondCall.waitForResult();
expect(secondValue.toNumber()).toBe(1001);
}, 25000);
});

0 comments on commit 6bef838

Please sign in to comment.