Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright 2025 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { EpochInfo, IotaTransactionBlockResponse } from "@iota/iota-sdk/client";
import { NotarizationClient, State } from "@iota/notarization/node";
import { getFundedClient } from "../util";

const STATE_METADATA: string | null = null; // "State metadata example";
const IMMUTABLE_DESCRIPTION: string | null = null; // "This metadata will not change";

let REFERENCE_GAS_PRICE: bigint | null = null;

const BILLION = 1000000000;
const MINIMUM_STORAGE_COST = 0.0029488; // Unit is IOTA not Nanos

function print_gas_cost(transaction_type: String, flexDataSize: number, response: IotaTransactionBlockResponse) {
const gasUsed = response.effects?.gasUsed;
const referenceGasPrice = REFERENCE_GAS_PRICE ? Number(REFERENCE_GAS_PRICE) : -1; // Fallback to -1 if EpochInfo is not available

if (gasUsed != undefined) {
const totalGasCost = parseInt(gasUsed.computationCost) + parseInt(gasUsed.storageCost)
- parseInt(gasUsed.storageRebate);
const storageCost = parseInt(gasUsed.storageCost) / BILLION;
const computationCostNanos = parseInt(gasUsed.computationCost);
const storageCostAboveMin = storageCost - MINIMUM_STORAGE_COST;
console.log(
"-------------------------------------------------------------------------------------------------------",
);
console.log(`--- Gas cost for '${transaction_type}' transaction`);
console.log(
"-------------------------------------------------------------------------------------------------------",
);
console.log(`referenceGasPrice: ${referenceGasPrice}`);
console.log(`computationCost: ${computationCostNanos / BILLION}`);
console.log(`Computation Units: ${computationCostNanos / referenceGasPrice}`);
console.log(`storageCost: ${storageCost}`);
console.log(`flexDataSize: ${flexDataSize}`);
console.log(`storageCost above minimum (0.0029488): ${storageCostAboveMin}`);
console.log(`storageCostAboveMin per flexDataSize: ${storageCostAboveMin / (flexDataSize - 1)}`);
console.log(`storageRebate: ${parseInt(gasUsed.storageRebate) / BILLION}`);
console.log(`totalGasCost (calculated): ${totalGasCost / BILLION}`);
console.log(
"-------------------------------------------------------------------------------------------------------",
);
} else {
console.log("Gas used information is not available.");
}
}

function randomString(length = 50) {
return [...Array(length + 10)].map((value) => (Math.random() * 1000000).toString(36).replace(".", "")).join("")
.substring(0, length);
}

async function create_dynamic_notarization(
notarizationClient: NotarizationClient,
stateDataSize: number,
): Promise<{ notarization: any; response: IotaTransactionBlockResponse }> {
console.log(`Creating a dynamic notarization for state updates with ${stateDataSize} bytes of state data`);

let stateData = randomString(stateDataSize);

const { output: notarization, response: response } = await notarizationClient
.createDynamic()
.withStringState(stateData, STATE_METADATA)
.withImmutableDescription(IMMUTABLE_DESCRIPTION)
.finish()
.buildAndExecute(notarizationClient);

console.log("✅ Created dynamic notarization:", notarization.id);
const flexDataSize = stateData.length + (STATE_METADATA ? STATE_METADATA.length : 0)
+ (IMMUTABLE_DESCRIPTION ? IMMUTABLE_DESCRIPTION.length : 0);
print_gas_cost("Create", flexDataSize, response);

return { notarization, response };
}

/** Create, update and destroy a Dynamic Notarization to estimate gas cost */
export async function createUpdateDestroy(): Promise<void> {
console.log("Create, update and destroy a Dynamic Notarization to estimate gas cost");

const notarizationClient = await getFundedClient();

const iotaClient = notarizationClient.iotaClient();
REFERENCE_GAS_PRICE = await iotaClient.getReferenceGasPrice();
console.log(
"Successfully fetched the referenceGasPrice: ",
REFERENCE_GAS_PRICE != null ? REFERENCE_GAS_PRICE : "Not Available",
);

let notarization;

// Create several dynamic notarizations with different initial state sizes. The notarization with the largest state size will be used for updates.
console.log("\n🆕 Creating dynamic notarizations with different initial state sizes...");
for (let i = 1; i <= 4; i++) {
const result = await create_dynamic_notarization(notarizationClient, 10 * i * i); // 10, 40, 90, 160 bytes
notarization = result.notarization;
}

// Perform multiple state updates
console.log("\n🔄 Performing state updates...");

for (let i = 1; i <= 3; i++) {
console.log(`\n--- Update ${i} ---`);

// Create new state with updated content and metadata
const newContent = randomString(i * 50); // Set this size to 138 bytes to keep total flex data size equal to the latest created notarization
const newMetadata = `Version ${i + 1}.0 - Update ${i}`;

// Update the state
const { output: _, response: response } = await notarizationClient
.updateState(
State.fromString(newContent, newMetadata),
notarization.id,
)
.buildAndExecute(notarizationClient);

console.log(`✅ State update ${i} completed`);
const flexDataSize = newContent.length + newMetadata.length;
print_gas_cost("Update", flexDataSize, response);
}

// Destroy the dynamic notarization
try {
const { output: _, response: response } = await notarizationClient
.destroy(notarization.id)
.buildAndExecute(notarizationClient);
console.log("✅ Successfully destroyed unlocked dynamic notarization");
print_gas_cost("Destroy", 1, response);
} catch (e) {
console.log("❌ Failed to destroy:", e);
}
}
62 changes: 62 additions & 0 deletions bindings/wasm/notarization_wasm/examples/src/gas-costs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Gas Cost Estimation Example for Notarization

This folder contains an example to estimate the gas cost for Notarization object creation, update and destroy operations.

It can be run like any other example.

The log output of the example is optimized to evaluate variables and constants needed to calculate gas cost as being
described in the following sections.

## Results of the Gas Cost Estimation

The gas cost for creating Dynamic and Locked Notarizations only differ in the amount of needed Storage Cost.
The mimimum Byte size of a Locked Notarization is 19 bytes larger than the one of a Dynamic Notarization due to the additional
lock information stored in the Notarization object. This results in a slightly higher Storage Cost (0.0001425 IOTA) for
Locked Notarizations compared to Dynamic Notarizations when they are created with the same amount of State Data, Metadata, etc.

**For the sake of simplicity, the following sections only describe the gas cost estimation for Dynamic Notarizations.**

### Creating Notarizations

The cost for creating a Notarization object can roughly be calculated by the following equation:

`TotalCost` = `FlexDataSize` * `FlexDataByteCost` + `MinimumStorageCost` + `ComputationCost`

`TotalCost` = F [Byte] * 0.0000076 [IOTA/Byte] + 0.00295 [IOTA] + 0.001 [IOTA]

Where:

| Parameter | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `FlexDataSize` | Sum of the byte sizes of State Data, State Metadata, Updatable Metadata and Immutable Metadata. The value must be reduced by 1 as the `MinimumStorageCost` uses 1 byte of State Data. |
| `FlexDataByteCost` | A constant value of 0.0000076 IOTA/Byte <br> This value denotes (`StorageCost` - `MinimumStorageCost`) divided by `FlexDataSize`. |
| `MinimumStorageCost` | A constant value of 0.00295 IOTA. <br> This value denotes the `StorageCost` for a Notarization with 1 Byte of `FlexDataSize` meaning a Notarization with 1 Byte of State Data, no meta data and no optional locks. |
| `ComputationCost` | A constant value of 0.001 IOTA. <br> Given the Gas Price is 1000 nano, the `ComputationCost` will always be 0.001 IOTA as creating Notarizations always consume 1000 Computation Units. |
| `TotalCost` | The amount of IOTA that would need to be paid for gas when Storage Rebate is not taken into account. The real gas cost will be lower, due to Storage Rebate, which is usually -0.0009804 IOTA when a Notarization object is created. |

Examples:

| `FlexDataSize` | `TotalCost` (Storage Rebate not taken into account) |
| -------------- | --------------------------------------------------- |
| 10 | 0.004026 IOTA |
| 100 | 0.00471 IOTA |
| 1000 | 0.01155 IOTA |

### Updating Dynamic Notarizations

The `TotalCost` for updating a Dynamic Notarization can roughly be calculated using the same equation used for creating
Notarization objects (see above).

The value for `FlexDataByteCost` should be set to 0.00000769 IOTA/Byte.

If the new Notarization State results in the same `FlexDataSize` as the overwritten old Notarization State, the Storage
Rebate will compensate the Storage Cost so that the real gas cost to be paid will be more or less the Computation Cost,
which is always 0.001 IOTA (presumed the Gas Price is 1000 nano).

### Destroying a Notarization

The `TotalCost` for destroying a Notarization is the Computation Cost which is 0.001 IOTA (presumed the Gas Price is 1000 nano).

Due to the Storage Rebate, which depends on the size of the stored Notarization object, the real gas cost to be paid will often be negative.

The Storage Rebate can roughly be calculated using the below equation. See above for more details about the used variables and constants.
3 changes: 3 additions & 0 deletions bindings/wasm/notarization_wasm/examples/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { updateState } from "./05_update_state";
import { updateMetadata } from "./06_update_metadata";
import { transferNotarization } from "./07_transfer_notarization";
import { accessReadOnlyMethods } from "./08_access_read_only_methods";
import { createUpdateDestroy } from "./gas-costs/01_create_update_destroy";
import { iotWeatherStation } from "./real-world/01_iot_weather_station";
import { legalContract } from "./real-world/02_legal_contract";

Expand Down Expand Up @@ -40,6 +41,8 @@ export async function main(example?: string) {
return await iotWeatherStation();
case "02_real_world_legal_contract":
return await legalContract();
case "01_gas_costs_create_update_destroy":
return await createUpdateDestroy();
default:
throw "Unknown example name: '" + argument + "'";
}
Expand Down
Loading