# Introducing Ethscriptions

Ethscriptions are a new way of creating and sharing digital artifacts on Ethereum using transaction calldata.

## Overview

Ethscriptions are digital artifacts created by encoding data in Ethereum transaction calldata. Unlike smart contract-based NFTs that store data in contract storage, ethscriptions use calldata—making them significantly cheaper while remaining 100% on-chain, permissionless, and censorship resistant.

The Ethscriptions protocol allows users to create and transfer digital artifacts at a fraction of the cost of traditional NFTs. Today, ethscriptions are used for images, tokens, and programmable assets.

### The Ethscriptions AppChain

The [Ethscriptions AppChain](/ethscriptions-appchain/overview) is a trust-minimized Ethereum L2 that provides cryptographic state, receipts, and EVM compatibility for ethscriptions. It uses a derivation pipeline that:

1. **Observes** Ethereum L1 calldata and events
2. **Translates** ethscription intents into deposit transactions
3. **Executes** them on an EVM with predeploy contracts

The AppChain is a Stage-2 rollup with no privileged roles—anyone can run a node and derive the canonical state from L1 data alone.

### Two Ways to Consume Ethscriptions

| Approach                                                       | Description                                                                                                              |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Traditional Indexer**                                        | Off-chain service that indexes L1 transactions and maintains state in a database. Simple queries, existing integrations. |
| [**Ethscriptions AppChain**](/ethscriptions-appchain/overview) | On-chain L2 with smart contracts, Merkle proofs, and EVM state. Enables protocol extensions, collections, and tokens.    |

Both approaches read the same L1 data and produce the same canonical ethscription state.

### Links

* [Ethscriptions Protocol GitHub](https://github.com/ethscriptions-protocol/)
* [Ethscriptions.com](https://ethscriptions.com/)

### What is Calldata?

Ethscriptions are cheaper than smart contracts because they store data on-chain using Ethereum transaction calldata, not smart contract storage.

When you send someone eth via an Ethereum transaction, calldata is the "notes field." Sometimes people write things in the notes field, but typically when you send eth to a person you leave it blank. When you interact with a smart contract, however, you add the information you're passing to the smart contract—the function name and parameters—to the calldata field.

Ethscriptions encode data into calldata as Data URIs, but this information is not directed at smart contracts on L1. Instead, the AppChain's derivation node observes these Data URIs and translates them into L2 transactions.

This video breaks it down:

{% embed url="<https://www.youtube.com/watch?v=SjVrSihJOkU>" %}
What are Ethscriptions? Venmo but you put an image in the "notes" field.
{% endembed %}

## FAQ

### **Are Ethscriptions secure and trustless?**

Yes. The Ethscriptions AppChain is a trust-minimized L2 with no privileged sequencer or admin roles. Anyone can run their own node and derive the canonical state from Ethereum L1 data. The derivation is deterministic—given the same L1 blocks, every node produces identical L2 state.

### **Are Ethscriptions decentralized?**

Yes. Ethscriptions reinterpret existing Ethereum data, which is decentralized by nature. No one's permission is required to use Ethscriptions and no one can ban you from using it. The AppChain uses based sequencing, meaning L2 block ordering is determined by L1 block ordering—not by a centralized sequencer.

### **How does the AppChain stay trust-minimized?**

The AppChain achieves trust-minimization through:

1. **Based sequencing** - L2 blocks are anchored to L1 blocks, preventing sequencer manipulation
2. **Deterministic derivation** - State can be independently verified from L1 data
3. **No admin keys** - No privileged roles that can pause, censor, or modify the chain
4. **Open source** - All code is publicly available for verification

### Who invented Ethscriptions?

The [first ethscription](https://ethscriptions.com/ethscriptions/0) was created in 2016, but the formal protocol was developed by [Tom Lehman](https://twitter.com/dumbnamenumbers) and [Michael Hirsch](https://x.com/0xHirsch). In addition to Bitcoin inscriptions, he was inspired by the famous "proto-Ethscription" from the Poly Network hacker that you can see [in this transaction](https://etherscan.io/tx/0x0ae3d3ce3630b5162484db5f3bdfacdfba33724ffb195ea92a6056beaa169490).

The author writes:

> ETHEREUM HAS THE POTENTIAL TO BE A SECURED AND ANONYMOUS COMMUNICATION CHANNEL, BUT ITS NOT FRIENDLY TO AVERAGE USERS. THE EXTRACTION OF MESSAGE REQUIRES SOME THEQUINIES, THE ENCRYPTION OF MESSAGE IS A MORE ADVANCED SKILL. I HAVE NO RESEARCH ON EXISTING PROJECTS. AND THE GAS FEE STOPS MOST USERS, THOUGH IT DOES NOT STOP REFUGEES. IS IT POSSIBLE TO ULTILIZE THE ETH NETWORK FOR FREE BY USING EXTREMELY LOW GAS? A SNAPCHAT ON CHAIN?

### More questions?

Jump into the [Discord](https://discord.gg/ethscriptions)!


# Quick Start

## Create an Ethscription in 60 Seconds

[Ethscriptions.com](https://ethscriptions.com) has an [easy creation tool](https://ethscriptions.com/create), but if you want to go step-by-step:

1. Convert an image (max size: \~90KB) to a Base64-encoded data URI (`data:image/png;base64,...`) using a service like [base64-image.de](https://www.base64-image.de/). The Ethscriptions protocol supports all data URIs but images work best.
2. Convert the data URI to hex using an online tool like [hexhero](https://www.hexhero.com/converters/utf8-to-hex).
3. Send a 0 ETH transaction *to the person you want to own the Ethscription* with the hex data from (2) in the "Hex data" field.
4. After a few moments it should appear on this site.

{% hint style="info" %}
**Duplicate Content**: By default, duplicate content is rejected—only the first ethscription with a given data URI is valid (uniqueness is determined by the SHA256 hash of the full data URI, including headers). To allow duplicates, add `rule=esip6` to your Data URI (e.g., `data:image/png;rule=esip6;base64,...`). See [ESIP-6](/esips/accepted-esips/esip-6-opt-in-ethscription-non-uniqueness) for details.
{% endhint %}

## How to Transfer Ethscriptions

1. Find the id of the Ethscription you want to transfer. An Ethscription's id is the transaction hash of the transaction that created it. It looks like this: `0xcdb372580242c1c1bbcd2914ddbdb609b33d2e2e163c6595e164cb4dc6665153`. You can get this from Etherscan or from this site.
2. Send a 0 ETH transaction to the new proposed owner, including the Ethscription ID in the "Hex data" field.

{% hint style="info" %}
**Bulk Transfers**: You can transfer multiple ethscriptions in a single transaction by concatenating their IDs (without `0x` prefixes) in the hex data field. See [ESIP-5](/esips/accepted-esips/esip-5-bulk-ethscription-transfers-from-eoas) for details.
{% endhint %}

## How to Track Ethscriptions

You can use [ethscriptions.com](https://ethscriptions.com)! However, if you don't want to rely on a third party, you have two options:

### Option 1: Traditional Indexer

Run your own indexer that follows [the protocol specification](/overview/protocol-specification). The [ethscriptions-indexer](https://github.com/ethscriptions-protocol/ethscriptions-indexer) is open source.

### Option 2: Ethscriptions AppChain

Run an [AppChain node](/ethscriptions-appchain/running-a-node) to get cryptographic state with Merkle proofs. The AppChain derives L2 state from L1 calldata using a deterministic pipeline—no trust required.

## Where to Inscribe

You can create ethscriptions by posting calldata to Ethereum L1:

| Method          | Description                                                                                                                                                |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **L1 Calldata** | Send a transaction with a Data URI in the hex data field                                                                                                   |
| **L1 Event**    | Smart contracts can emit `ethscriptions_protocol_CreateEthscription` events ([ESIP-3](/esips/accepted-esips/esip-3-smart-contract-ethscription-creations)) |

The AppChain observes both methods and translates them into L2 transactions.

## Next Steps

* [Protocol Specification](/overview/protocol-specification) - Detailed rules for creating and transferring
* [AppChain Overview](/ethscriptions-appchain/overview) - Learn about the L2 derivation pipeline
* [Collections](/ethscriptions-appchain/collections) - Create curated NFT collections (AppChain only)
* [Fixed Denomination Tokens](/ethscriptions-appchain/fixed-denomination-tokens) - Create fungible tokens (AppChain only)


# Protocol Specification

An overview of the protocol

## High Level

### Indexing Mechanics

Ethscriptions protocol state is determined by indexing all Ethereum transactions in order, starting with transactions in the Ethscriptions "genesis block" and proceeding from there sequentially in order of block number and transaction index within the block.

**Traditional Indexer Genesis Blocks:**

<pre class="language-ruby"><code class="lang-ruby"><strong>ethscriptionsGenesisBlocks = [
</strong><strong>    1608625, 3369985, 3981254, 5873780, 8205613,
</strong><strong>    9046950, 9046974, 9239285, 9430552, 10548855, 10711341, 15437996, 17478950
</strong><strong>]
</strong></code></pre>

**AppChain L1 Anchor Block:** `17478949` (one block earlier because the L2 includes a genesis block 0)

The Ethscriptions protocol only considers successful transactions. If a transaction has `status == 0` it must be ignored—i.e., transaction status must be either `1` or `null` as in the case of transactions with `blockNumber <= 4370000`.

### AppChain Derivation Pipeline

The [Ethscriptions AppChain](/ethscriptions-appchain/overview) provides an alternative to traditional indexing. Instead of maintaining state in an off-chain database, it derives L2 blocks from L1 data using a deterministic pipeline:

```
L1 Block → Observe → Translate → Execute → L2 Block
```

1. **Observe**: Watch L1 blocks, receipts, and logs for ethscription intents (Data URIs in calldata, ESIP events)
2. **Translate**: Convert intents into deposit transactions (EIP-2718 type `0x7d`)
3. **Execute**: Send deposits to geth via Engine API, executing against predeploy contracts

#### Deposit Transaction Format

Each L1 ethscription intent becomes an L2 deposit transaction:

```
Type: 0x7d (Deposit)
Fields:
  - sourceHash: Deterministic hash derived from L1 tx
  - from: Original L1 sender (spoofed via deposit semantics)
  - to: Ethscriptions predeploy contract
  - data: ABI-encoded createEthscription() or transferEthscription() call
```

The `sourceHash` ensures deterministic, reproducible derivation:

```
sourceHash = keccak256(domain || keccak256(blockHash || sourceTypeHash || selector || sourceIndex))
```

#### Content Storage (SSTORE2)

The Ethscriptions predeploy stores content using SSTORE2:

* Content is split into chunks
* Each chunk is deployed as contract bytecode
* Pointers are stored in the main contract
* Retrieval concatenates chunks

This is cheaper than SSTORE for large content and makes content immutable.

#### Fork Block Numbers

| Feature                     | Start Block | Reference                                                                                |
| --------------------------- | ----------- | ---------------------------------------------------------------------------------------- |
| ESIP-1 (Contract Transfers) | 17672762    | [ESIP-1](/esips/accepted-esips/esip-1-smart-contract-ethscription-transfers)             |
| ESIP-2 (Safe Escrow)        | 17764910    | [ESIP-2](/esips/accepted-esips/esip-2-safe-trustless-smart-contract-ethscription-escrow) |
| ESIP-3 (Contract Creations) | 18130000    | [ESIP-3](/esips/accepted-esips/esip-3-smart-contract-ethscription-creations)             |
| ESIP-5 (Bulk Transfers)     | 18330000    | [ESIP-5](/esips/accepted-esips/esip-5-bulk-ethscription-transfers-from-eoas)             |
| ESIP-7 (Gzip Compression)   | 19376500    | [ESIP-7](/esips/accepted-esips/esip-7-support-gzipped-calldata-in-ethscription-creation) |
| ESIP-8 (Blob Attachments)   | 19526000    | [ESIP-8](/esips/accepted-esips/esip-8-ethscription-attachments-aka-blobscriptions)       |

### Creating Ethscriptions

#### From an EOA

Any successful Ethereum transaction whose input data (when interpreted as UTF-8, see algorithm below for doing this) is a valid data URI (see spec below) and who has a "to" (i.e., is not a contract creation) creates an Ethscription, provided the data URI is unique *or* the data uri has the parameter `rule=esip6`. [Read more](https://docs.ethscriptions.com/esips/accepted-esips/esip-6-opt-in-ethscription-non-uniqueness).

For the URI to be unique, no Ethscription from a previous block or a transaction earlier in the block can have a dataURI with the same sha256. The sha is taken of the UTF-8 version of the dataURI.

The transaction hash of the transaction in which an ethscription was created is that ethscription's id. The recipient of the creation transaction is the Ethscription’s initial owner. The sender of the creation transaction is the Ethscription's creator.

DataURIs can be gzipped per ESIP-7 starting in block `19376500`. [Read more](https://docs.ethscriptions.com/esips/accepted-esips/esip-7-support-gzipped-calldata-in-ethscription-creation).

#### From a Smart Contract

See details in [ESIP-3](https://docs.ethscriptions.com/esips/accepted-esips/esip-3-smart-contract-ethscription-creations). The start block for ESIP-3 is `18130000`.

#### Ethscription Attachments

Per [ESIP-8](https://docs.ethscriptions.com/esips/esip-8-ethscription-attachments-aka-blobscriptions), starting in block `19526000` you can add attachments to an ethscription using EIP-4844 blobs.

### Transferring Ethscriptions

#### The Basics

The protocol defines for each ethscription a list of valid ethscription transfers. This list is ordered first by block number, then transaction index, then log index (in the case the transfer was triggered by an event).

The "from" in the first valid transfer is the ethscription's creator and the "to" in the final transfer is the ethscriptions current owner.

An ethscription's "previous owner" is the address that is in the "from" of the most recent valid transfer.

**Transferring Upon Ethscription Creation**

The creation of a new ethscription counts as a valid transfer from the "from" on the ethscription creation's transaction to the "to" on this same transaction. If I create an ethscription in a transaction with you as the "to," you are the current owner and I am the previous owner.

#### Transferring From EOAs

Any Ethereum transaction whose input data is an ethscription id as defined above is a valid Ethscription transfer, provided the transaction sender is the Ethscription’s owner. Because internal transactions from smart contracts do not have input data, this method only works for EOAs.

#### Transferring From EOAs (Under ESIP-5)

If the input data of a transaction (without its leading `0x`) is a sequence of 1 or more valid ethscription ids (without their leading `0x`), that transaction will constitute a valid transfer for each ethscription that is owned by the transaction's creator. The transaction must have occurred in `18330000` or a later block. [Read more](https://docs.ethscriptions.com/esips/accepted-esips/esip-5-bulk-ethscription-transfers-from-eoas).

#### Transferring From Smart Contracts, Under ESIP-1

If a contract emits `ethscriptions_protocol_TransferEthscription`(signature below), the protocol should register a valid ethscription transfer from the emitting contract to `recipient` of the `ethscription` with id `ethscriptionId`, provided the emitting contract owns that ethscription when emitting the event, and the event is emitted in block `17672762` or a later block.

#### Transferring From Smart Contracts, Under ESIP-2

If a contract emits `ethscriptions_protocol_TransferEthscriptionForPreviousOwner`, the protocol should register a valid ethscription transfer from the emitting contract to `recipient` of the ethscription with id `ethscriptionId`, provided:

1. The emitting contract owns the ethscription with id `ethscriptionId` when it emits the event.
2. The ethscription's previous owner was `previousOwner`.
3. The event is emitted in block `17764910` or a later block.

## The Details

#### How to interpret hex input data as UTF-8

Any method functionally equivalent to this code will work. Note that null bytes are removed even though they are valid UTF-8. This is a pragmatic choice based around the special behavior of these characters in postgres string columns.

```javascript
function hexToUTF8(hexString) {
  if (hexString.indexOf('0x') === 0) {
    hexString = hexString.slice(2);
  }

  const bytes = new Uint8Array(hexString.length / 2);

  for (let index = 0; index < bytes.length; index++) {
    const start = index * 2;
    const hexByte = hexString.slice(start, start + 2);
    const byte = Number.parseInt(hexByte, 16);
    if (Number.isNaN(byte) || byte < 0)
      throw new Error(
        `Invalid byte sequence ("${hexByte}" in "${hexString}").`
      );
    bytes[index] = byte;
  }

  let result = new TextDecoder().decode(bytes);
  return result.replace(/\0/g, '');
}
```

#### How to validate a dataURI

Any method functionally equivalent to this Ruby class will work. Note that any syntactically valid mimetype is allowed.

Base64 decoding is done according to RFC 4648 and is "strict." We do not attempt to recover from any encoding issues, meaning that `encode(decode(b64_string)) == b64_string` must be true.

A good test case is the string `str = "bD5="`. Lenient decoders will decode this to `l>`, but the correct encoding of `l>` is `bD4=` and so `encode(decode(str)) != str`, which means str is not valid Base64 for our purposes.

```ruby
class DataUri
  REGEXP = %r{
    \Adata:
    (?<mediatype>
      (?<mimetype> .+? / .+? )?
      (?<parameters> (?: ; .+? = .+? )* )
    )?
    (?<extension>;base64)?
    ,
    (?<data>.*)
  }x.freeze

  def self.valid?(uri)
    match = REGEXP.match(uri)

    match && valid_base64_content?(match[:data], match[:extension])
  end

  private 

  def self.valid_base64_content?(data, extension)
    if extension
      begin
        Base64.strict_decode64(data)
        true
      rescue ArgumentError
        false
      end
    else
      true
    end
  end
end

```

#### ESIP-1 Transfer Event Signature

```solidity
ethscriptions_protocol_TransferEthscription(
  address indexed recipient,
  bytes32 indexed ethscriptionId
)
```

#### ESIP-2 Transfer Event Signature

```solidity
event ethscriptions_protocol_TransferEthscriptionForPreviousOwner(
    address indexed previousOwner,
    address indexed recipient,
    bytes32 indexed ethscriptionId
);
```


# Overview

An Ethereum L2 for Ethscriptions with full EVM compatibility

## What is the AppChain?

* RPC: [https://mainnet.ethscriptions.com](https://mainnet.ethscriptions.com/)
* Block explorer: [https://explorer.ethscriptions.com](https://explorer.ethscriptions.com/)

The Ethscriptions AppChain is an Ethereum L2 that provides an alternative way to consume and interact with ethscriptions. Instead of relying on an off-chain indexer, the AppChain runs as a derivation pipeline that turns L1 ethscription activity into canonical L2 blocks.

The result is an OP-style "app chain" that keeps Ethscriptions UX unchanged while providing:

* **Merkle state** - Cryptographic proofs for all ethscription data
* **Receipts** - Transaction receipts for every operation
* **EVM compatibility** - Standard tooling works out of the box

## Two Ways to Consume Ethscriptions

| Approach                | Description                                                                      | Best For                                                              |
| ----------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| **Traditional Indexer** | Off-chain service that indexes L1 transactions and maintains state in a database | Simple queries, existing integrations                                 |
| **AppChain**            | On-chain L2 with smart contracts and EVM state                                   | Smart contract interactions, protocol extensions, collections, tokens |

Both approaches read the same L1 data and produce the same canonical ethscription state. The AppChain adds an execution layer that enables advanced features.

## Genesis Block

The AppChain anchors to Ethereum L1 starting at block **17478949**. This is one block before the traditional indexer's genesis block (17478950) because the AppChain includes an L2 block 0 (genesis block) that contains the initial state.

| System              | L1 Genesis Block |
| ------------------- | ---------------- |
| Traditional Indexer | 17478950         |
| AppChain            | 17478949         |

## How It Works

The AppChain runs a derivation pipeline:

1. **Observe** - Watch Ethereum L1 via JSON-RPC for ethscription intents (Data URIs and ESIP events)
2. **Translate** - Convert intents into deposit-style EVM transactions
3. **Execute** - Send transactions to geth via Engine API, producing L2 blocks
4. **Seal** - Geth seals blocks, predeploy contracts mutate state

```
L1 Block → Derivation Node → Engine API → Geth → L2 Block
```

## AppChain-Only Features

The following features are only available on the AppChain:

| Feature                                                                        | Description                                                |
| ------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| [Protocol Handlers](/ethscriptions-appchain/protocol-handlers)                 | Pluggable system for extending ethscription functionality  |
| [Collections](/ethscriptions-appchain/collections)                             | Curated NFT collections with merkle proof enforcement      |
| [Fixed Denomination Tokens](/ethscriptions-appchain/fixed-denomination-tokens) | ERC-20 tokens that move in fixed batches tied to NFT notes |

These features require smart contract execution and are not available through the traditional indexer.

## Architecture

The AppChain consists of two main components:

### Derivation Node (Ruby)

* Observes L1 blocks, receipts, and logs
* Parses ethscription intents from calldata and events
* Builds deposit transactions for L2 execution
* Communicates with geth via Engine API

### Execution Client (ethscriptions-geth)

* Modified geth client for the Ethscriptions L2
* Executes deposit transactions
* Maintains EVM state
* Provides standard JSON-RPC interface

## Benefits

### For Users

* Same ethscription experience - create and transfer as before
* Lower costs for complex operations via L2
* Smart contract composability

### For Developers

* Standard EVM tooling (ethers.js, web3.js, Foundry)
* Verifiable state with Merkle proofs
* Build on top of ethscriptions with custom protocols

### For Validators

* Deterministic derivation from L1 data
* Can verify state independently
* Optional validation against reference API

## Getting Started

To run your own AppChain node, see [Running a Node](/ethscriptions-appchain/running-a-node).

To learn about building protocol extensions, see [Protocol Handlers](/ethscriptions-appchain/protocol-handlers).


# Running a Node

How to run an Ethscriptions AppChain node with Docker Compose

This guide explains how to run your own Ethscriptions AppChain node using Docker Compose.

## Prerequisites

* **Docker Desktop** (includes the Compose plugin)
* **L1 RPC endpoint** - Archive-quality recommended for historical sync

## Quick Start

```bash
# Clone the repository
git clone https://github.com/ethscriptions-protocol/ethscriptions-node.git
cd ethscriptions-node

# Copy the environment template
cp docker-compose/.env.example docker-compose/.env

# Edit .env with your settings (see Environment Reference below)
# At minimum, set L1_RPC_URL to your L1 endpoint

# Bring up the stack
cd docker-compose
docker compose --env-file .env up -d

# Follow logs while it syncs
docker compose logs -f node

# Query the L2 RPC (default port 8545)
curl -X POST http://localhost:8545 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'

# Shut down when done
docker compose down
```

## Services

The stack runs two containers:

| Service | Description                                               |
| ------- | --------------------------------------------------------- |
| `geth`  | Ethscriptions-customized Ethereum execution client (L2)   |
| `node`  | Ruby derivation app that processes L1 data into L2 blocks |

The node waits for geth to be healthy before starting. Both services communicate via a shared IPC socket.

## Environment Reference

Key variables in `docker-compose/.env`:

### Core Configuration

| Variable               | Description                                       | Default                      |
| ---------------------- | ------------------------------------------------- | ---------------------------- |
| `COMPOSE_PROJECT_NAME` | Docker resource naming prefix                     | `ethscriptions-evm`          |
| `JWT_SECRET`           | 32-byte hex for Engine API auth (must match geth) | —                            |
| `L1_NETWORK`           | Ethereum network (mainnet, sepolia, etc.)         | `mainnet`                    |
| `L1_RPC_URL`           | Archive-quality L1 RPC endpoint                   | —                            |
| `L1_GENESIS_BLOCK`     | L1 block where the rollup anchors                 | `17478949`                   |
| `GENESIS_FILE`         | Genesis snapshot filename                         | `ethscriptions-mainnet.json` |
| `GETH_EXTERNAL_PORT`   | Host port for L2 RPC                              | `8545`                       |

### Performance Tuning

| Variable              | Description                   | Default |
| --------------------- | ----------------------------- | ------- |
| `L1_PREFETCH_FORWARD` | Blocks to prefetch ahead      | `200`   |
| `L1_PREFETCH_THREADS` | Prefetch worker threads       | `10`    |
| `JOB_CONCURRENCY`     | SolidQueue worker concurrency | `6`     |
| `JOB_THREADS`         | Job worker threads            | `3`     |

### Geth Configuration

| Variable           | Description                                 | Default  |
| ------------------ | ------------------------------------------- | -------- |
| `GC_MODE`          | `full` (pruned) or `archive` (full history) | `full`   |
| `STATE_HISTORY`    | State trie history depth                    | `100000` |
| `TX_HISTORY`       | Transaction history depth                   | `100000` |
| `ENABLE_PREIMAGES` | Retain preimages                            | `true`   |
| `CACHE_SIZE`       | State cache size                            | `25000`  |

### Validation (Optional)

| Variable                     | Description                            | Default |
| ---------------------------- | -------------------------------------- | ------- |
| `VALIDATION_ENABLED`         | Enable validator against reference API | `false` |
| `ETHSCRIPTIONS_API_BASE_URL` | Reference API endpoint                 | —       |
| `ETHSCRIPTIONS_API_KEY`      | API authentication key                 | —       |

## Monitoring

### View Logs

```bash
# All services
docker compose logs -f

# Just the derivation node
docker compose logs -f node

# Just geth
docker compose logs -f geth
```

### Check Block Height

```bash
# L2 block number
curl -s -X POST http://localhost:8545 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' | jq -r '.result' | xargs printf "%d\n"
```

### Check Sync Status

The derivation node logs show the current L1 block being processed. Compare this to the current L1 head to gauge sync progress.

## Validator (Optional)

The validator compares L2 state against a reference Ethscriptions API to verify derivation correctness. It pauses the importer when discrepancies appear so you can investigate.

To enable:

```bash
VALIDATION_ENABLED=true
ETHSCRIPTIONS_API_BASE_URL=https://your-api-endpoint.com
ETHSCRIPTIONS_API_KEY=your-api-key
```

The temporary SQLite databases in `storage/` and the SolidQueue worker pool support this reconciliation. Once historical import is verified, the derivation app remains stateless.

## Local Development

If you want to modify the Ruby code outside of Docker:

```bash
# Install Ruby 3.4.x (via rbenv, rvm, or asdf)
ruby --version  # Should show 3.4.x

# Install dependencies
bundle install

# Initialize local SQLite files
bin/setup

# Run the derivation (requires running ethscriptions-geth and L1 RPC)
# See bin/jobs and config/derive_ethscriptions_blocks.rb
```

The Docker Compose stack is recommended for production-like runs.

## Troubleshooting

### Node won't start

* Check that geth is healthy: `docker compose ps`
* Verify `L1_RPC_URL` is accessible
* Ensure `JWT_SECRET` matches between services

### Slow sync

* Increase `L1_PREFETCH_FORWARD` and `L1_PREFETCH_THREADS`
* Use a faster L1 RPC endpoint
* Consider archive mode (`GC_MODE=archive`) only if needed

### Out of disk space

* Pruned mode (`GC_MODE=full`) uses less space
* Reduce `STATE_HISTORY` and `TX_HISTORY`

## Resources

* [GitHub Repository](https://github.com/ethscriptions-protocol/ethscriptions-node)
* [ethscriptions-geth](https://github.com/ethscriptions-protocol/ethscriptions-geth)


# Protocol Handlers

Extend ethscriptions with pluggable protocol handlers

Protocol handlers allow developers to extend ethscription functionality with custom on-chain logic. When an ethscription includes protocol parameters, the Ethscriptions contract routes the call to a registered handler.

{% hint style="info" %}
Protocol handlers are an **AppChain-only** feature. They require smart contract execution on the L2.
{% endhint %}

## How It Works

1. **Registration** - A handler contract registers with the main Ethscriptions contract
2. **Creation** - User creates an ethscription with protocol parameters in the Data URI
3. **Routing** - The Ethscriptions contract detects the protocol and calls the handler
4. **Execution** - The handler performs custom logic (mint tokens, add to collection, etc.)

```
User → L1 Transaction → Derivation Node → Ethscriptions Contract → Protocol Handler
```

## Built-in Protocols

| Protocol                           | Purpose                                         | Documentation                                                                  |
| ---------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------ |
| `erc-721-ethscriptions-collection` | Curated NFT collections with merkle enforcement | [Collections](/ethscriptions-appchain/collections)                             |
| `erc-20-fixed-denomination`        | Fungible tokens with fixed-denomination notes   | [Fixed Denomination Tokens](/ethscriptions-appchain/fixed-denomination-tokens) |

## Protocol Data URI Format

Protocols are invoked by adding parameters to the Data URI. Two encoding styles are supported:

### Header-Based (for binary content)

Best for images and other binary data where the content itself is the payload:

```
data:image/png;rule=esip6;p=erc-721-ethscriptions-collection;op=add_self_to_collection;d=<base64-json>;base64,<image-bytes>
```

| Parameter        | Description                             |
| ---------------- | --------------------------------------- |
| `p=<protocol>`   | Protocol handler name (lowercase)       |
| `op=<operation>` | Operation to invoke on the handler      |
| `d=<base64>`     | Base64-encoded JSON parameters          |
| `rule=esip6`     | (Optional) Allow duplicate content URIs |

### JSON Body (for text-based operations)

Best for operations where the parameters ARE the content:

```
data:application/json,{"p":"erc-20-fixed-denomination","op":"deploy","tick":"mytoken","max":"1000000","lim":"1000"}
```

The JSON body contains:

* `p` - Protocol handler name
* `op` - Operation name
* Additional operation-specific fields

## Example: Creating a Collection Item

Header-based format for adding an image to a collection:

```
data:image/png;rule=esip6;p=erc-721-ethscriptions-collection;op=add_self_to_collection;d=eyJjb2xsZWN0aW9uSWQiOiIweC4uLiIsIml0ZW1JbmRleCI6MX0=;base64,iVBORw0KGgo...
```

Where the base64-decoded `d` parameter contains:

```json
{
  "collection_id": "0x...",
  "item": {
    "item_index": "1",
    "name": "Item #2",
    "background_color": "#00FF00",
    "description": "The second item",
    "attributes": [{"trait_type": "Rarity", "value": "Rare"}],
    "merkle_proof": []
  }
}
```

## Example: Deploying a Token

JSON body format for deploying a fixed-denomination token:

```
data:application/json,{"p":"erc-20-fixed-denomination","op":"deploy","tick":"mytoken","max":"1000000","lim":"1000"}
```

## Protocol Handler Contract Interface

Handlers implement the `IProtocolHandler` interface:

```solidity
interface IProtocolHandler {
    // Called when ethscription is transferred
    function onTransfer(
        bytes32 ethscriptionId,
        address from,
        address to
    ) external;

    // Returns the protocol name
    function protocolName() external pure returns (string memory);
}
```

Operation functions (prefixed with `op_`) are called dynamically based on the `op` parameter in the data URI. For example, the collections manager implements:

* `op_create_collection_and_add_self(...)`
* `op_add_self_to_collection(...)`
* `op_edit_collection(...)`

These are not part of the interface - the Ethscriptions contract uses dynamic dispatch to call them.

## Events

The Ethscriptions contract emits events for protocol operations:

```solidity
event ProtocolHandlerSuccess(
    bytes32 indexed ethscriptionId,
    string protocol,
    bytes returnData
);

event ProtocolHandlerFailed(
    bytes32 indexed ethscriptionId,
    string protocol,
    bytes revertData
);
```

## Registration

Protocols are registered at genesis or through governance. The main contract maintains a mapping:

```solidity
mapping(string => address) public protocolHandlers;
```

When an ethscription with protocol params is created, the contract looks up the handler and calls the appropriate `op_*` function.

## Security Considerations

* Protocol handlers run within the Ethscriptions contract's context
* Handlers cannot modify ethscription ownership directly
* All state changes are atomic with the ethscription creation
* Failed handler calls emit `ProtocolHandlerFailed` but don't revert the ethscription creation


# Collections

Curated NFT collections with optional merkle proof enforcement

The ERC-721 Ethscriptions Collections protocol allows creators to build curated collections of ethscriptions with rich metadata and optional access control.

{% hint style="info" %}
Collections are an **AppChain-only** feature. They require smart contract execution on the L2.
{% endhint %}

## Overview

* **Collection**: A named set of ethscriptions with metadata (name, symbol, description, max supply)
* **Items**: Individual ethscriptions added to a collection
* **Merkle Enforcement**: Optional cryptographic restriction on which items can be added

## Creating a Collection

Use the `create_collection_and_add_self` operation to create a collection and add the first item in one transaction:

```
data:image/png;rule=esip6;p=erc-721-ethscriptions-collection;op=create_collection_and_add_self;d=<base64-json>;base64,<image-bytes>
```

{% hint style="info" %}
The `rule=esip6` parameter allows duplicate content. Without it, if the same data URI (including headers) was used in a previous ethscription, the new ethscription would be rejected as a duplicate. Uniqueness is based on SHA256 of the full data URI, not just the payload.
{% endhint %}

Where the base64-decoded `d` parameter contains:

```json
{
  "metadata": {
    "name": "My Collection",
    "symbol": "MYC",
    "max_supply": "100",
    "description": "A curated collection of digital artifacts",
    "logo_image_uri": "",
    "banner_image_uri": "",
    "background_color": "",
    "website_link": "https://example.com",
    "twitter_link": "myhandle",
    "discord_link": "https://discord.gg/...",
    "merkle_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "initial_owner": "0x1234567890abcdef1234567890abcdef12345678"
  },
  "item": {
    "item_index": "0",
    "name": "Item #1",
    "background_color": "#FF0000",
    "description": "The first item in the collection",
    "attributes": [
      { "trait_type": "Rarity", "value": "Legendary" },
      { "trait_type": "Color", "value": "Red" }
    ],
    "merkle_proof": []
  }
}
```

### Metadata Object Fields

All fields must be present in exact order. Use empty strings for optional values.

| Field              | Description                                                      |
| ------------------ | ---------------------------------------------------------------- |
| `name`             | Collection name                                                  |
| `symbol`           | Short symbol (e.g., "MYC")                                       |
| `max_supply`       | Maximum number of items (as string)                              |
| `description`      | Collection description (can be empty)                            |
| `logo_image_uri`   | Logo image as Data URI (can be empty)                            |
| `banner_image_uri` | Banner image as Data URI (can be empty)                          |
| `background_color` | Default background color (can be empty)                          |
| `website_link`     | Project website URL (can be empty)                               |
| `twitter_link`     | Twitter/X handle (can be empty)                                  |
| `discord_link`     | Discord invite URL (can be empty)                                |
| `merkle_root`      | Merkle root for access control (use zero bytes32 for owner-only) |
| `initial_owner`    | Address that will own the collection (lowercase)                 |

### Item Object Fields

| Field              | Description                                   |
| ------------------ | --------------------------------------------- |
| `item_index`       | Position in collection (0-indexed, as string) |
| `name`             | Item name                                     |
| `background_color` | Item-specific background color                |
| `description`      | Item description                              |
| `attributes`       | Array of `{ trait_type, value }` objects      |
| `merkle_proof`     | Array of proof hashes (for non-owner adds)    |

{% hint style="warning" %}
**Strict Key Order**: For JSON-based operations, keys must appear in exactly the order shown in the tables above. Attribute objects must use `{ "trait_type": "...", "value": "..." }` key order.
{% endhint %}

## Adding Items to a Collection

After creating a collection, add items with `add_self_to_collection`:

```
data:image/png;rule=esip6;p=erc-721-ethscriptions-collection;op=add_self_to_collection;d=<base64-json>;base64,<image-bytes>
```

Where the `d` parameter contains:

```json
{
  "collection_id": "0x...",
  "item": {
    "item_index": "1",
    "name": "Item #2",
    "background_color": "#00FF00",
    "description": "The second item",
    "attributes": [
      { "trait_type": "Rarity", "value": "Rare" },
      { "trait_type": "Color", "value": "Green" }
    ],
    "merkle_proof": []
  }
}
```

The `collection_id` is the L1 transaction hash of the collection creation.

## Merkle Proof Enforcement

When a collection has a non-zero `merkle_root`, non-owners must provide a merkle proof to add items. This ensures only pre-approved items with exact metadata can be added.

### How It Works

1. **Creator generates merkle tree** from approved items
2. **Each leaf** is computed from item metadata
3. **Creator sets merkle root** when creating collection
4. **Non-owners provide proofs** when adding items

### Merkle Leaf Computation

Each leaf is computed as:

```solidity
keccak256(abi.encode(
    contentHash,      // keccak256 of content bytes (bytes32)
    itemIndex,        // uint256
    name,             // string
    backgroundColor,  // string
    description,      // string
    attributes        // (string,string)[] - array of (trait_type, value) tuples
))
```

### Merkle Tree Structure

For a 3-item collection, the tree looks like:

```
        root
       /    \
    H(0,1)   leaf2
    /    \
 leaf0  leaf1
```

Where:

* **Proof for leaf0**: `[leaf1, leaf2]`
* **Proof for leaf1**: `[leaf0, leaf2]`
* **Proof for leaf2**: `[H(leaf0, leaf1)]`

### Pair Hashing

The merkle tree uses byte-wise ordering (same as OpenZeppelin):

```typescript
function hashPair(a: Hex, b: Hex): Hex {
  // Compare bytes, not strings
  const aBytes = hexToBytes(a);
  const bBytes = hexToBytes(b);
  let aLessThanB = false;
  for (let i = 0; i < 32; i++) {
    if (aBytes[i] !== bBytes[i]) {
      aLessThanB = aBytes[i] < bBytes[i];
      break;
    }
  }
  return keccak256(concat(aLessThanB ? [a, b] : [b, a]));
}
```

This ensures consistent proof verification regardless of sibling order.

### Adding Items with Proofs

Non-owners include the merkle proof in the `item` object:

```json
{
  "collection_id": "0x...",
  "item": {
    "item_index": "1",
    "name": "Item #2",
    "background_color": "#00FF00",
    "description": "The second item",
    "attributes": [
      { "trait_type": "Rarity", "value": "Rare" }
    ],
    "merkle_proof": ["0xaab5a305...", "0x58672b0c..."]
  }
}
```

### Owner Bypass

Collection owners can always add items without providing merkle proofs. This allows:

* Adding items not in the original tree
* Making corrections
* Flexibility for collection management

## Example: Creating a Merkle-Enforced Collection

This walkthrough creates a 3-item collection where:

| Item           | Index | Added By  | Merkle Proof Required? |
| -------------- | ----- | --------- | ---------------------- |
| Item 1 (Red)   | 0     | Owner     | No (owner bypass)      |
| Item 2 (Green) | 1     | Non-owner | Yes                    |
| Item 3 (Blue)  | 2     | Non-owner | Yes                    |

### Step 1: Compute Content Hashes

For each image, compute the keccak256 hash of the raw bytes:

```
Item 0 content hash: 0x666af27e...
Item 1 content hash: 0x06e51d26...
Item 2 content hash: 0x09ecc1a2...
```

### Step 2: Build Merkle Leaves

Compute each leaf from the item metadata:

```
Leaf 0: keccak256(abi.encode(0x666af27e..., 0, "Item #1", "#FF0000", "First item", [("Rarity", "Common")]))
        = 0xd9b535b9...

Leaf 1: keccak256(abi.encode(0x06e51d26..., 1, "Item #2", "#00FF00", "Second item", [("Rarity", "Rare")]))
        = 0xaab5a305...

Leaf 2: keccak256(abi.encode(0x09ecc1a2..., 2, "Item #3", "#0000FF", "Third item", [("Rarity", "Epic")]))
        = 0x58672b0c...
```

### Step 3: Compute Merkle Root

```
H(leaf0, leaf1) = 0x659a61c9...
Merkle Root = H(H(leaf0, leaf1), leaf2) = 0x06fbc22a...
```

### Step 4: Create Collection (Owner)

The owner creates the collection with the merkle root and adds the first item:

1. Send a 0 ETH transaction to any address
2. Include the hex-encoded Data URI with `op=create_collection_and_add_self`
3. The `merkle_root` is set to `0x06fbc22a...`
4. Save the transaction hash as `collection_id`

The owner doesn't need a merkle proof for their own item.

### Step 5: Add Items (Non-Owner)

A different address adds items 2 and 3 with merkle proofs:

For Item 2 (index 1):

```json
{
  "collection_id": "0x<tx-hash-from-step-4>",
  "item": {
    "item_index": "1",
    "name": "Item #2",
    "background_color": "#00FF00",
    "description": "Second item",
    "attributes": [
      { "trait_type": "Rarity", "value": "Rare" }
    ],
    "merkle_proof": ["0xd9b535b9...", "0x58672b0c..."]
  }
}
```

The proof must match exactly, and the metadata must match what was used to compute the leaf.

## Operations Reference

| Operation                        | Description                           |
| -------------------------------- | ------------------------------------- |
| `create_collection_and_add_self` | Create collection and add first item  |
| `add_self_to_collection`         | Add item to existing collection       |
| `edit_collection`                | Update collection metadata            |
| `edit_collection_item`           | Update item metadata                  |
| `transfer_ownership`             | Transfer collection ownership         |
| `renounce_ownership`             | Surrender ownership (to zero address) |
| `remove_items`                   | Delete items from collection          |
| `lock_collection`                | Prevent further additions             |

{% hint style="info" %}
**ESIP-6 is optional.** Add `rule=esip6` to your data URI only if you need to allow duplicate content (e.g., sending the same JSON command multiple times). Without it, an ethscription with identical content to an existing one will not be created. For image-based operations, add it to the header: `data:image/png;rule=esip6;p=...`. For text-based operations: `data:;rule=esip6,{...json...}`.
{% endhint %}

## Editing Collections

Update collection metadata with `edit_collection`. Send as a data URI:

```
data:;rule=esip6,{"p":"erc-721-ethscriptions-collection","op":"edit_collection",...}
```

JSON payload (all fields required; pass current values to keep them, empty strings will clear fields):

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "edit_collection",
  "collection_id": "0x...",
  "description": "Updated description",
  "logo_image_uri": "",
  "banner_image_uri": "",
  "background_color": "",
  "website_link": "",
  "twitter_link": "",
  "discord_link": "",
  "merkle_root": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
```

Only the collection owner can edit.

## Editing Items

Update item metadata with `edit_collection_item` (all fields required):

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "edit_collection_item",
  "collection_id": "0x...",
  "item_index": "0",
  "name": "New Item Name",
  "background_color": "#FF0000",
  "description": "Updated description",
  "attributes": [
    { "trait_type": "Rarity", "value": "Legendary" }
  ]
}
```

Only the collection owner can edit items.

## Removing Items

Remove items with `remove_items` using ethscription IDs (transaction hashes):

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "remove_items",
  "collection_id": "0x...",
  "ethscription_ids": [
    "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
  ]
}
```

Only the collection owner can remove items.

## Transferring Ownership

Transfer collection ownership with `transfer_ownership`:

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "transfer_ownership",
  "collection_id": "0x...",
  "new_owner": "0x..."
}
```

Only the current owner can transfer ownership.

## Renouncing Ownership

Permanently surrender ownership with `renounce_ownership`:

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "renounce_ownership",
  "collection_id": "0x..."
}
```

After renouncing, no one can edit the collection or add items (unless they have valid merkle proofs for a non-zero merkle root collection).

## Locking Collections

Once locked, no more items can be added:

```json
{
  "p": "erc-721-ethscriptions-collection",
  "op": "lock_collection",
  "collection_id": "0x..."
}
```

This is irreversible. Only the collection owner can lock.

## Error Handling

| Error                   | Cause                                                                                |
| ----------------------- | ------------------------------------------------------------------------------------ |
| `Invalid Merkle proof`  | Proof doesn't match root, or metadata differs from what was used to compute the leaf |
| `Merkle proof required` | Non-owner tried to add to a collection with zero merkle root (owner-only mode)       |
| `Item slot taken`       | Index already has an item                                                            |
| `Collection locked`     | Cannot add to locked collection                                                      |
| `Exceeds max supply`    | Collection is full                                                                   |
| `Not collection owner`  | Only owner can perform this operation                                                |

## Security Considerations

1. **Content Hash Verification** - The merkle leaf includes the content hash, ensuring exact image content is verified
2. **Metadata Binding** - All metadata is bound to the merkle proof and cannot be changed after the tree is computed
3. **Owner Bypass** - Collection owners can always add items, useful for corrections
4. **Locking** - Once locked, no more items can be added even with valid proofs
5. **Zero Merkle Root** - When `merkle_root` is zero, only the owner can add items

## Generating Merkle Trees (TypeScript)

Below is complete TypeScript code using [viem](https://viem.sh/) for generating merkle trees and collection calldata.

### Dependencies

```bash
npm install viem
```

### Helper Functions

```typescript
import {
  keccak256,
  encodeAbiParameters,
  stringToHex,
  concat,
  hexToBytes,
  type Hex,
} from 'viem';

/**
 * Compare two bytes32 values byte-by-byte (matches OpenZeppelin)
 */
function lt32(a: Hex, b: Hex): boolean {
  const aBytes = hexToBytes(a);
  const bBytes = hexToBytes(b);
  for (let i = 0; i < 32; i++) {
    if (aBytes[i] !== bBytes[i]) return aBytes[i] < bBytes[i];
  }
  return false;
}

/**
 * Hash pair with byte-wise ordering (matches OpenZeppelin MerkleProof)
 */
function hashPair(a: Hex, b: Hex): Hex {
  return keccak256(concat(lt32(a, b) ? [a, b] : [b, a]));
}

/**
 * Compute content hash from image bytes
 */
function computeContentHash(imageBase64: string): Hex {
  const imageBytes = Uint8Array.from(Buffer.from(imageBase64, 'base64'));
  return keccak256(imageBytes);
}

/**
 * Compute merkle leaf hash matching the Solidity contract
 */
function computeLeafHash(
  contentHash: Hex,
  itemIndex: bigint,
  name: string,
  backgroundColor: string,
  description: string,
  attributes: { traitType: string; value: string }[]
): Hex {
  const encoded = encodeAbiParameters(
    [
      { name: 'contentHash', type: 'bytes32' },
      { name: 'itemIndex', type: 'uint256' },
      { name: 'name', type: 'string' },
      { name: 'backgroundColor', type: 'string' },
      { name: 'description', type: 'string' },
      { name: 'attributes', type: 'tuple[]', components: [
        { name: 'traitType', type: 'string' },
        { name: 'value', type: 'string' },
      ]},
    ],
    [
      contentHash,
      itemIndex,
      name,
      backgroundColor,
      description,
      attributes.map(a => ({ traitType: a.traitType, value: a.value })),
    ]
  );
  return keccak256(encoded);
}

/**
 * Build merkle tree from 3 leaves
 *
 * Tree structure:
 *         root
 *        /    \
 *     H(0,1)   leaf2
 *    /    \
 * leaf0  leaf1
 */
function buildMerkleTree(leaves: [Hex, Hex, Hex]): {
  root: Hex;
  proofs: [Hex[], Hex[], Hex[]];
} {
  const [leaf0, leaf1, leaf2] = leaves;
  const h01 = hashPair(leaf0, leaf1);
  const root = hashPair(h01, leaf2);

  return {
    root,
    proofs: [
      [leaf1, leaf2],  // Proof for leaf0
      [leaf0, leaf2],  // Proof for leaf1
      [h01],           // Proof for leaf2
    ],
  };
}

/**
 * Generate data URI for collection operations
 */
function generateCollectionDataUri(
  operation: string,
  params: object,
  imageBase64: string
): string {
  const jsonBase64 = Buffer.from(JSON.stringify(params)).toString('base64');
  return `data:image/png;rule=esip6;p=erc-721-ethscriptions-collection;op=${operation};d=${jsonBase64};base64,${imageBase64}`;
}

/**
 * Convert data URI to hex calldata for transaction
 */
function dataUriToHex(dataUri: string): Hex {
  return stringToHex(dataUri);
}
```

### Complete Example

```typescript
// Sample 1x1 pixel PNGs (red, green, blue)
const IMAGES = [
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==',
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAEBgIApD5fRAAAAABJRU5ErkJggg==',
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBgIA/JDm2AAAAABJRU5ErkJggg==',
];

const ITEMS = [
  { name: 'Item #1', bg: '#FF0000', desc: 'First item', attrs: [{ traitType: 'Color', value: 'Red' }] },
  { name: 'Item #2', bg: '#00FF00', desc: 'Second item', attrs: [{ traitType: 'Color', value: 'Green' }] },
  { name: 'Item #3', bg: '#0000FF', desc: 'Third item', attrs: [{ traitType: 'Color', value: 'Blue' }] },
];

const OWNER_ADDRESS = '0xYourAddressHere';

// Step 1: Compute content hashes
const contentHashes = IMAGES.map(img => computeContentHash(img));
console.log('Content hashes:', contentHashes);

// Step 2: Compute merkle leaves
const leaves = ITEMS.map((item, i) => computeLeafHash(
  contentHashes[i],
  BigInt(i),
  item.name,
  item.bg,
  item.desc,
  item.attrs
)) as [Hex, Hex, Hex];
console.log('Leaves:', leaves);

// Step 3: Build merkle tree
const { root: merkleRoot, proofs } = buildMerkleTree(leaves);
console.log('Merkle root:', merkleRoot);
console.log('Proofs:', proofs);

// Step 4: Generate create collection calldata
const createParams = {
  metadata: {
    name: 'My Collection',
    symbol: 'MYC',
    max_supply: '3',
    description: 'A merkle-enforced collection',
    logo_image_uri: '',
    banner_image_uri: '',
    background_color: '',
    website_link: '',
    twitter_link: '',
    discord_link: '',
    merkle_root: merkleRoot,
    initial_owner: OWNER_ADDRESS.toLowerCase(),
  },
  item: {
    item_index: '0',
    name: ITEMS[0].name,
    background_color: ITEMS[0].bg,
    description: ITEMS[0].desc,
    attributes: ITEMS[0].attrs.map(a => ({ trait_type: a.traitType, value: a.value })),
    merkle_proof: [],  // Owner bypasses merkle check
  },
};

const createDataUri = generateCollectionDataUri(
  'create_collection_and_add_self',
  createParams,
  IMAGES[0]
);

console.log('Create collection data URI:', createDataUri);
console.log('Create collection hex:', dataUriToHex(createDataUri));

// Step 5: Generate add item calldata (for non-owner)
// Replace with actual collection_id after creating collection
const COLLECTION_ID = '0x<tx-hash-from-create>';

const addItemParams = {
  collection_id: COLLECTION_ID,
  item: {
    item_index: '1',
    name: ITEMS[1].name,
    background_color: ITEMS[1].bg,
    description: ITEMS[1].desc,
    attributes: ITEMS[1].attrs.map(a => ({ trait_type: a.traitType, value: a.value })),
    merkle_proof: proofs[1],  // Include proof for non-owner
  },
};

const addDataUri = generateCollectionDataUri(
  'add_self_to_collection',
  addItemParams,
  IMAGES[1]
);

console.log('Add item data URI:', addDataUri);
console.log('Add item hex:', dataUriToHex(addDataUri));
```

### Sending Transactions

To create an ethscription, send a 0 ETH transaction with the hex calldata:

```typescript
import { createWalletClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount('0xYourPrivateKey');
const client = createWalletClient({
  account,
  chain: mainnet,
  transport: http('https://your-rpc-url'),
});

// Self-ethscription (send to yourself)
const txHash = await client.sendTransaction({
  to: account.address,
  data: dataUriToHex(createDataUri),
  value: 0n,
});

console.log('Transaction hash (this is the collection_id):', txHash);
```


# Fixed Denomination Tokens

ERC-20 tokens that move in fixed batches tied to NFT notes

The ERC-20 Fixed Denomination protocol creates fungible tokens where balances move in fixed batches (denominations) tied to NFT "notes."

{% hint style="info" %}
Fixed Denomination Tokens are an **AppChain-only** feature. They require smart contract execution on the L2.
{% endhint %}

## Overview

Unlike standard ERC-20 tokens where users can transfer arbitrary amounts, fixed denomination tokens:

* Move in **fixed batches** (the denomination)
* Are tied to **NFT notes** that represent the token amount
* Transfer **automatically** when the note transfers

This creates a unique hybrid between fungible and non-fungible tokens.

## How It Differs from Standard ERC-20

| Feature          | Standard ERC-20       | Fixed Denomination           |
| ---------------- | --------------------- | ---------------------------- |
| Transfer amounts | Arbitrary             | Fixed denomination only      |
| Transfer method  | `transfer()` function | Transfer the NFT note        |
| Divisibility     | Yes                   | No (whole notes only)        |
| Balance tracking | Single balance        | Balance = sum of owned notes |

## Deploy a Token

Create an ethscription with JSON content to deploy a new token:

```json
{"p":"erc-20-fixed-denomination","op":"deploy","tick":"mytoken","max":"1000000","lim":"1000"}
```

As a Data URI:

```
data:application/json,{"p":"erc-20-fixed-denomination","op":"deploy","tick":"mytoken","max":"1000000","lim":"1000"}
```

{% hint style="warning" %}
**Strict JSON Format**: The JSON must be minified (no whitespace), use exact key order as shown, and contain no extra fields. The legacy protocol name `"p":"erc-20"` is also accepted for backward compatibility.
{% endhint %}

### Deploy Parameters

| Field  | Description                         | Constraints                                                |
| ------ | ----------------------------------- | ---------------------------------------------------------- |
| `tick` | Token symbol                        | Lowercase alphanumeric, max 28 chars                       |
| `max`  | Maximum total supply                | uint256, **must be divisible by `lim`** (`max % lim == 0`) |
| `lim`  | Amount per mint note (denomination) | uint256, must divide evenly into `max`                     |

{% hint style="warning" %}
**Constraint**: `max` must be evenly divisible by `lim`. For example, `max=1000` and `lim=100` is valid (10 notes), but `max=1000` and `lim=300` is invalid.
{% endhint %}

## Mint Notes

After deployment, create notes by minting:

```json
{"p":"erc-20-fixed-denomination","op":"mint","tick":"mytoken","id":"1","amt":"1000"}
```

As a Data URI:

```
data:application/json,{"p":"erc-20-fixed-denomination","op":"mint","tick":"mytoken","id":"1","amt":"1000"}
```

### Mint Parameters

| Field  | Description                              | Constraints                                     |
| ------ | ---------------------------------------- | ----------------------------------------------- |
| `tick` | Token symbol (must match deployed token) | Must be a deployed token                        |
| `id`   | Unique note identifier within the token  | **Must be ≥ 1** (IDs start at 1, not 0)         |
| `amt`  | Token amount for this note               | **Must equal `lim`** from deploy (`amt == lim`) |

{% hint style="warning" %}
**Constraints**:

* `id` must be ≥ 1 (note IDs start at 1)
* `amt` must exactly equal the token's `lim` value
  {% endhint %}

Each mint creates:

1. An **ethscription** (the mint inscription)
2. An **NFT note** representing the token amount
3. **ERC-20 balance** credited to the minter

## Transfer Mechanics

Transferring tokens works differently than standard ERC-20:

### Standard ERC-20 (disabled)

```solidity
// This does NOT work for fixed denomination tokens
token.transfer(recipient, amount);
```

### Fixed Denomination (how it works)

Transfer the **ethscription** (the mint inscription) to move tokens:

```
To: 0xRecipient
Value: 0 ETH
Data: 0x<mint-inscription-txhash>
```

When the ethscription transfers:

1. The inscription moves to the new owner
2. The NFT note automatically transfers
3. The ERC-20 balance automatically moves

All three are synchronized atomically.

## Example Flow

### 1. Deploy Token

Alice deploys "mytoken" with max supply 10,000 and denomination 100:

```json
{"p":"erc-20-fixed-denomination","op":"deploy","tick":"mytoken","max":"10000","lim":"100"}
```

### 2. Mint Notes

Alice mints note #1:

```json
{"p":"erc-20-fixed-denomination","op":"mint","tick":"mytoken","id":"1","amt":"100"}
```

Alice now has:

* 1 mint inscription (ethscription)
* 1 NFT note (tokenId = 1)
* 100 "mytoken" ERC-20 balance

### 3. Transfer

Alice transfers the mint inscription to Bob:

```
To: Bob's address
Data: 0x<alice-mint-txhash>
```

Result:

* Mint inscription → Bob
* NFT note #1 → Bob
* 100 "mytoken" balance: Alice → Bob

## Querying Balances

### ERC-20 Balance

```javascript
const balance = await tokenContract.balanceOf(address);
```

### Note Ownership

```javascript
const owner = await tokenContract.ownerOf(noteId);
```

### Notes Owned

Each note's `amount` contributes to the holder's ERC-20 balance. The total balance equals the sum of all owned notes' amounts.

## Use Cases

### Collectible Tokens

Each note is a unique collectible that also carries fungible value.

### Batch Transfers

Transfer multiple notes to move large amounts efficiently.

### Marketplace Trading

Notes can be traded on NFT marketplaces while carrying their token value.

### Fair Distribution

Fixed denominations ensure equal distribution - everyone gets the same sized "bills."

## Technical Details

### Contract Architecture

* **ERC20FixedDenominationManager** - Handles deploy/mint operations
* **ERC20FixedDenomination** - Individual token contract (ERC-20 + ERC-721 hybrid)

### Storage

Each token stores:

* Token metadata (tick, max supply, denomination)
* Note registry (id → ethscription mapping)
* Balances (derived from note ownership)

### Events

```solidity
event TokenDeployed(string tick, uint256 maxSupply, uint256 denomination);
event NoteMinted(string tick, uint256 noteId, address owner, uint256 amount);
event NoteTransferred(string tick, uint256 noteId, address from, address to);
```

## Limitations

* Cannot transfer partial amounts (only whole notes)
* Cannot combine notes
* Cannot split notes
* Direct ERC-20 transfers are disabled

## Security Considerations

1. **Atomic transfers** - ERC-20 and NFT always move together
2. **No double-spending** - Note ownership enforced on-chain
3. **Immutable denomination** - Cannot change after deployment
4. **Supply cap** - Cannot exceed max supply


# What are ESIPs?

Proposals for improvement to the Ethscriptions protocol.


# Accepted ESIPs


# ESIP-1: Smart Contract Ethscription Transfers

This ESIP is LIVE

#### Version History

* June 29: Changed event name to be more explicit and to reduce changes of collision.
* June 29: Added spec for case in which there are multiple transfers in a given transaction.

#### Specification

Incorporate one new smart contract event into the Ethscriptions Protocol:

```solidity
ethscriptions_protocol_TransferEthscription(
  address indexed recipient,
  bytes32 indexed ethscriptionId
)
```

Event signature:

```solidity
// "0xf30861289185032f511ff94a8127e470f3d0e6230be4925cb6fad33f3436dffb"
keccak256("ethscriptions_protocol_TransferEthscription(address,bytes32)")
```

When a contract emits `ethscriptions_protocol_TransferEthscription`, the protocol should register a valid ethscription transfer from the emitting contract to `recipient` of the `ethscription` with id `ethscriptionId`, provided the emitting contract owns that ethscription when emitting the event, and the event is emitted in `17672762` or a later block.

If there are multiple valid events they should be processed in the order of their log index.

If the input data of the transaction also represents a valid transfer, this transfer will be processed before all event-based transfers.

#### Rationale

Ethscriptions can be transferred to any address, which means smart contracts can own them. However, smart contracts cannot currently transfer or create ethscriptions themselves.

This inhibits the creation of protocol-native apps that require smart contracts, such as marketplaces. Further, it makes the protocol difficult to use for smart contract wallet users.

This proposal lays out a simple and low gas mechanism for enabling smart contracts to transfer ethscriptions, with ethscription creation to follow soon.

Indexing these events across all contracts increases the burden of operating an indexer, but this extra cost is incremental given that indexers must inspect the calldata of every transaction anyway.

#### Notes

A previous version of this proposal included an additional smart contract event for Ethscription creation:

```solidity
CreateEthscription(
  address indexed initialOwner,
  string dataURI
)
```

However I think the need for this event is smaller and will consider it in a different proposal so as to maintain as much simplicity as possible and being as deliberate as possible in making changes


# ESIP-2: Safe Trustless Smart Contract Ethscription Escrow

Discuss and offer feedback [in this GitHub Issue](https://github.com/ethscriptions-protocol/ESIPs/issues/3).

## Abstract

This proposal introduces ESIP-2, an enhancement to the Ethscriptions Protocol that enables smart contracts to safely and trustlessly escrow Ethscriptions.

ESIP-2 accomplishes this by offering a mechanism for conditional transfers, relieving contracts from the requirement to identify the depositor of a given Ethscription.

## Specification

Add a new smart contract event into the Ethscriptions Protocol:

```solidity
event ethscriptions_protocol_TransferEthscriptionForPreviousOwner(
    address indexed previousOwner,
    address indexed recipient,
    bytes32 indexed ethscriptionId
);
```

When a contract emits this event, the protocol should register a valid ethscription transfer from the emitting contract to `recipient` of the ethscription with id `ethscriptionId`, provided:

1. The emitting contract owns the ethscription with id `ethscriptionId` when it emits the event.
2. The ethscription's previous owner was `previousOwner` as defined below.

An ethscription's "current owner" is the address that is in the "to" of the most recent valid transfer of that ethscription.

An ethscription's "previous owner" is the address that is in the "from" of the most recent valid transfer.

"Previous owner" doesn't necessarily mean "previous unique owner." For example, if you transfer an ethscription to me and then I transfer it to myself, I will be both the "current owner" and the "previous owner."

#### Implementation Guidelines

After ESIP-2, a valid ethscription transfer must have two properties:

1. Its "from" must equal the "to" of the previous valid transfer
2. Transfers sent under ESIP-2 will have an "enforced previous owner." In the case one exists, the enforced previous owner must equal the "from" of the previous valid transfer.

Below is an example of how an Ethscription transfers could be validated after the implementation of ESIP-2:

```javascript
const _ = require('lodash');

function validTransfers(ethscriptionTransfers) {
  const sorted = _.sortBy(
    ethscriptionTransfers,
    ['blockNumber', 'transactionIndex', 'transferIndex']
  );

  const valid = [];
  
  for (const transfer of sorted) {
    const lastValid = valid[valid.length - 1];
    const basicRulePasses = valid.length === 0 || transfer.from === lastValid.to;
    const previousOwnerRulePasses =
      transfer.enforcedPreviousOwner === null || 
      transfer.enforcedPreviousOwner === (lastValid?.from || null);

    if (basicRulePasses && previousOwnerRulePasses) {
      valid.push(transfer);
    }
  }

  return valid;
}
```

## Rationale

ESIP-2 is formulated primarily to enable smart contracts to safely escrow ethscriptions.

The idea of the smart contract escrow is that you send an ethscription to a smart contract, and, though that ethscription is owned by the smart contract, you retain some power over it—typically the ability to withdraw it and the ability to instruct the smart contract to send it to someone else.

Marketplaces are a common use-case for smart contract ethscription escrow. Because it is currently not possible for people to give smart contracts approval to transfer their ethscriptions, in order to list an ethscription for sale it must be transferred to the marketplace contract first.

With the introduction of `ethscriptions_protocol_TransferEthscription` in ESIP-1, smart contracts have the capability send and receive ethscriptions and function as marketplaces / escrows. However with just ESIP-1, smart contracts cannot obtain the information required to function as **safe** escrows without additional help.

The purpose of ESIP-2 is to enable smart contracts to overcome this limitation.

#### Who is the Depositor?

As an escrow, a smart contract should act to the benefit of the depositor of a given ethscription. However, because smart contracts cannot access ethscription ownership information, contracts cannot determine who deposited a given ethscription.

For example, if Alice and Bob both send a transaction to a smart contract with calldata `0xb1bdb91f010c154dd04e5c11a6298e91472c27a347b770684981873a6408c11c`, the smart contract can recognize this as a potential deposit, but it cannot know which (if either) of Alice or Bob's transactions is a legitimate deposit.

Because the contract can't determine the ethscription's depositor, it cannot determine who should have the power to control the ethscription once deposited. For example, the smart contract cannot determine who should have the power to withdraw the ethscription.

Because a smart contract cannot distinguish between Alice and Bob's deposits, it might treat them equally, leading to this exploit:

1. Alice "Deposits" id 0x123
2. Bob Deposits id 0x123
3. (Bob's deposit is real, Alice's isn't)
4. Alice requests a withdraw
5. Contract emits `ethscriptions_protocol_TransferEthscription(Alice, 0x123)`
6. Bob requests a withdraw
7. Contract emits `ethscriptions_protocol_TransferEthscription(Bob, 0x123)`

Alice owns the ethscription after (5) and the transfer in (7) fails.

#### Giving Contracts More Information

The most straightforward way to avoid this exploit is to require a trusted third party to confirm which deposits are valid.

For example, after you deposited id 0x123 you could go to the trusted party, ask them to verify it was your deposit that caused the contract to own id 0x123, and create a signed message memorializing this information.

Then you could present this signed message to the escrow contract to prove your deposit was legitimate. The contract would know to believe your message by comparing the signer of the message to the address of the trusted party.

Finally, when deposits are pending confirmation, they cannot be withdrawn, because the contract doesn’t know who should have the ability to do so.

Here's how the exploit would be foiled using this approach:

1. Alice "Deposits" id 0x123
2. Bob Deposits id 0x123
3. Smart Contract Freezes Assets
4. Third Party informs "Bob is real depositor"
5. Alice requests a withdraw
6. Contracts does nothing
7. Bob requests a withdraw
8. Contract emits `ethscriptions_protocol_TransferEthscription(Bob, 0x123)`

This solution works, but if the third party is not available it will be impossible for anyone to withdraw their assets. It would be preferable to have a decentralized alternative.

#### Reducing Contract Informational Needs

If a contract doesn't itself have a piece of information, it is not possible to deliver that information to the contract in a trustless fashion. Because of this, trustless solutions for contract escrow involve reducing the information a contract requires to make correct decisions, rather than supplying the contract with inaccessible information.

Specifically, ESIP-2 creates a mechanism for smart contracts to act in the interests of a depositor without having to know who that depositor is. Contracts achieve this through conditional transfers. Instead absolute transfers like "Send 0x123 to Alice," contracts can say "Send 0x123 to Alice, *if and only if* Alice deposited 0x123."

Now the potential exploit looks like this:

1. Alice "Deposits" id 0x123
2. Bob Deposits id 0x123
3. (Bob's deposit is real, Alice's isn't)
4. Alice requests a withdraw
5. Contracts emits `ethscriptions_protocol_TransferEthscriptionForPreviousOwner(Alice, Alice, 0x123)`
6. Bob requests a withdraw
7. Contract emits `ethscriptions_protocol_TransferEthscriptionForPreviousOwner(Bob, Bob, 0x123)`

With ESIP-2 the contract doesn't have to gather the information necessary to determine which of Alice and Bob's withdrawal requests are legitimate and to change its behavior accordingly.

Instead, the contract does the same thing for Alice's withdraw as it does for Bob's. However, because `TransferEthscriptionForPreviousOwner` is only valid when Alice is the legitimate previous owner—which she cannot be here as her deposit is invalid—this transfer is invalid under the protocol and, like all invalid transfers, will be ignored by indexers.

The goal is to make smart contracts "dumber." Instead of smart contracts having to decide which user requests to ignore based on different user permissions, the smart contract can treat all user requests the same, knowing that the invalid requests will be filtered out at the protocol level.

## Example Smart Contract

This is an example implementation of an `EthscriptionsEscrower` base contract that a marketplace can inherit from.

For example, a marketplace would call something like `_transferEthscription(seller, msg.sender, ethscriptionId)` in the "buy" function.

In addition to ESIP-2 it contains an additional best practice of an enforced 5 block cooldown period between transfers to account for potential indexer delays and reorgs.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

library EthscriptionsEscrowerStorage {
    struct Layout {
        mapping(address => mapping(bytes32 => uint256)) ethscriptionReceivedOnBlockNumber;
    }

    bytes32 internal constant STORAGE_SLOT =
        keccak256('ethscriptions.contracts.storage.EthscriptionsEscrowerStorage');

    function s() internal pure returns (Layout storage l) {
        bytes32 slot = STORAGE_SLOT;
        assembly {
            l.slot := slot
        }
    }
}

contract EthscriptionsEscrower {
    error EthscriptionNotDeposited();
    error EthscriptionAlreadyReceivedFromSender();
    error InvalidEthscriptionLength();
    error AdditionalCooldownRequired(uint256 additionalBlocksNeeded);
    
    event ethscriptions_protocol_TransferEthscriptionForPreviousOwner(
        address indexed previousOwner,
        address indexed recipient,
        bytes32 indexed id
    );
    
    event PotentialEthscriptionDeposited(
        address indexed owner,
        bytes32 indexed potentialEthscriptionId
    );
    
    event PotentialEthscriptionWithdrawn(
        address indexed owner,
        bytes32 indexed potentialEthscriptionId
    );
    
    uint256 public constant ETHSCRIPTION_TRANSFER_COOLDOWN_BLOCKS = 5;
    
    function _transferEthscription(address previousOwner, address to, bytes32 ethscriptionId) internal virtual {
        _validateTransferEthscription(previousOwner, to, ethscriptionId);
        
        emit ethscriptions_protocol_TransferEthscriptionForPreviousOwner(previousOwner, to, ethscriptionId);
        
        _afterTransferEthscription(previousOwner, to, ethscriptionId);
    }
    
    function withdrawEthscription(bytes32 ethscriptionId) public virtual {
        _transferEthscription(msg.sender, msg.sender, ethscriptionId);
        
        emit PotentialEthscriptionWithdrawn(msg.sender, ethscriptionId);
    }
    
    function _onPotentialEthscriptionDeposit(address previousOwner, bytes memory userCalldata) internal virtual {
        if (userCalldata.length != 32) revert InvalidEthscriptionLength();
        
        bytes32 potentialEthscriptionId = abi.decode(userCalldata, (bytes32));
        
        if (userEthscriptionPossiblyStored(previousOwner, potentialEthscriptionId)) {
            revert EthscriptionAlreadyReceivedFromSender();
        }

        EthscriptionsEscrowerStorage.s().ethscriptionReceivedOnBlockNumber[previousOwner][potentialEthscriptionId] = block.number;
        
        emit PotentialEthscriptionDeposited(previousOwner, potentialEthscriptionId);
    }
    
    function _validateTransferEthscription(
        address previousOwner,
        address to,
        bytes32 ethscriptionId
    ) internal view virtual {
        if (userEthscriptionDefinitelyNotStored(previousOwner, ethscriptionId)) {
            revert EthscriptionNotDeposited();
        }
        
        uint256 blocksRemaining = blocksRemainingUntilValidTransfer(previousOwner, ethscriptionId);
        
        if (blocksRemaining != 0) {
            revert AdditionalCooldownRequired(blocksRemaining);
        }
    }
    
    function _afterTransferEthscription(
        address previousOwner,
        address to,
        bytes32 ethscriptionId
    ) internal virtual {
        delete EthscriptionsEscrowerStorage.s().ethscriptionReceivedOnBlockNumber[previousOwner][ethscriptionId];
    }
    
    function blocksRemainingUntilValidTransfer(
        address previousOwner,
        bytes32 ethscriptionId
    ) public view virtual returns (uint256) {
        uint256 receivedBlockNumber = EthscriptionsEscrowerStorage.s().ethscriptionReceivedOnBlockNumber[previousOwner][ethscriptionId];
        
        if (receivedBlockNumber == 0) {
            revert EthscriptionNotDeposited();
        }
        
        uint256 blocksPassed = block.number - receivedBlockNumber;
        
        return blocksPassed < ETHSCRIPTION_TRANSFER_COOLDOWN_BLOCKS ?
            ETHSCRIPTION_TRANSFER_COOLDOWN_BLOCKS - blocksPassed :
            0;
    }
    
    function userEthscriptionDefinitelyNotStored(
        address owner,
        bytes32 ethscriptionId
    ) public view virtual returns (bool) {
        return EthscriptionsEscrowerStorage.s().ethscriptionReceivedOnBlockNumber[owner][ethscriptionId] == 0;
    }
    
    function userEthscriptionPossiblyStored(
        address owner,
        bytes32 ethscriptionId
    ) public view virtual returns (bool) {
        return !userEthscriptionDefinitelyNotStored(owner, ethscriptionId);
    }
    
    fallback() external virtual {
        _onPotentialEthscriptionDeposit(msg.sender, msg.data);
    }
}
```


# ESIP-3: Smart Contract Ethscription Creations

## Abstract

ESIP-3 introduces a mechanism for smart contracts to create ethscriptions using Ethereum events. Start block: `18130000`

## Specification

Add a new smart contract event into the Ethscriptions Protocol:

```solidity
event ethscriptions_protocol_CreateEthscription(
    address indexed initialOwner,
    string contentURI
);
```

When a contract emits this event in or after block `18130000`, the protocol should register a valid ethscription creation attempt with:

1. `contentURI` interpreted as the ethscription's utf-8 encoded dataURI with all null bytes removed.
2. `initialOwner` as the created ethscription's initial owner.
3. The emitting contract as the creator.

Functionally speaking, this event is the equivalent of an EOA hex-encoding `contentURI` and putting it in the calldata of an Ethereum transaction from itself to `initialOwner`. As with ethscriptions created via input data, all null bytes are removed from the UTF8 `contentURI` of ethscriptions created through events.

As with EOA-initiated ethscription creations, ESIP-3 ethscription creations are only valid if `contentURI` is both unique and [a syntactically valid dataURI](https://docs.ethscriptions.com/overview/how-ethscriptions-work#how-to-validate-a-datauri).

#### Example `contentURI` format

`data:,1234`.

Note: it is utf-8 encoded, *not* hex-encoded. Note also this specific example is a duplicate and would not result in an ethscription creation.

#### Ethscriptions and Ethereum Transactions remain 1-1

ESIP-3 does **not** change the fact that each Ethereum transaction may have only one corresponding ethscription. If multiple aspects of a transaction constitute valid ethscription creations, calldata will be prioritized over events, and events with lower log indices will be prioritized over those with higher indices.

Example 1:

1. Calldata: valid creation
2. Event Log Index 1: valid creation
3. Event Log Index 2: valid creation

In this case, an ethscription will be created according to the calldata and Events 1 and 2 will be ignored.

Example 2:

1. Calldata: empty (i.e., invalid creation)
2. Event Log Index 1: valid creation
3. Event Log Index 2: valid creation

Here, Event 1's log will trigger the ethscription creation. If calldata and Event 1 were both invalid then Event 2's log would trigger the ethscription creation.

## Rationale

Contracts must have the same powers as EOAs and this is the cheapest way to do it.

We propose maintaining the 1-1 correspondence between ethscriptions and Ethereum transactions because the convention that `ethscriptionId` = `transactionHash` has proven useful.

Multiple ethscriptions in a transaction are also an inefficient way of capturing a user's intent. Creating multiple ethscriptions in a transaction will always have an underlying purpose and structure, and we should be capturing this structure using [ESIP-4](https://docs.ethscriptions.com/esips/esip-4-the-ethscriptions-virtual-machine).

For example, instead of forcing a user to bulk create ethscriptions of this form:

```
data:,{"p":"erc-20","op":"mint","tick":"fair","id":"17560","amt":"1000"}
data:,{"p":"erc-20","op":"mint","tick":"fair","id":"17561","amt":"1000"}
data:,{"p":"erc-20","op":"mint","tick":"fair","id":"17562","amt":"1000"}
data:,{"p":"erc-20","op":"mint","tick":"fair","id":"17563","amt":"1000"}
data:,{"p":"erc-20","op":"mint","tick":"fair","id":"17564","amt":"1000"}
data:,{"p":"erc-20","op":"mint","tick":"fair","id":"17565","amt":"1000"}
data:,{"p":"erc-20","op":"mint","tick":"fair","id":"17566","amt":"1000"}
...
```

We should capture the user's intent with a single ethscription containing the command `mint(50)`.

#### File size

Unlike calldata, events have no size limit (aside from the 30M block gas limit). Practically this means that ESIP-3 expands ethscriptions' file size limit to beyond 3.5MB.


# ESIP-5: Bulk Ethscription Transfers from EOAs

## Abstract

Bulk transferring means transferring more than one ethscription in a single Ethereum transaction.

ESIP-1 and ESIP-2 gave Smart Contracts the ability to transfer ethscriptions through events. Because multiple events can be emitted in a single transaction, ESIP-1 and ESIP-2 also gave Smart Contracts the ability to bulk transfer ethscriptions.

ESIP-5 brings EOAs to the level of Smart Contracts by introducing a mechanism for EOAs to bulk transfer ethscriptions.

## Specification

Pre-ESIP-5, this was the rule for EOAs transferring an ethscription:

> Any Ethereum transaction whose input data is an ethscription id \[...] is a valid Ethscription transfer, provided the transaction sender is the Ethscription’s owner.

ESIP-5 retains the spirit of this rule, but allows users to add multiple ordered ethscription ids as input data.

If the input data of a transaction (without its leading `0x`) is a sequence of 1 or more valid ethscription ids (without their leading `0x`), that transaction will constitute a valid transfer for each ethscription that is owned by the transaction's creator.

#### An Example

Suppose a transaction has the input data:

{% code overflow="wrap" %}

```
0x8ad5dc6c7a6133eb1c42b2a1443125b57913c9d63376825e310e4d1222a91e24533c5e38d1b8bf75166bd6443a443cd25bd36c087e1a5b8b0881b388fa1a942c
```

{% endcode %}

We first remove the leading `0x`:

{% code overflow="wrap" %}

```
8ad5dc6c7a6133eb1c42b2a1443125b57913c9d63376825e310e4d1222a91e24533c5e38d1b8bf75166bd6443a443cd25bd36c087e1a5b8b0881b388fa1a942c
```

{% endcode %}

Now we observe that this hex string's length is 128, which is an even multiple of 64, which is the length of an ethscription id with its leading `0x` removed.

Now we split the hex string into two chunks of 64 characters and determine whether these chunks are valid ethscription ids. We prepend the `0x` and check for ethscriptions. Now we find:

1. `0x8ad5dc6c7a6133eb1c42b2a1443125b57913c9d63376825e310e4d1222a91e24` is [Ethscription #2](https://ethscriptions.com/ethscriptions/0x8ad5dc6c7a6133eb1c42b2a1443125b57913c9d63376825e310e4d1222a91e24).
2. `0x533c5e38d1b8bf75166bd6443a443cd25bd36c087e1a5b8b0881b388fa1a942c` is [Ethscription #1](https://ethscriptions.com/ethscriptions/0x533c5e38d1b8bf75166bd6443a443cd25bd36c087e1a5b8b0881b388fa1a942c).

Because both ids correspond to valid ethscriptions, we proceed. If one or more weren't valid ethscriptions, we would ignore the invalid ethscriptions and continue processing.

Now we look at the ids in the order they were listed in calldata, and register in order:

1. A valid transfer for `0x8ad5dc6c7a6133eb1c42b2a1443125b57913c9d63376825e310e4d1222a91e24` if the "from" of the transaction is the owner of this ethscription as of this moment.
2. A valid transfer for `0x533c5e38d1b8bf75166bd6443a443cd25bd36c087e1a5b8b0881b388fa1a942c` is "from" of the transaction is the owner of this ethscription as of this moment.

If the "from" of the transaction is *not* the owner of the ethscription, we skip that transfer and continue processing. This means that if the "from" is the owner on some, but not all, ethscriptions some of the transfers will be valid and some will not.

## Rationale

The goal is to reduce the question of bulk transferring to a sequence of individual transfers. This is why there can be partially valid bulk transfers—creating a notion of bulk validity is additional complexity.

However, we must enforce a notion of global validity for all ethscription ids, otherwise we introduce too much potential for unintentional transfers and confusion.


# ESIP-6: Opt-in Ethscription Non-uniqueness

## Abstract

Currently, only the first ethscription with a given content uri is valid.

For example, if there is an existing ethscription with content `data:,1234`, then no future ethscription can be created with this same content.

This mechanic was designed for the digital artifact use-case when it is valuable to know provenance. It also makes Ethscriptions content-addressable, allowing users to look up ownership and other metadata using only ethscription content.

However, uniqueness creates problems for use-cases where guaranteed delivery is necessary. For example, if two people are using Ethscriptions as a messaging protocol, they shouldn't have to worry about making each message globally unique.

This problem is more acute in the case of Smart Contract-created ethscriptions because while people can "try again" if their ethscription is a duplicate, Smart Contracts cannot "revert" in the case of ethscription creation failure.

For example, if a Smart Contract has collected money from a user in exchange for creating an ethscription, the Smart Contract cannot return this money if the creation fails.

ESIP-6 proposes a backwards-compatible mechanism to support all of these use-cases. By default, duplicate ethscriptions will continue to be invalid, as they are today. However, users will be able to modify dataURIs to "opt-in" to potential duplication on ethscriptions they create after this ESIP is live.

## Specification

#### Opt In Non-uniqueness

To opt in to potential duplication, a user must add a special "magic" parameter to their dataURI.

In a dataURI, parameters are strings that appear after the mimetype and before the optional "base64" and the start of the content. The most common use of parameters is to specify a character encoding for the dataURI as in this example:

`data:text/plain;charset=utf-8,hi`

In this dataURI, `charset` is a parameter and it has the value `utf-8`.

We will discuss the choice of magic parameter below, but for now let's assume it is `rule=esip6`.

If a user wants to mark an ethscription "okay to duplicate" they would add the parameter `rule=esip6` to their dataURI. For example:

`data:text/plain;charset=utf-8;rule=esip6,hi`

If there were no other parameter, it would look like this:

`data:text/plain;rule=esip6,hi`

Marking an ethscription "okay to duplicate" also guarantees that it will never be invalidated as a duplicate itself because any potential duplicate would also contain the parameter `rule=esip6` which marks *it* as "okay to duplicate."

#### Updated Indexer Behavior

To implement this ESIP, indexers must change their behavior. Here is how an indexer should determine if a new ethscription is valid.

1. Determine whether the ethscription's content is a valid dataURI. The rules for dataURI validity are **not** changing in this ESIP. Everything that was a valid dataURI previously is still valid, and everything that wasn't a valid dataURI is still invalid.
   1. If the ethscription has an invalid dataURI then it is an invalid ethscription. If it has a valid dataURI, proceed to step 2.<br>
2. Does the ethscription contain `rule=esip6` as a dataURI parameter?
   1. If yes, the ethscription is valid. If no, proceed to step 3.<br>
3. Does another ethscription created in an earlier block, or created in the same block but with an earlier transaction index, have the same content?
   1. If yes, the ethscription is invalid. If no, it is valid.

#### Parsing dataURI parameters

DataURI validity is defined by this Ruby regular expression:

```ruby
%r{
  data:
  (?<mediatype>
    (?<mimetype> .+? / .+? )?
    (?<parameters> (?: ; .+? = .+? )* )
  )?
  (?<extension>;base64)?
  ,
  (?<data>.*)
}x
```

Here is example code you can use to find the correct parameter using this regex:

```ruby
def is_esip6?(uri)
  match = REGEXP.match(uri)
  String(match[:parameters]).split(';').include?('rule=esip6')
end
```

#### Client Behavior

Ethscriptions clients are encouraged to indicate the presence of the `rule=esip6` parameter as well as the number of duplicates that exist for a specific `rule=esip6` ethscription.

Many clients display "Ethscription Numbers" that indicate the order in which a given ethscription was created. Clients are encouraged to continue assigning numbers to all valid ethscriptions, whether or not they include the `rule=esip6` parameter.

#### Smart Contract Behavior

Because Smart Contracts cannot "try again" in the case of duplicates, Smart Contracts should include the `rule=esip6` parameter in any scenario in which ethscription creation failure would lead to loss of funds or ethscriptions.

## Rationale

Ethscriptions cannot succeed as a general protocol without the ability to guarantee message delivery. If we can't rely on our ability to create ethscriptions, we can't rely on the creation of an ethscription to trigger something important, and this limits what we can use ethscriptions to do.

The immediate need for this ESIP comes from the fact that it is impossible to create a secure Ethscriptions VM bridge if Smart Contracts cannot reliably communicate with Dumb Contracts by creating ethscriptions.

However, this proposal is not restricted to Ethscription VM-related ethscriptions because the need for message delivery is more universal.

Why do it this way?

#### Why Not Change the Default to Allow Duplicates?

Even if this were a good change, it is too late to make.

We cannot change the default retroactively because people have relied on protocol rules to make important decisions and invalidating those decisions would irreparably damage trust in the protocol.

We also cannot change the default going forward because as we have seen this will still leave past ethscriptions un-duplicatable.

#### Front Running and Censorship

In the end, ESIP-6 isn't really about the ability to create duplicate ethscriptions.

It has always been possible to create "pseudo" duplicates of ethscriptions by varying parts of images that do not affect pixels but do affect the final bytes of the ethscription. It is also possible to "duplicate" a JSON object by creating a new object that shares keys and values but differs in some respect the JSON parser ignores.

Theoretically users could take advantage of this to ensure message delivery by creating a message that was a "pseudo" duplicate of an existing message but whose bytes were different.

Unfortunately, this fix does not work because of front running. Someone can always observe the ethscription you are creating and create the same one earlier in the same block. This ESIP gives users a method to create duplicates that cannot be censored by front runners and this is absolutely necessary for Ethscriptions to be the uncensorable protocol it was always intended to be.


# ESIP-7: Support Gzipped Calldata in Ethscription Creation

## Abstract

For EOAs, the cost of creating an ethscription is determined by the size of the calldata payload. Therefore, to save gas, it is crucial to make this payload as small as possible.

Often this happens "for free" via the ethscribing of file formats that implement native compression, such as PNG (though even here ethscribers must pay to encode as base64).

However most ethscription content is JSON, a format that has no native compression. Because of this, a protocol-level compression solution has the potential to unlock massive savings.

Specifically, the total size of all EOA-created ethscriptions is about 1.5gb. Gzipping these ethscriptions would reduce size by more than 500mb, **a massive 35% reduction**!

This is somewhat skewed by outliers but the median ethscription would be reduced in size by 14%, which is still significant.

For fun, [here is the most-compressible ethscription that currently exists](https://ethscriptions.com/ethscriptions/0xf50ca8aa758b9ff524b7f7805703beae28c505c6a1fc513370b1390c9ba62c4c). Were it gzipped it would be 150x smaller! Though as we'll see below, it is not a candidate for this ESIP.

## Specification

Users may gzip calldata payloads for ethscriptions created via transaction inputs. This ESIP does not apply to contract-created ethscriptions or ethscription transfers.

Indexers should recognize gzipped ethscriptions via the magic leading byte sequence `0x1F8B`. When such an ethscription is recognized it should be unzipped and then processed normally.

When queried, ethscriptions should be returned in their uncompressed form so that users and API consumers do not have to change behavior.

**Compression Ratio Limit**

To avoid [zip bomb attacks](https://en.wikipedia.org/wiki/Zip_bomb), gzipped calldata will only be valid if the compression ratio is less than or equal to 10x. For example, if the calldata is 10kb, it cannot decompress to more than 100kb, otherwise it is considered invalid.

When compressed, 99% of current ethscriptions would have a compression ratio of 3.85x or less, so a 10x limit should be plenty for all realistic use-cases.

**Reference Implementation**

Find a complete reference implementation for this ESIP in [this pull request](https://github.com/0xFacet/ethscriptions-indexer/pull/58). Here is the implementation for the compression ratio limit:

```ruby
module HexDataProcessor
  def self.hex_to_utf8(hex_string, support_gzip:)
    clean_hex_string = hex_string.gsub(/\A0x/, '')
    binary_data = hex_string_to_binary(clean_hex_string)
    
    if support_gzip && gzip_compressed?(binary_data)
      decompressed_data = decompress_with_ratio_limit(binary_data, 10)
    else
      decompressed_data = binary_data
    end
  
    return nil unless decompressed_data
    
    clean_utf8(decompressed_data)
  end

  def self.hex_string_to_binary(hex_string)
    ary = hex_string.scan(/../).map { |pair| pair.to_i(16) }
    ary.pack('C*')
  end

  def self.gzip_compressed?(data)
    data[0..1].bytes == [0x1F, 0x8B]
  end

  def self.decompress_with_ratio_limit(data, max_ratio)
    original_size = data.bytesize
    decompressed = StringIO.new

    Zlib::GzipReader.wrap(StringIO.new(data)) do |gz|
      while chunk = gz.read(16.kilobytes) # Read in chunks
        decompressed.write(chunk)
        if decompressed.length > original_size * max_ratio
          return nil # Exceeds compression ratio limit
        end
      end
    end

    decompressed.string
  rescue Zlib::Error
    nil
  end

  def self.clean_utf8(binary_data)
    utf8_string = binary_data.force_encoding('UTF-8')
    
    unless utf8_string.valid_encoding?
      utf8_string = utf8_string.encode('UTF-8', invalid: :replace, undef: :replace, replace: "\uFFFD")
    end
    
    utf8_string.delete("\u0000")
  end
end
```

## Rationale

UTF-8 dataURIs are a clear and intuitive transport mechanism for ethscription content. For example, this approach allows anyone to consume the Ethscriptions Protocol using only Etherscan.

However, we have matured as a protocol to the point that minimizing expense is a more dominant concern.

Gzipping is a backwards compatible approach that will be completely transparent to the end user and requires only localized changes to indexers. Even with a "slow" language like Ruby, unzipping is extremely fast: on the order of 1ms for typical ethscription payloads.


# ESIP-8: Ethscription Attachments aka "BlobScriptions"

### Links <a href="#abstract" id="abstract"></a>

* [Reference Implementation](https://github.com/0xFacet/ethscriptions-indexer/pull/60)
* [ESIP-8 Discussion](https://github.com/ethscriptions-protocol/ESIP-Discussion/issues/17)

### Abstract <a href="#abstract" id="abstract"></a>

The introduction of blobs in EIP-4844 enables anyone to store data on Ethereum for 10x to 100x cheaper than calldata. This comes at a cost, however: the Ethereum protocol doesn't guarantee the availability of blob data for more than 18 days.

However, on a practical level it is not clear how burdensome this limitation will be. Because L2s use blobs to store transaction data there will be strong incentives to create publicly accessible archives of blob data to enhance the transparency and auditability of Layer 2s.

Also, like IPFS, blob data is completely decentralized—as long as one person has blob data it can be verified and used by anyone.

This ESIP proposes using blobs to store data within the Ethscriptions Protocol. We presuppose the ready availability of blob data and require indexers to store or find user blob data along with the other blockchain data the Ethscriptions Protocol currently uses.

Specifically, ESIP-8 proposes a new "sidecar" **attachment** field for Ethscriptions that is composed from the data in one or more blobs. This field is in addition to the existing **content** field.

The name "Ethscription Attachment" is preferred over "Ethscription Blob" (or similar) because transactions can have multiple blobs, but ethscriptions can only have one attachment (that is composed of all the blobs together).

### An Example <a href="#specification" id="specification"></a>

Consider the ethscription created by [this Sepolia transaction](https://sepolia.etherscan.io/tx/0x5d04d632d3affef95b0ae141f2b5b5af474ab80a24925672bdd551637990054b). The transaction's calldata contains the hex data `0x646174613a2c68656c6c6f2066726f6d20457468736372697074696f6e2063616c6c6461746121` which corresponds to the dataURI "data:,hello from Ethscription calldata!" which becomes the ethscription's content.

[The transaction's blobs](https://sepolia.etherscan.io/tx/0x5d04d632d3affef95b0ae141f2b5b5af474ab80a24925672bdd551637990054b#blobs), when interpreted according to the rules described below, contains the data for this image which becomes the ethscription's attachment:

<div align="center"><figure><img src="/files/Lpiod0pOD5zVz9M7wfeg" alt="" width="375"><figcaption></figcaption></figure></div>

### Specification <a href="#specification" id="specification"></a>

All new ethscriptions have an optional `attachment` field. If an ethscription is created in a transaction with no blobs this field will be `null`.

If an ethscription's creation transaction does include blobs *and* the ethscription was created via calldata (i.e., not via an event emission), its blobs are concatenated and interpreted as an untagged [CBOR](https://cbor.io/) object (as defined by [RFC 8949](https://datatracker.ietf.org/doc/html/rfc8949)) that decodes into a hash with *exactly* these keys:

* `content`
* `contentType`

If the concatenated data is a valid CBOR object, and that object decodes into a hash with exactly those two fields, an attachment for the ethscription is created.

The case in which the blobs are invalid and an attachment is *not* created is handled identically to the case in which there are no blobs at all. I.e., the ethscription is still created if it's otherwise valid, just with no attachment.

Note:

* There is no uniqueness requirement for the attachment's content and/or contentType.
* Attachment `content`, `contentType`, and the container CBOR object itself can each be optionally gzipped **with a maximum compression ratio of 10x**.
* The attachment is **not** valid if:
  * If the CBOR object has a tag
  * If the decoded object his not a hash
  * If the decoded hash's keys aren't exactly `content` and `contentType`. There cannot be extra keys.
  * The values of `content` and `contentType` aren't both strings (either binary or UTF-8).

When such an attachment exists, the indexer's API must include the path for retrieving it in an `attachment_path` field in the JSON representation of an ethscription with at most a one block delay between ethscription creation and inclusion of the URL. For example, if an ethscription is created in block 15, the attachment\_url must appear no later than block 17.

The attachment\_url field will be available *in addition* to the `content_uri` field.

#### `contentType` Max Length

To enable performant filtering by `contentType`, indexers must only store the first 1,000 characters of the user-submitted content type. Content types of more than 1,000 characters will be truncated, but the attachment will still be valid.

#### Creating an Ethscription Attachment in Javascript

You can use the `cbor` package and Viem's `toBlobs`:

```typescript
const { toBlobs } = require('viem');
const fs = require('fs');
const cbor = require('cbor');

const imagePath = '/whatever.gif'
const imageData = fs.readFileSync(imagePath);

const dataObject = {
  contentType: 'image/gif',
  content: imageData
};

const cborData = cbor.encode(dataObject);
const blobs = toBlobs({ data: cborData });
```

#### **Getting Blob Data**

Blob data is available on a block-level through the `blob_sidecars` API endpoint available on Ethereum Beacon nodes. If you don't want to run a node yourself, [you can use Quicknode](https://www.quicknode.com/docs/ethereum/eth-v1-beacon-blob_sidecars-id).

The input to this function is a "block id," which is a slot number (not block number) or block root. Block roots are available on normal Ethereum API requests, but only for the *previous* block (the field is `parentBeaconBlockRoot`).

This means that attachments must be populated on a one block delay.

#### **Associating Blobs with Ethereum Transactions**

Because the Beacon API only provides blob information on a block level, it requires some additional logic to match blobs to the transactions that created them. Fortunately, transactions now have a `blobVersionedHashes` field that can be computed from the `kzg_commitments` field on the block-level blob data.

Here's an example implementation (it's `O(n^2)` but the numbers involved are small)

```ruby
def transaction_blobs
  blob_versioned_hashes.map do |version_hash|
    blob_from_version_hash(version_hash)
  end
end

def blob_from_version_hash(version_hash)
  block_blob_sidecars.find do |blob|
    kzg_commitment = blob["kzg_commitment"].sub(/\A0x/, '')
    binary_kzg_commitment = [kzg_commitment].pack("H*")
    sha256_hash = Digest::SHA256.hexdigest(binary_kzg_commitment)
    modified_hash = "0x01" + sha256_hash[2..-1]
    
    version_hash == modified_hash
  end
end
```

#### Converting Blob Content to an Attachment

At a high-level we use normalize the blob data, concatenate it, and CBOR-decode it. However blobs have a few interesting quirks that make this more challenging:

* Currently blobs have a minimum length of 128kb. If your data is smaller than that you'll have to pad it (probably will null bytes) to the full length.
* Blobs are composed of "segments" of 32 bytes, none of which, when interpreted as an integer, can exceed the value of the cryptography-related "BLS modulus", which is 52435875175126190479447740508185965837690552500527637822603658699938581184513.

So if you want to use blobs you need a protocol for communicating where the data ends and a mechanism for ensuring no 32 byte segment is too large.

Here Ethscriptions will follow [Viem's approach](https://github.com/wevm/viem/blob/main/src/utils/blob/toBlobs.ts):

* Left-pad each segment with a null byte. A `0x00` in the most significant byte ensures no segment can be larger than the BLS modulus.
* End the content of every blob with `0x80`, which, when combined with the rule above, provides an unambiguous way to determine the length of the data in the blob.

When a blob creator follows these rules (or just use's Viem's [`toBlobs`](https://viem.sh/docs/utilities/toBlobs#toblobs)), you can decode it into bytes by using Viem's [`fromBlobs`](https://viem.sh/docs/utilities/fromBlobs#fromblobs). There is a Ruby implementation as well in the appendix.

Once you decode the blob you can create an attachment using something like this class.

```ruby
class EthscriptionAttachment < ApplicationRecord
  class InvalidInputError < StandardError; end
  
  has_many :ethscriptions,
    foreign_key: :attachment_sha,
    primary_key: :sha,
    inverse_of: :attachment
  
  delegate :ungzip_if_necessary!, to: :class
  attr_accessor :decoded_data
  
  def self.from_eth_transaction(tx)
    blobs = tx.blobs.map{|i| i['blob']}
    
    cbor = BlobUtils.from_blobs(blobs: blobs)

    from_cbor(cbor)
  end
  
  def self.from_cbor(cbor_encoded_data)
    cbor_encoded_data = ungzip_if_necessary!(cbor_encoded_data)
    
    decoded_data = CBOR.decode(cbor_encoded_data)
    
    new(decoded_data: decoded_data)
  rescue EOFError, *cbor_errors => e
    raise InvalidInputError, "Failed to decode CBOR: #{e.message}"
  end
  
  def decoded_data=(new_decoded_data)
    @decoded_data = new_decoded_data
    
    validate_input!
    
    self.content = ungzip_if_necessary!(decoded_data['content'])
    self.content_type = ungzip_if_necessary!(decoded_data['contentType'])
    self.size = content.bytesize
    self.sha = calculate_sha
    
    decoded_data
  end
  
  def calculate_sha
    combined = [
      Digest::SHA256.hexdigest(content_type),
      Digest::SHA256.hexdigest(content),
    ].join
    
    "0x" + Digest::SHA256.hexdigest(combined)
  end
  
  def self.ungzip_if_necessary!(binary)
    HexDataProcessor.ungzip_if_necessary(binary)
  rescue Zlib::Error, CompressionLimitExceededError => e
    raise InvalidInputError, "Failed to decompress content: #{e.message}"
  end
  
  private
  
  def validate_input!
    unless decoded_data.is_a?(Hash)
      raise InvalidInputError, "Expected data to be a hash, got #{decoded_data.class} instead."
    end
    
    unless decoded_data.keys.to_set == ['content', 'contentType'].to_set
      raise InvalidInputError, "Expected keys to be 'content' and 'contentType', got #{decoded_data.keys} instead."
    end
    
    unless decoded_data.values.all?{|i| i.is_a?(String)}
      raise InvalidInputError, "Invalid value type: #{decoded_data.values.map(&:class).join(', ')}"
    end
  end
  
  def self.cbor_errors
    [CBOR::MalformedFormatError, CBOR::UnpackError, CBOR::StackError, CBOR::TypeError]
  end
end

```

#### Hashing Attachments

It's useful to be able to generate a unique hash of an Ethscription Attachment in order for indexers to avoid storing duplicate data and for users to determine which other ethscriptions have the same attachment. This can be done in many ways, but to promote uniformity ESIP-8 defines this canonical method of hashing Ethscription Attachments:

1. Compute the sha256 hash of the attachment's *ungzipped* `contentType` and `content` fields.
2. Remove the leading `0x` if present.
3. Concatenate the hex string representations of the hashes with the `contentType` hash first.
4. Hash this concatenated string and add a `0x` prefix.

Here is a Javascript implementation:

```typescript
import { sha256, stringToBytes } from 'viem';

const attachment = {
  contentType: 'text/plain',
  content: 'hi',
};

const contentTypeHash = sha256(stringToBytes(attachment.contentType));
const contentHash = sha256(stringToBytes(attachment.content));

const combinedHash =
  contentTypeHash.replace(/^0x/, '') + contentHash.replace(/^0x/, '');

const finalHash = sha256(combinedHash as `0x${string}`);
```

And a Ruby implementation:

```ruby
require 'digest'

attachment = {
  'contentType' => 'text/plain',
  'content' => 'hi',
}

content_type_hash = Digest::SHA256.hexdigest(attachment['contentType'])
content_hash = Digest::SHA256.hexdigest(attachment['content'])

combined_hash = content_type_hash + content_hash

final_hash = "0x" + Digest::SHA256.hexdigest(combined_hash)
```

#### Appendix: Ruby `BlobUtils`

```ruby
module BlobUtils
  # Constants from Viem
  BLOBS_PER_TRANSACTION = 2
  BYTES_PER_FIELD_ELEMENT = 32
  FIELD_ELEMENTS_PER_BLOB = 4096
  BYTES_PER_BLOB = BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB
  MAX_BYTES_PER_TRANSACTION = BYTES_PER_BLOB * BLOBS_PER_TRANSACTION - 1 - (1 * FIELD_ELEMENTS_PER_BLOB * BLOBS_PER_TRANSACTION)

  # Error Classes
  class BlobSizeTooLargeError < StandardError; end
  class EmptyBlobError < StandardError; end
  class IncorrectBlobEncoding < StandardError; end

  # Adapted from Viem
  def self.to_blobs(data:)
    raise EmptyBlobError if data.empty?
    raise BlobSizeTooLargeError if data.bytesize > MAX_BYTES_PER_TRANSACTION
    
    if data =~ /\A0x([a-f0-9]{2})+\z/i
      data = [data].pack('H*')
    end

    blobs = []
    position = 0
    active = true

    while active && blobs.size < BLOBS_PER_TRANSACTION
      blob = []
      size = 0

      while size < FIELD_ELEMENTS_PER_BLOB
        bytes = data.byteslice(position, BYTES_PER_FIELD_ELEMENT - 1)

        # Push a zero byte so the field element doesn't overflow
        blob.push(0x00)

        # Push the current segment of data bytes
        blob.concat(bytes.bytes) unless bytes.nil?

        # If the current segment of data bytes is less than 31 bytes,
        # stop processing and push a terminator byte to indicate the end of the blob
        if bytes.nil? || bytes.bytesize < (BYTES_PER_FIELD_ELEMENT - 1)
          blob.push(0x80)
          active = false
          break
        end

        size += 1
        position += (BYTES_PER_FIELD_ELEMENT - 1)
      end

      blob.fill(0x00, blob.size...BYTES_PER_BLOB)
      
      blobs.push(blob.pack('C*').unpack1("H*"))
    end

    blobs
  end
  
  def self.from_blobs(blobs:)
    concatenated_hex = blobs.map do |blob|
      hex_blob = blob.sub(/\A0x/, '')
      
      sections = hex_blob.scan(/.{64}/m)
      
      last_non_empty_section_index = sections.rindex { |section| section != '00' * 32 }
      non_empty_sections = sections.take(last_non_empty_section_index + 1)
      
      last_non_empty_section = non_empty_sections.last
      
      if last_non_empty_section == "0080" + "00" * 30
        non_empty_sections.pop
      else
        last_non_empty_section.gsub!(/80(00)*\z/, '')
      end
      
      non_empty_sections = non_empty_sections.map do |section|
        unless section.start_with?('00')
          raise IncorrectBlobEncoding, "Expected the first byte to be zero"
        end
        
        section.delete_prefix("00")
      end
      
      non_empty_sections.join
    end.join
    
    [concatenated_hex].pack("H*")
  end
end

```


# Draft ESIPs


# API Overview

## [Learn about the free ethscriptions API here!](https://api-docs.ethscriptions.com/reference)


# Floored Ape Ethscribe

Ethscribe with compression to save cost!

## <https://flooredape.io/ethscribe>

<figure><img src="/files/362bJ2RzWvTm5JXbgcy8" alt=""><figcaption></figcaption></figure>


# Ethscriber.xyz

A great tool for ethscribing text!

## <https://ethscriber.xyz/>

<figure><img src="/files/BXNBLyE3WRO67FxDfuhi" alt=""><figcaption></figcaption></figure>


# THESE DOCS HAVE MOVED

To <https://docs.facet.org/>

Everything below is left purely for historical interest. It is all deprecated.


# Welcome to Ethscriptions VM!

### Introduction

On Aug 7, 2023 we proposed the Ethscriptions Virtual Machine (ESC VM), a new protocol built on top of Ethscriptions. The purpose of the ESC VM is to enhance the functionality and scope of the Ethscriptions Protocol by enabling it to function as a general computation engine.

Users access the VM by creating special ethscriptions that the VM interprets as computer commands to special computer programs called Dumb Contracts.

Read the rest of the proposal here:

{% embed url="<https://docs.ethscriptions.com/esips/esip-4-the-ethscriptions-virtual-machine>" %}

### <mark style="background-color:green;">The first Ethscriptions VM implementation is now Open Source</mark>

In addition to deploying and calling existing Dumb Contracts, you can use this technology to create your own Dumb Contracts **on Goerli**.

Our Ethscriptions VM implementation has two components:

1. [Ethscriptions VM API](https://github.com/ethscriptions-protocol/ethscriptions-vm-api): This is a Ruby on Rails app that ingests ethscriptions from an Ethscriptions indexer, executes Dumb Contract logic, and stores the result. All Dumb Contracts live in this app.<br>
2. [Ethscriptions VM Client](https://github.com/ethscriptions-protocol/ethscriptions-vm-client): This is a Next app that you can use to try out Dumb Contracts. Think of it a little like Etherscan. You can connect it to your local VM API instance or hook it up our VM API instance at <https://goerli-api.ethscriptionsvm.com>.

You can also try out a hosted version of the app on <https://goerli.ethscriptionsvm.com>.

### What do I do now?

These apps are not yet production-ready. We are launching them early in beta form to get feedback from the community so we can go live on mainnet as quickly as possible.

So please help us! Test the VM and submit feedback in GitHub Issues on each repo:

* [VM Client](https://github.com/ethscriptions-protocol/ethscriptions-vm-client/issues)
* [VM API](https://github.com/ethscriptions-protocol/ethscriptions-vm-api/issues)

The other thing you can do is to write Dumb Contracts! All Dumb Contracts will be contributed by the community so we are looking out for great submissions.

If you have one, or you have any proposal for changing the apps, please submit a pull request!


# Introduction

Rubidity is a high-level, object-oriented programming language designed for creating Dumb Contracts. The goal of Rubidity is to closely mimic Solidity so Dumb Contracts are easy to write for Smart Contract developers.

### Core Ruby Concepts You Should Know

**Blocks**: In Rubidity, blocks of code are encapsulated between `do` and `end` keywords. These blocks are often used to define the bodies of functions, loops, and control structures.

```ruby
function :mint, { amount: :uint256 }, :public do
  require(amount > 0, 'Amount must be positive')
  _mint(to: msg.sender, amount: amount)
end
```

**Symbols**: Symbols are similar to strings but are prefixed with a colon. They are essentially the same thing as strings but by custom are used for identifiers.

```ruby
ruby string :public, :name
```


# Rubidity by Example

Let's start with some examples from [Solidity's introduction](https://docs.soliditylang.org/en/v0.8.21/introduction-to-smart-contracts.html).

### Storage Example

This basic Solidity contract sets the value of a variable and then exposes it to others to access:

```solidity
contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}
```

Here is how this logic translates to Rubidity:

```ruby
class Contracts::SimpleStorage < Contract
  uint256 :storedData
  
  function :set, { x: :uint256 }, :public do
    s.storedData = x
  end
  
  function :get, {}, :public, :view, returns: :uint256 do
    return s.storedData
  end
  
  constructor() {}
end
```

Looking point by point, here's what the Solidity version does and how Rubidity translates it.

### **Contract Definition**

* **Solidity**: `contract SimpleStorage { ... }`
* **Rubidity**: `class Contracts::SimpleStorage < Contract`

  In Solidity, you define a contract using the `contract` keyword. In Rubidity, you define a contract as a Ruby class that inherits from a base class called `Contract`.

**State Variable**

* **Solidity**: `uint storedData;`
* **Rubidity**: `uint256 :storedData`

  Solidity uses the `uint` keyword to define a state variable. Rubidity uses a Ruby symbol to define a state variable along with its type, in this case, `uint256`.

### **Function Definitions**

#### **Set Function**

**Solidity**:<br>

```solidity
function set(uint x) public {
  storedData = x;
}
```

**Rubidity**:<br>

```ruby
function :set, { x: :uint256 }, :public do
  s.storedData = x
end
```

The `set` function is public in both languages and takes an unsigned integer as an argument. In Rubidity, the function signature also specifies its visibility (`:public`) and arguments (`{ x: :uint256 }`).

#### **Get Function**

**Solidity**:

```solidity
function get() public view returns (uint) {
  return storedData;
}
```

**Rubidity**:

```ruby
function :get, {}, :public, :view, returns: :uint256 do
  return s.storedData
end
```

The `get` function is public and has a `view` property in both languages. The Solidity version uses the `returns` keyword to specify the return type, while Rubidity does it via the `returns: :uint256` option.

**Constructor**

* **Solidity**: No explicit constructor.
* **Rubidity**: `constructor() {}`

  Both Solidity and Rubidity examples don't utilize a constructor for any initial setup, but the Rubidity code explicitly includes an empty constructor for clarity.

### **Accessing State**

In Rubidity, state variables are accessed using the `s.` prefix, as seen in `s.storedData = x` and `return s.storedData`. Writing to state using an unprefixed variable is not possible in Ruby and the explicitness of `s.` is nice anyway.

### Token Minting Example

Let's break down this `OpenMintToken` Rubidity contract line-by-line, focusing on how it might translate to a Solidity contract. The contract is an ERC20 token with a capped supply and additional limitations on individual mints.

```ruby
class Contracts::OpenMintToken < Contract
  is :ERC20
  
  uint256 :public, :maxSupply
  uint256 :public, :perMintLimit
  
  constructor(
    name: :string,
    symbol: :string,
    maxSupply: :uint256,
    perMintLimit: :uint256,
    decimals: :uint256
  ) {
    ERC20(name: name, symbol: symbol, decimals: decimals)
    s.maxSupply = maxSupply
    s.perMintLimit = perMintLimit
  }
  
  function :mint, { amount: :uint256 }, :public do
    require(amount > 0, 'Amount must be positive')
    require(amount <= s.perMintLimit, 'Exceeded mint limit')
    
    require(s.totalSupply + amount <= s.maxSupply, 'Exceeded max supply')
    
    _mint(to: msg.sender, amount: amount)
  end
  
  function :airdrop, { to: :addressOrDumbContract, amount: :uint256 }, :public do
    require(amount > 0, 'Amount must be positive')
    require(amount <= s.perMintLimit, 'Exceeded mint limit')
    
    require(s.totalSupply + amount <= s.maxSupply, 'Exceeded max supply')
    
    _mint(to: to, amount: amount)
  end
end
```

1. **Inheritance**: `is :ERC20`
   * This line means that the `OpenMintToken` contract inherits from an existing ERC20 contract, inheriting its state variables, functions, and logic.
2. **State Variables**: `uint256 :public, :maxSupply` and `uint256 :public, :perMintLimit`
   * These lines define two public state variables, `maxSupply` and `perMintLimit`. Public state variables are accessible to external contracts and can also have getter methods generated automatically.
3. **Constructor**:

   ```ruby
   ruby constructor(
     name: :string,
     symbol: :string,
     maxSupply: :uint256,
     perMintLimit: :uint256,
     decimals: :uint256
   )
   ```

   * The constructor function initializes the contract. It takes the token's name, symbol, maximum supply, per-mint limit, and decimals as parameters.
4. **State Variable Initialization**: `s.maxSupply = maxSupply` and `s.perMintLimit = perMintLimit`
   * These lines initialize the state variables using the `s.` prefix, which is specific to Rubidity for accessing and manipulating state variables.
5. **Mint Function**:

   ```ruby
   function :mint, { amount: :uint256 }, :public do
     require(amount > 0, 'Amount must be positive')
     require(amount <= s.perMintLimit, 'Exceeded mint limit')
     require(s.totalSupply + amount <= s.maxSupply, 'Exceeded max supply')
     _mint(to: msg.sender, amount: amount)
   end
   ```

   * This is a public function to mint new tokens. The `require` statements serve as checks that the `amount` is positive, within the per-mint limit, and won't exceed the max supply. `_mint` is a likely internal function inherited from the ERC20 contract that actually mints the tokens.
6. **Airdrop Function**:

   ```ruby
   function :airdrop, { to: :addressOrDumbContract, amount: :uint256 }, :public do
     require(amount > 0, 'Amount must be positive')
     require(amount <= s.perMintLimit, 'Exceeded mint limit')
     require(s.totalSupply + amount <= s.maxSupply, 'Exceeded max supply')
     _mint(to: to, amount: amount)
   end
   ```

   * Similar to the `mint` function but allows specifying a recipient address `to`. It performs the same `require` checks and then invokes `_mint` to create the tokens for the target address.

The Rubidity code uses specific language constructs that mirror Solidity but in a Ruby-like syntax, making it more expressive while maintaining similar logic and functionalities.


# Data Types

Rubidity is strongly-typed. This documentation aims to give you an overview of Rubidity's type system so you can create more robust and reliable applications.

### Available Types in Rubidity

* `:string`: Text-based data. Rubidity strings are immutable.
* `:mapping`: Key-value storage for different types.
* `:address`: Ethereum address (in hexadecimal).
* `:dumbContract`: A specific type of contract ID (hexadecimal).
* `:addressOrDumbContract`: Either an Ethereum address or a specific type of contract ID.
* `:ethscriptionId`: Unique identifiers for Ethscriptions (hexadecimal).
* `:bool`: Boolean values (true or false).
* `:uint256`: Unsigned 256-bit integers.
* `:int256`: Signed 256-bit integers.
* `:array`: Lists of other types.
* `:datetime`: Date and time (stored as unsigned 256-bit integers).

Every Rubidity variable has a type. This means that whenever you declare a variable in an event, function, or state variable, you must include its type. Here's an example of declaring the type of a state variable:

```ruby
uint256 :public, :totalSupply
```

You don't have to declare the type of a local variable, so you can just write `val = s.totalSupply` (recall `s.` is how you access state variables). When you do this val takes its type from `totalSupply`.

You also don't have to declare the type of literals like "I'm a string". Literals have their types inferred when they are assigned to typed variables. For example, "100" will be interpreted as the integer 100 when assigned to a uint256 and as the string "100" when assigned to a string.

Example:

```ruby
ruby code# State variable with type
uint256 :public, :totalSupply

# Local variable without explicit type
val = s.totalSupply
```

Most of the time a variable can remain untyped until its value is assigned to a typed variable. However, sometimes you must explicitly cast variables. For example, typed variables can only be equal to other typed variables. In these cases you can cast a variable with `7.cast(:uint256)`.

### Type Coercion and Validation in Rubidity

Rubidity employs a strong system of type validation and coercion to ensure that variables adhere to their declared types. This involves transforming literal values into the corresponding Rubidity types and reporting type mismatches.

Here's a brief rundown of Rubidity's type coercion rules:

* **:address**: Accepts hexadecimal strings that match the Ethereum address format (`0x` followed by 40 hexadecimal characters). The address is then normalized to lowercase.
* **:uint256 and :int256**: These types accept both integer and string representations. Strings are attempted to be coerced into integers. uint256 and int256 cannot be out of the range of their Solidity counterparts.
* **:string**: Only accepts string literals. Starting from the latest update, strings are immutable.
* **:bool**: Accepts only `true` or `false`.
* **:dumbContract and :ethscriptionId**: Accepts hexadecimal strings matching specific patterns (`0x` followed by 64 hexadecimal characters).
* **:addressOrDumbContract**: Accepts either an address or a `:dumbContract`, again matching the relevant hexadecimal patterns.
* **:datetime**: Relies on `:uint256` type coercion, as it's represented as an unsigned integer internally.
* **:mapping**: Accepts a Hash and ensures that keys and values match the specified types. Coerces these into a special mapping proxy object.
* **:array**: Accepts an array and ensures that the values match the specified type. Coerces these into a special array proxy object.

### Default Values

In Rubidity, every type comes with a default value that gets assigned when a variable is declared but not initialized. Understanding these defaults is crucial for avoiding unintended behavior in your dApps. Here is the rundown:

* **Integers (:int256, :uint256, :datetime)**: Default to `0`.
* **Address Types (:address, :addressOrDumbContract)**: Default to a zero-address, which is `0x0000000000000000000000000000000000000000`.
* **Contract Identifiers (:dumbContract, :ethscriptionId)**: Default to a zero-identifier, `0x0000000000000000000000000000000000000000000000000000000000000000`.
* **String (:string)**: Default to an empty string `''`.
* **Boolean (:bool)**: Default to `false`.
* **Mapping (:mapping)**: Default to an empty mapping proxy object. The key and value types are set according to your specifications.
* **Array (:array)**: Default to an empty array proxy object. The value type is set according to your specification.


# State Variables

## Defining State Variables in Rubidity

### Overview

State variables are data elements that store the contract's state. In Rubidity, you can define state variables, specify their types, and set visibility options (`public`, `private`, or `internal`) along with additional flags like `immutable` and `constant`.

### Basic Syntax

The basic syntax to define a state variable is as follows:

```ruby
type :visibility, :variable_name, :flags
```

* `type`: The type of the variable (`uint256`, `string`, etc.)
* `visibility`: Visibility of the variable (`public`, `private`, or `internal`)
* `variable_name`: The name of the state variable
* `flags`: Additional flags like `:immutable` or `:constant`

### Examples

Declare a public string variable named `name`:

```ruby
string :public, :name
```

Declare a public unsigned integer named `totalSupply`:

```ruby
uint256 :public, :totalSupply
```

### Advanced Types

For complex types like mappings and arrays, special syntax is used:

#### **Mappings**

```ruby
mapping ({ key_type: :value_type }), :visibility, :variable_name
```

#### **Arrays**

```ruby
array :value_type, :visibility, :variable_name
```

### Automatically Generated Getters

Much like Solidity, for every public state variable, Rubidity automatically generates a getter function.

### Accessing State Variables in Contract Logic

In your contract logic, you can access state variables using the `s` object:

```ruby
s.variable_name
```

Example:

```ruby
# Accessing a state variable
function :get_balance, { addr: :address }, :public, :view, returns: :uint256 do
  return s.balances[addr] # Accessing s.balances
end
```

#### Flags (Not Yet Implemented)

* `:immutable`: The variable can only be set once, typically in the constructor.
* `:constant`: The variable's value is set at compile time and cannot be changed.

***

<br>


# Functions

### Function Definition

You define functions with the `function` method with the following signature:

```ruby
function :function_name, {arg1: :type1, arg2: :type2, ...}, *options, returns: return_type do
  # function body
end
```

For example:

```ruby
function :tokenURI, { id: :uint256 }, :public, :view, :override, returns: :string do
  require(_exists(id: id), 'ERC721Metadata: URI query for nonexistent token')
  # ...
end
```

**Parameters**

* `:function_name` - The name of the function
* `{arg1: :type1, arg2: :type2, ...}` - A hash of argument names to types.
* `*options` - Zero or more flags that specify additional function characteristics like visibility (`:public`, `:private`), state mutability (`:view`, `:pure`, `:payable`), etc.
* `returns: return_type` - Specifies the return type. This is optional but if provided, the function must return a value that matches this type.

**Inside the Function Body**

* `s.variableName` can be used to read from and write to state variables.
* If you write an identifier like `variableName` without the `s.` prefix, the system will look for it among the function's arguments first, and then among the contract's functions.

### Calling Functions in Other Contracts

To invoke functions from another contract, the contract ID is needed. You can create an instance of that contract using `DumbContract(id)` and then call its methods.

Example:

```ruby
DumbContract(id).transfer(
  to: msg.sender,
  amount: output_amount
)
```

#### Automatically Generated Functions

If you have a public state variable, a getter function is automatically generated. So, there's no need to write a separate getter function for that variable.


# Events

In Rubidity, events serve as mechanisms to log and emit changes or activities that occur within a smart contract. Here is how you can define and emit events in Rubidity.

**Event Definition**

To define an event in your Rubidity contract, you use the `event` keyword followed by the event name and a hash describing the expected arguments:

```ruby
event :Transfer, { from: :addressOrDumbContract, to: :addressOrDumbContract, amount: :uint256 }
```

In this example, a `Transfer` event is defined with three arguments—`from`, `to`, and `amount`. Their types are also specified, making it strongly-typed just like any other aspect of Rubidity.

**Event Emission**

To emit an event, you call the `emit` method within your contract's methods. The `emit` method requires two arguments: the name of the event to emit and a hash containing the arguments to pass to the event:

```ruby
def some_transfer_function(from, to, amount)
  # Perform transfer logic here
  emit(:Transfer, { from: from, to: to, amount: amount })
end
```

**Event Validation**

As with functions, Rubidity performs argument validation when emitting an event. Specifically, it checks:

* If the event is defined in the contract.
* If any required arguments are missing.
* If any unexpected arguments are included.

**Example**

Here is a small example that demonstrates defining and emitting an event in a Rubidity contract:

```ruby
class Contracts::MyToken < Contract
  event :Transfer, { from: :address, to: :address, amount: :uint256 }
  
  function :do_transfer, { from: :address, to: :address, amount: :uint256 }, :public do
    # Perform the transfer logic here
    emit(:Transfer, { from: from, to: to, amount: amount })
  end
end
```


# Inheritance

#### Overview

In Rubidity, inheritance allows you to create new contracts that inherit the properties and methods of existing contracts. This is similar to how inheritance works in Solidity. The `is` keyword is used to denote inheritance. The concept of `virtual` and `override` modifiers is also present to provide fine-grained control over inherited methods.

#### Syntax

To declare that one contract inherits from another, the `is` keyword is used:

```ruby
class YourContract < Contract
  is :ParentContract
  ...
end
```

This causes all of `ParentContract's` functions, events, and state variables to be merged into `YourContract`.

Conversely, if a parent contract marks itself abstract, it can only be inherited from, but never deployed:

```ruby
class Contracts::ERC20 < Contract
  abstract
  
  event :Transfer, { from: :addressOrDumbContract, to: :addressOrDumbContract, amount: :uint256 }
  event :Approval, { owner: :addressOrDumbContract, spender: :addressOrDumbContract, amount: :uint256 }
end
```

#### Virtual and Override Modifiers

When a contract inherits from another, Rubidity automatically merges functions from parent contracts. If both parent and child contracts have a function with the same name, the child's function will override the parent's function, provided that the parent function is marked as `virtual` and the child's function is marked as `override`.

* `virtual`: Denotes that a function can be overridden in derived contracts.
* `override`: Used in a derived contract to specify that the function intentionally overrides a `virtual` function in the parent contract.

#### Super

Within a child function you can call the parent implementation with a **`_super_`** prefix. For example, `_super_transferFrom`. Ordinarily only the immediate parent can be called, but in constructors any parent can be called with this more explicit syntax: `ERC721(name: name, symbol: symbol)`.

### Example

This example shows off inheritance, overriding, and the two different forms of super:

```ruby
class Contracts::ERC721 < Contract
  string :public, :name

  constructor(name: :string, symbol: :string) {
    s.name = name
    s.symbol = symbol
  }
  
  function :transferFrom, { from: :addressOrDumbContract, to: :addressOrDumbContract, id: :uint256 }, :public, :virtual do
    # Transfer logic
  end
end
```

```ruby
class Contracts::TransferTracker < Contract
  is :ERC721
  
  uint256 :public, :transferCount
  
  constructor(
    name: :string,
    symbol: :string,
    # rest of args
  ) {
    ERC721(name: name, symbol: symbol)
    # Rest of the constructor code goes here
  }
  
  function :transferFrom, { from: :addressOrDumbContract, to: :addressOrDumbContract, id: :uint256 }, :public, :override do
    s.transferCount += 1
    s.name = "I have been transferred #{string(s.transferCount)} times!"
    
    _super_transferFrom(from: from, to: to, id: id)
  end
end
```

<br>


# Global Variables

In Rubidity, you have access to several built-in global objects within Dumb Contracts to interact with the Ethereum network and transactions. These globals are analogous to the `msg`, `tx`, and `block` objects in Solidity.

Here's an example:

```ruby
class ExampleContract < Contract
  constructor() {}

  function :interact, {}, :public do
    sender = msg.sender
    origin = tx.origin
    block_num = block.number
    block_time = block.timestamp

    # Log an event or do something with these variables
  end

  # Demonstrating the use of esc
  function :findAnEthscription, { id: :ethscriptionId }, :public, returns: :string do
    ethscriptionDetails = esc.getEthscriptionById(ethscriptionId: id)
    
    # Here you can access individual fields
    id = ethscriptionDetails.ethscriptionId
    creator = ethscriptionDetails.creator

    # Return current owner
    return ethscriptionDetails.currentOwner
  end
end

```

Here's a look at some of these globals:

**`msg`**

The `msg` global provides access to the message sender's details.

**Properties**

* `msg.sender`: Represents the address of the account that is directly responsible for this transaction. Its type is either `addressOrDumbContract`.

**Usage**

```ruby
msg.sender
```

**`tx`**

The `tx` global offers details about the transaction.

**Properties**

* `tx.origin`: Indicates the address of the externally owned account that initiated the transaction. Its type is `address`.

**Usage**

```ruby
tx.origin
```

**`block`**

The `block` global provides details about the current block.

**Properties**

* `block.number`: The block number. Its type is `uint256`.
* `block.timestamp`: The block timestamp. Its type is `datetime`.

**Methods**

* `block.blockhash(block_number)`: Retrieves the hash of a block by its number.

**Usage**

```ruby
block.number
block.timestamp
block.blockhash(some_block_number)
```

**`esc`**

The `esc` global is unique to Rubidity and is tailored for Ethscriptions.

**Methods**

* `esc.getEthscriptionById(ethscription_id)`: Finds an Ethscription by its ID and returns a structured response containing details like block number, creator, current owner, and so on.

**Usage**

```ruby
esc.getEthscriptionById("some_ethscription_id")
```

Rubidity doesn't currently support structs, but the return value of getEthscriptionById is a special case. It behaves as if it was this struct:

```ruby
struct EthscriptionDetails {
  ethscriptionId: :ethscriptionId,
  blockNumber: :uint256,
  blockBlockhash: :string,
  transactionIndex: :uint256,
  creator: :address,
  currentOwner: :address,
  initialOwner: :address,
  creationTimestamp: :uint256,
  previousOwner: :address,
  contentUri: :string,
  contentSha: :string,
  mimetype: :string
}
```


# Error Handling

Rubidity provides a set of tools to help you write secure and reliable smart contracts. This includes native support for preconditions via `require` statements and automatic type checking for variables and parameters.

#### Require Statements

The `require` function is used to ensure that certain conditions are met before a function proceeds. If the condition specified in `require` is false, the function will throw an error, and all changes to the state will be reverted.

Here's an example that demonstrates the use of `require` in a `mint` function:

```ruby
function :mint, { amount: :uint256 }, :public do
  # Ensure the mint amount is positive
  require(amount > 0, 'Amount must be positive')
  
  # Ensure the mint amount does not exceed a pre-defined per mint limit
  require(amount <= s.perMintLimit, 'Exceeded mint limit')

  # Ensure the total supply won't exceed the max supply
  require(s.totalSupply + amount <= s.maxSupply, 'Exceeded max supply')
  
  # Proceed to mint
  _mint(to: msg.sender, amount: amount)
end
```

In this example, the `require` statements act as guards that prevent undesirable actions. If any of the `require` statements fail, the function will terminate, providing the string message as an error reason.

#### Type Checking

Rubidity also performs automatic type checking on function parameters and state variables. This means that if you declare a variable as a `:uint256`, Rubidity will ensure that you can only assign unsigned 256-bit integers to that variable. Any attempt to assign a different type will result in a runtime error.


# VM API Data Model

### Database Schema

The VM relies on three primary database tables to capture the contract information, contract states, and call receipts.

#### `contracts` Table

* **Fields:**
  * `contract_id`: A unique identifier for the smart contract.
  * `type`: Categorizes the contract, such as ERC20 or ERC721.
  * `created_at` & `updated_at`: Standard timestamps.
* **Role**: Maintains a registry of all smart contracts deployed or interacted with.

#### `contract_states` Table

* **Fields:**
  * `contract_id`: Reference to the `contracts` table.
  * `ethscription_id`: The ID of the Ethscription transaction that led to this particular contract state.
  * `state`: JSON object containing the contract's current state.
  * `block_number` & `transaction_index`: Ethereum block and transaction index that capture the state.
* **Role**: Holds historical states of smart contracts, enabling time-travel queries for auditing or state reversion.

#### `contract_call_receipts` Table

* **Fields:**
  * `contract_id`: Reference to the `contracts` table.
  * `ethscription_id`: ID of the initiating Ethscription.
  * `caller`: Ethereum address of the entity initiating the function call.
  * `status`: Execution status code.
  * `function_name` & `function_args`: Function details.
  * `logs`: Execution logs.
  * `timestamp`: Time of call.
  * `error_message`: Stored if the call results in an error.
* **Role**: Records the outcomes of all contract function calls, enabling debugging, auditing, and transaction history views.

### Execution Flow

1. **Transaction Initialization**: Upon receiving a new Ethscription, initial validation occurs. If a new contract is deployed, a new row is created in the `contracts` table.
2. **Pre-Execution State**: The latest `contract_state` is fetched based on the `contract_id` to set the initial state of the smart contract.
3. **Function Execution**: The specified function in the smart contract is called, potentially altering its state.
4. **Post-Execution State**: A new row is added to `contract_states` capturing the updated contract state.
5. **Receipt Logging**: A new row is logged in `contract_call_receipts` with all the details of the function call, including its success or failure status.
6. **API Exposure**: This data is now accessible via various API endpoints, like `ContractsController#show_call_receipt`, which queries `contract_call_receipts` based on `ethscription_id` to deliver detailed transaction receipts.

By harmonizing the capabilities of Ethereum smart contracts with the efficiency of Ethscriptions, the VM creates a powerful environment for developing, deploying, and interacting with decentralized applications, all at a fraction of the cost usually associated with on-chain operations.


# VM API By Example

Given this contract:

```ruby
class Contracts::OpenMintToken < Contract
  is :ERC20
  
  uint256 :public, :maxSupply
  uint256 :public, :perMintLimit
  
  constructor(
    name: :string,
    symbol: :string,
    maxSupply: :uint256,
    perMintLimit: :uint256,
    decimals: :uint256
  ) {
    ERC20(name: name, symbol: symbol, decimals: decimals)
    s.maxSupply = maxSupply
    s.perMintLimit = perMintLimit
  }
  
  function :mint, { amount: :uint256 }, :public do
    require(amount > 0, 'Amount must be positive')
    require(amount <= s.perMintLimit, 'Exceeded mint limit')
    
    require(s.totalSupply + amount <= s.maxSupply, 'Exceeded max supply')
    
    _mint(to: msg.sender, amount: amount)
  end
  
  function :airdrop, { to: :addressOrDumbContract, amount: :uint256 }, :public do
    require(amount > 0, 'Amount must be positive')
    require(amount <= s.perMintLimit, 'Exceeded mint limit')
    
    require(s.totalSupply + amount <= s.maxSupply, 'Exceeded max supply')
    
    _mint(to: to, amount: amount)
  end
end
```

What happens when the Ethscriptions VM receives an ethscription with this data uri?

{% code overflow="wrap" %}

```
data:application/vnd.esc.contract.call+json,{"contractId":"0xdb6c87fd9b5e1aafdbc9cf4550856abde133e08e04910fe5fc319b63f5c08b3f","functionName":"mint","args":{"amount":5},"salt":"010e00f8408679670da12261c013b222"}
```

{% endcode %}

#### Step 1: Ethscription Validation

* The Ethscriptions VM first validates the incoming Ethscription transaction.
* It extracts the `content_uri` and decodes it to obtain the JSON payload containing `contractId`, `functionName`, and `args`.

#### Step 2: Contract Lookup

* The VM queries the `contracts` table to find the corresponding contract using the `contractId` provided (`0xdb6c87fd9b5e1aafdbc9cf4550856abde133e08e04910fe5fc319b63f5c08b3f` in this case).
* If the contract exists the VM proceeds to the next step.

#### Step 3: Pre-Execution State

* The VM fetches the latest state of the contract from the `contract_states` table based on the `contract_id`.
* The VM uses this state to set the initial values of state variables such as `maxSupply`, `perMintLimit`, and `totalSupply`.

#### Step 4: Function Parameters Validation

* The VM determines that the mint function exists on the contract.
* The VM then validates the parameters passed to the `mint` function, in this case, `amount: 5`. This includes type validations.
* Checks are made according to the conditions in the smart contract:
  * `amount` must be positive
  * `amount` must be less than or equal to `perMintLimit`
  * `totalSupply` + `amount` must be less than or equal to `maxSupply`

#### Step 5: Execute Mint Function

* If the validation checks pass, the VM calls the `mint` function.
* `_mint` is internally invoked, which increases the `totalSupply` and updates the balance of `msg.sender`.

#### Step 6: Post-Execution State

* A new entry is added to the `contract_states` table to reflect the state change post-minting.
* This includes the updated `totalSupply`, and the updated balance for `msg.sender`.

#### Step 7: Log Receipt

* A new row is inserted into the `contract_call_receipts` table, containing details of the function call, including:
  * `contract_id`
  * `ethscription_id`
  * `caller`
  * `status` (indicating success or failure)
  * `function_name` (in this case, "mint")
  * `function_args` (in this case, `{"amount": 5}`)
  * `logs` (any events emitted or other logs)
  * `timestamp`

#### Step 8: API Availability

* After the execution and logging, all this newly created data becomes available via API endpoints for querying or analysis.

Through these steps, the Ethscriptions VM efficiently executes the contract method, updates the state, and logs all necessary information for further interactions or audits.


