ENVIO HyperIndex with Oyster CVM

ENVIO HyperIndex with Oyster CVM

HyperIndex is Envio’s full-featured blockchain indexing framework that converts on-chain events into structured, queryable databases with GraphQL APIs. While HyperIndex offers fast access to on-chain data, the default setup does not provide verification or confidentiality.

Oyster CVM solves this by running the indexer inside a Trusted Execution Environment, providing two key security guarantees:

1. Confidentiality

The HyperIndex runs inside Oyster CVM’s Trusted Execution Environment (TEE), an isolated runtime environment designed to protect data and execution from external access. This hardware-enforced isolation ensures confidentiality, since no external process, including the host OS or cloud provider, can modify the code or data running inside the enclave.

  • Data Protection: All data processed by the indexer, including on-chain events, user balances and database entries, remains protected from unauthorized access by the host operating system, cloud provider administrators or external attackers.
  • Code Isolation: The indexer’s execution environment keeps its logic fully isolated, preventing anyone outside the enclave from modifying the code while it is running.

2. Verifiability through Attestation

Attestation enables a remote user to verify that the Oyster CVM enclave is running the expected, untampered version of the HyperIndex indexer. This verification is rooted in the Nitro Security Module, which acts as the hardware-backed trust anchor for the Nitro system.

During attestation, the enclave produces an Attestation Document that the Nitro Security Module signs. This document contains cryptographic measurements, including PCR values (Platform Configuration Registers). These values are hashes representing the enclave’s code, configuration, and image identity.

By comparing these measurements with the expected values, a user can confirm that:

  • The enclave is running the intended HyperIndex build.
  • The code and configuration remain unchanged.
  • The environment is a valid, attested Oyster CVM TEE instance.

This provides strong integrity guarantees and ensures that the results produced by HyperIndex are verifiable and come from a securely attested environment.

How we ran HyperIndex in Oyster CVM

For our setup, we chose to index an ERC20 token contract deployed on the Ethereum mainnet and index its transfers in order to fetch user balances by their wallet addresses.

  1. The setup began by initializing an Envio project using the command:

    pnpx envio init
    

    The Envio command line utility prompts for details such as the blockchain ecosystem, contract address, and the events to index, then generates a starter template for the indexer project.

  2. The next step was to copy the contract ABI into the project directory and update the config.yaml file to include the abi_file_path. The final config.yaml looked like this:

    name: envio-indexer
    networks:
    - id: 1
      start_block: 23370920
      contracts:
      - name: YBToken
        abi_file_path: ./abis/token.json
        address:
        - 0x01791F726B4103694969820be083196cC7c045fF
        handler: src/EventHandlers.ts
        events:
        - event: Approval(address indexed owner, address indexed spender, uint256 value)
        - event: OwnershipTransferred(address indexed previous_owner, address indexed new_owner)
        - event: RoleMinterChanged(address indexed minter, bool status)
        - event: Transfer(address indexed sender, address indexed receiver, uint256 value)
    unordered_multichain_mode: true
    preload_handlers: true
    
  3. Next, to index user balances, we created a new User entity in schema.graphql file and modified the transfer event handler to fetch each user’s balance using their wallet address.

    New entity in schema.graphql:

    type User @entity {
      id: ID!
      address: String!
      balance: BigInt!
    }
    

    Handler:

    YBToken.Transfer.handler(async ({ event, context }) => {
      const entity: YBToken_Transfer = {
        id: `${event.chainId}_${event.block.number}_${event.logIndex}`,
        sender: event.params.sender,
        receiver: event.params.receiver,
        value: event.params.value,
      };
    
      context.YBToken_Transfer.set(entity);
    
      const from = event.params.sender;
      const to = event.params.receiver;
      const amount = event.params.value;
      const ZeroAddress = "0x0000000000000000000000000000000000000000";
    
    
      if (from !== ZeroAddress) {
        const sender = await context.User.get(from);
    
        if (sender) {
          context.User.set({
            ...sender,
            balance: sender.balance - amount,
          });
        }
      }
    
      const receiver = await context.User.get(to);
    
      if (receiver) {
        context.User.set({
          ...receiver,
          balance: receiver.balance + amount,
        });
      } else {
        context.User.set({
          id: to,
          address: to,
          balance: amount,
        });
      }
    });
    
  4. To run the indexer in Oyster CVM, we used the Dockerfile and docker-compose configuration file provided by the Envio team for the indexer, its database, and the GraphQL engine.

    For the docker-compose setup, we switched to host network mode and updated the local IP references accordingly. The modified docker-compose file we used is shown below.

    services:
      envio-postgres:
        image: postgres:17.5
        restart: always
        ports:
          - "${ENVIO_PG_PORT:-5433}:5432"
        volumes:
          - envio-indexer-storage:/var/lib/postgresql/data
        environment:
          POSTGRES_PASSWORD: ${ENVIO_PG_PASSWORD:-testing}
          POSTGRES_USER: ${ENVIO_PG_USER:-postgres}
          POSTGRES_DB: ${ENVIO_PG_DATABASE:-envio-dev}
        network_mode: host
      graphql-engine:
        image: hasura/graphql-engine:v2.43.0
        ports:
          - "${HASURA_EXTERNAL_PORT:-8080}:8080"
        user: 1001:1001
        depends_on:
          - "envio-postgres"
        restart: always
        environment:
          HASURA_GRAPHQL_DATABASE_URL: postgres://${ENVIO_PG_USER:-postgres}:${ENVIO_PG_PASSWORD:-testing}@localhost:5432/${ENVIO_PG_DATABASE:-envio-dev}
          HASURA_GRAPHQL_ENABLE_CONSOLE: ${HASURA_GRAPHQL_ENABLE_CONSOLE:-true} # can make this default to false once we are further in our development.
          HASURA_GRAPHQL_ENABLED_LOG_TYPES:
            startup, http-log, webhook-log, websocket-log,
            query-log
          HASURA_GRAPHQL_NO_OF_RETRIES: 10
          HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET:-testing}
          HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES: "true"
          PORT: 8080
          HASURA_GRAPHQL_UNAUTHORIZED_ROLE: public
        healthcheck:
          test: timeout 1s bash -c ':> /dev/tcp/0.0.0.0/8080' || exit 1
          interval: 5s
          timeout: 2s
          retries: 50
          start_period: 5s
        network_mode: host
      envio-indexer:
        image: sagarparker/enviotest:latest
        ports:
          - "0.0.0.0:9898:${ENVIO_INDEXER_PORT:-9898}"
        depends_on:
          graphql-engine:
            condition: service_healthy
        deploy:
          resources:
            limits:
              cpus: "0.8"
              memory: 800M
        restart: always
        environment:
          ENVIO_PG_PASSWORD: ${ENVIO_PG_PASSWORD:-testing}
          ENVIO_PG_HOST: localhost
          ENVIO_PG_PORT: 5432
          ENVIO_PG_USER: ${ENVIO_PG_USER:-postgres}
          ENVIO_PG_DATABASE: ${ENVIO_PG_DATABASE:-envio-dev}
          ENVIO_PG_PUBLIC_SCHEMA: ${ENVIO_PG_PUBLIC_SCHEMA:-envio}
          ENVIO_INDEXER_HOST: 0.0.0.0
          ENVIO_INDEXER_PORT: ${ENVIO_INDEXER_PORT:-9898}
          CONFIG_FILE: ${CONFIG_FILE:-config.yaml}
          LOG_LEVEL: ${LOG_LEVEL:-trace}
          LOG_STRATEGY: ${LOG_STRATEGY:-console-pretty}
          HASURA_GRAPHQL_ENDPOINT: http://localhost:8080/v1/metadata
          HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET:-testing}
          HASURA_SERVICE_HOST: localhost
          HASURA_SERVICE_PORT: 8080
          TUI_OFF: ${TUI_OFF:-true}
        healthcheck:
          test: timeout 1s bash -c ':> /dev/tcp/0.0.0.0/8080' || exit 1
          interval: 5s
          timeout: 2s
          retries: 50
          start_period: 5s
        network_mode: host
    volumes:
      envio-indexer-storage:
    
  5. The final step for the indexer deployment was to install the oyster-cvm CLI and deploy the enclave.

    Command for installing oyster-cvm CLI:

    # for linux with amd64 architecture
    sudo wget https://artifacts.marlin.org/oyster/binaries/oyster-cvm_latest_linux_amd64 -O /usr/local/bin/oyster-cvm
    
    # for linux with arm64 architecture
    sudo wget https://artifacts.marlin.org/oyster/binaries/oyster-cvm_latest_linux_arm64 -O /usr/local/bin/oyster-cvm
    
    # for darwin with arm64 architecture (M series Macs)
    sudo wget https://artifacts.marlin.org/oyster/binaries/oyster-cvm_latest_darwin_arm64 -O /usr/local/bin/oyster-cvm
    

    Command for deploying the oyster-cvm instance:

    oyster-cvm deploy --wallet-private-key <YOUR_WALLET_PRIVATE_KEY> --duration-in-minutes 120 --docker-compose docker-compose.yaml --arch amd64  --instance-type c5.2xlarge
    

    On successful deployment, the above command returns the instance ip which can be used to fetch user’s balance using a graphQL query.

  6. Fetching user balances using a GraphQL query

    The GraphQL query below returns a user’s on-chain token balance from the indexer running in Oyster CVM.

    curl --location 'http://<OYSTER_CVM_INSTANCE_IP>:8080/v1/graphql' \
    --header 'Content-Type: application/json' \
    --data '{"query":"query MyQuery {\n  User(where: {address: {_eq: \"0x4E9193C8CDe3A4f3dBE52ff61F66D9389ffBcaF0\"}}) {\n    address\n    balance\n  }\n}","variables":{}}'
    

    The Hasura dashboard can be accessed by visiting:

    http://<OYSTER_CVM_INSTANCE_IP>:8080/console

  7. Attestation verification with Oyster CVM CLI

    The enclave attestation for the instance running the Envio indexer can be performed using the following command:

    oyster-cvm verify --enclave-ip <OYSTER_CVM_INSTANCE_IP>
    

    Sample output :

Conclusion

Running HyperIndex inside Oyster CVM brings verifiability and confidentiality to blockchain indexing without compromising on speed or developer experience. The TEE-backed environment ensures that both the indexer’s data and execution remain protected, while attestation allows users to verify the integrity of the system. This combination provides a trustworthy foundation for building secure, data-driven applications on top of Envio’s indexing framework.