Skip to main content
Conditional Transactions are the second privacy primitive part of SKALE’s BITE (Blockchain Integrated Threshold Encryption) Protocol. They enable smart contracts to store encrypted data and request decryption directly from within Solidity and the EVM. While Encrypted Transactions focuses on encrypting transaction payloads for privacy, Conditional Transactions (CTXs) are transactions initiated by smart contracts in one block and executed in the next block with decrypted data.
This example uses the BITE V2 Sandbox 2. Contact the SKALE Team in https://discord.gg for access.

How CTX Works

  1. Encrypt off-chain: Use bite.encryptMessage() to encrypt data
  2. Submit to contract: Send encrypted data to a smart contract
  3. Request decryption: Contract calls BITE.submitCTX() to create a Conditional Transaction
  4. Next block: SKALE consensus decrypts and calls onDecrypt() callback with decrypted values

Prerequisites

  • Node.js 18+ and bun or npm
  • @skalenetwork/bite TypeScript SDK
  • @skalenetwork/bite-solidity Solidity helpers
  • Access to BITE V2 Sandbox 2
Compiler Requirements
  • Solidity version: >= 0.8.27
  • EVM version: istanbul or lower (via pragma or compiler settings)

Project Setup

mkdir ctx-example && cd ctx-example
npm init -y
npm i --save-dev hardhat
npx hardhat init

# Install BITE dependencies
npm i @skalenetwork/bite @skalenetwork/bite-solidity @openzeppelin/contracts

Smart Contract

Create SimpleSecret.sol:
pragma solidity >=0.8.27;

import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { BITE } from "@skalenetwork/bite-solidity/BITE.sol";
import { IBiteSupplicant } from "@skalenetwork/bite-solidity/interfaces/IBiteSupplicant.sol";

contract SimpleSecret is IBiteSupplicant {
    using Address for address payable;

    bytes public decryptedMessage;
    address public ctxSender;

    uint256 public constant CTX_GAS_LIMIT = 2500000; 
    uint256 public constant CTX_GAS_PAYMENT = 0.06 ether;

    error AccessViolation();

    // Submit encrypted secret for decryption
    function revealSecret(bytes calldata encrypted) external payable {
        require(msg.value == CTX_GAS_PAYMENT, "Invalid CTX gas payment");

        bytes[] memory encryptedArgs = new bytes[](1);
        encryptedArgs[0] = encrypted;

        bytes[] memory plaintextArgs = new bytes[](0);

        // Submit CTX - returns address that will call onDecrypt
        address payable ctxSender = BITE.submitCTX(
            BITE.SUBMIT_CTX_ADDRESS,
            CTX_GAS_LIMIT,
            encryptedArgs,
            plaintextArgs
        );

        // Refund any unused ETH
        payable(ctxSender).sendValue(msg.value);
    }

    // Called by SKALE consensus in next block with decrypted data
    function onDecrypt(
        bytes[] calldata decryptedArgs,
        bytes[] calldata /* plaintextArgs */
    ) external override {
        decryptedMessage = decryptedArgs[0];
    }

    function getSecret() external view returns (bytes memory) {
        return decryptedMessage;
    }
    receive() external payable {}
    fallback() external payable {}
}

Key Points

  • IBiteSupplicant requires implementing onDecrypt() callback
  • BITE.submitCTX() creates the Conditional Transaction
  • BITE.SUBMIT_CTX_ADDRESS is the predeployed CTX handler
  • ctxSender stores the address that will call back - used for security

Test Script

Create run-simple.ts:
import { BITE } from "@skalenetwork/bite";
import { Contract, JsonRpcProvider, Wallet, ContractFactory } from "ethers";
import SimpleSecretJson from "./out/SimpleSecret.sol/SimpleSecret.json";

const providerUrl = "https://base-sepolia-testnet.skalenodes.com/v1/bite-v2-sandbox-2";
const INSECURE_ETH_PRIVATE_KEY = "0x..."; // Replace with your private key

async function main() {
    const provider = new JsonRpcProvider(providerUrl);
    const signer = new Wallet(INSECURE_ETH_PRIVATE_KEY, provider);
    const bite = new BITE(providerUrl);
    const CTX_GAS_PAYMENT = BigInt(60000000000000000)

    // Deploy contract
    console.log("Deploying SimpleSecret...");
    const factory = new ContractFactory(
        SimpleSecretJson.abi,
        SimpleSecretJson.bytecode,
        signer
    );
    const contract = await factory.deploy();
    await contract.waitForDeployment();
    const address = await contract.getAddress();
    console.log(`Contract: ${address}\n`);

    // Encrypt a secret message
    const secret = "0x" + Buffer.from("Hello BITE!").toString("hex");
    console.log(`Encrypting: "${secret}"`);
    const encrypted = await bite.encryptMessage(secret);
    console.log(`Encrypted: ${encrypted}\n`);

    // Submit encrypted secret to contract
    console.log("Submitting encrypted secret...");
    const tx = await contract.revealSecret(encrypted, { 
        gasLimit: 500000,
        value: CTX_GAS_PAYMENT 
    });
    await tx.wait();
    console.log("Submitted! Waiting for next block...\n");

    // Wait for next block (onDecrypt executes)
    const currentBlock = await provider.getBlockNumber();
    let nextBlock = currentBlock;
    while (nextBlock <= currentBlock) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        nextBlock = await provider.getBlockNumber();
    }

    // Check decrypted result
    const result = await contract.getSecret();
    const decoded = Buffer.from(result.slice(2), "hex").toString();
    console.log(`Decrypted secret: "${decoded}"`);
}

main().catch(console.error);

Running

# Build contract
npx hardhat compile

# Run script
bun run run-simple.ts

Resources