Early version of BITE Phase 2 MVP spec

BITE Phase 2 MVP Spec

With BITE Phase 2, each block can include Contract-Action-Transactions (CATs)— transactions initiated by smart contracts execution in the previous block.

CATs enable smart contracts to decrypt data and the perform actions automatically on this data.

Key Benefits of Using BITE Phase 2

  • Automation: Contracts act on decrypted data automatically, without requiring another user transaction.
  • Efficiency: Decryption is done in the same batch as BITE Phase 1, so no extra performance overhead.
  • Determinism: Execution happens in a predictable order (CATs run before regular transactions in block N+1).

Contract-Action-Transactions

  1. A SC in block $N$ calls $decryptAndExecute$ precompile passing an $encryptedAruments$ array and an $plaintextArguments$ array of plaintext arguments.

  2. A CAT transaction is added to the next block. CAT transactions are placed in front of regular transactions in the block. They are not subject to block gas limit.

  3. CAT transactions have the same $msg.sender$ as the transaction that created them.

  4. CAT transaction $to$ field is the SC that originated it. The SC sends a transaction to itself.

  5. CAT transaction always calls $onDecrypt$ function of the SC that originated them.

  6. CAT transactions are decrypted during the same batch decrypt as the BITE Phase 1 transaction, during finalization of block $N$. Therefore, BITE Phase 2 does not change performance compared BITE Phase 1.

decryptAndExecute Precompile

This function creates a CAT transaction

/**
Create a CAT transaction that will be decrypted and executed in the next block

 * @notice Decrypts the provided encrypted arguments and executes the associated logic using both decrypted and plaintext arguments in the next block 
 * @param encryptedArguments An array of encrypted byte arrays representing the arguments that need to be decrypted before execution.
 * @param plaintextArguments An array of byte arrays representing the arguments that are already in plaintext and do not require decryption.
 */


function decryptAndExecute(
    bytes[] calldata encryptedArguments,
    bytes[] calldata plaintextArguments
) external;

onDecrypt call

If a smart contract defines an $onDecrypt()$ function, it can initiate decryption in Block $N$, and the decryption results are passed to $onDecrypt()$ in Block $N+1$.


/**

  Execute SC call on decrypted arguments

 @param decryptedArguments An array of decrypted byte arrays representing the encrypted arguments that were passed to decryptAndExecute
 * @param plaintextArguments An array of byte arrays representing the arguments that are already in plaintext and do not require decryption.
 */

function onDecrypt(
    bytes[] calldata decryptedArguments,
    bytes[] calldata plaintextArguments
) external;

Implementation: skaled-consensus interface

Skaled-consensus interface is modified to pass CAT transactions generated during previous block from skaled to consensus.
They are placed in the block in front of regular transactions, and decrypted at the finalization of each block, in the same batch as BITE 1 transactions

    // Returns hashes and bytes of new transactions as well as state root to put into block proposal
    virtual transactions_vector ConsensusExtFace::pendingTransactions(size_t _limit, u256 &_stateRoot
#ifdef BITE2
    , transactions_vector& _catTransactions
#endif
    );

Rock-Paper-Scissors example.

The example below uses BITE Protocol Phase 2 to implement Rock-Paper-Scissors games for two players where the smart contract collects encrypted moves from two players, and then decrypts both at the same time

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * Minimal interface to the Phase 2 precompile (void return).
 * Replace PRECOMPILE_ADDR with the actual address on your network.
 */
interface IBitePhase2 {
    /**
     * @notice Creates a CAT that will decrypt args and call onDecrypt in the next block.
     * @param encryptedArguments Encrypted arguments, decrypted during finalization of the current block.
     * @param plaintextArguments Plaintext arguments, passed through as-is.
     */
    function decryptAndExecute(
        bytes[] calldata encryptedArguments,
        bytes[] calldata plaintextArguments
    ) external;
}

contract RockPaperScissors {
    // -------------------- Config --------------------
    address constant PRECOMPILE_ADDR = 0x0000000000000000000000000000000000000100;
    IBitePhase2 constant BITE = IBitePhase2(PRECOMPILE_ADDR);

    enum Move {
        None,       // 0
        Rock,       // 1
        Paper,      // 2
        Scissors    // 3
    }

    // -------------------- Events --------------------
    event GameCreated(uint256 indexed gameId, address indexed p1, address indexed p2);
    event EncryptedMoveSubmitted(uint256 indexed gameId, address indexed player);
    event WinnerDecided(
        uint256 indexed gameId,
        address winner,        // address(0) means draw
        Move p1Move,
        Move p2Move
    );

    // -------------------- Storage --------------------
    struct Game {
        address p1;
        address p2;
        bytes encMove1;   // encrypted Move for p1
        bytes encMove2;   // encrypted Move for p2
        bool p1Submitted;
        bool p2Submitted;

        // Controls to ensure the CAT callback is expected
        bool pendingCat;
        address expectedCaller; // msg.sender that scheduled decryptAndExecute
        bool finished;
    }

    uint256 public nextGameId;
    mapping(uint256 => Game) public games;

    // -------------------- Game Flow --------------------

    function createGame(address opponent) external returns (uint256 gameId) {
        require(opponent != address(0) && opponent != msg.sender, "bad opponent");
        gameId = nextGameId++;
        games[gameId].p1 = msg.sender;
        games[gameId].p2 = opponent;
        emit GameCreated(gameId, msg.sender, opponent);
    }

    /**
     * @notice Each player submits their encrypted move (opaque bytes).
     * The second submission triggers decryptAndExecute in the same tx.
     *
     * Expected decryption: each encrypted blob decrypts to a single byte 1..3 (Move enum).
     */
    function submitEncryptedMove(uint256 gameId, bytes calldata encMove) external {
        Game storage g = games[gameId];
        require(!g.finished, "game finished");
        require(msg.sender == g.p1 || msg.sender == g.p2, "not a player");

        if (msg.sender == g.p1) {
            require(!g.p1Submitted, "p1 already submitted");
            g.encMove1 = encMove;
            g.p1Submitted = true;
        } else {
            require(!g.p2Submitted, "p2 already submitted");
            g.encMove2 = encMove;
            g.p2Submitted = true;
        }

        emit EncryptedMoveSubmitted(gameId, msg.sender);

        // If both moves are in and we haven't scheduled a CAT yet, schedule it now.
        if (g.p1Submitted && g.p2Submitted && !g.pendingCat) {
            g.pendingCat = true;
            g.expectedCaller = msg.sender; // per spec, CAT msg.sender == caller of decryptAndExecute

            // encryptedArguments: both encrypted moves
            bytes;
            encArgs[0] = g.encMove1;
            encArgs[1] = g.encMove2;

            // plaintextArguments: pass identifiers to reconstruct context in onDecrypt
            // - gameId
            // - p1, p2
            bytes;
            plain[0] = abi.encode(gameId);
            plain[1] = abi.encode(g.p1);
            plain[2] = abi.encode(g.p2);

            // Schedule CAT; no return value
            BITE.decryptAndExecute(encArgs, plain);
        }
    }

    /**
     * @notice CAT callback (executed in Block N+1). Receives decrypted moves and our plaintext context.
     * Security notes for MVP:
     *  - We gate by `pendingCat` and by `expectedCaller` (the account that scheduled the CAT).
     *  - In production, consider adding a CAT nonce or blockTag in plaintext args for stronger domain separation.
     */
    function onDecrypt(
        bytes[] calldata decryptedArguments, // [ p1MoveDecrypted, p2MoveDecrypted ]
        bytes[] calldata plaintextArguments  // [ gameId, p1, p2 ]
    ) external {
        // Decode context
        require(plaintextArguments.length == 3, "bad plaintext len");
        (uint256 gameId) = abi.decode(plaintextArguments[0], (uint256));
        (address p1) = abi.decode(plaintextArguments[1], (address));
        (address p2) = abi.decode(plaintextArguments[2], (address));

        Game storage g = games[gameId];
        require(!g.finished, "already finished");
        require(g.pendingCat, "no pending CAT");
        require(msg.sender == g.expectedCaller, "unexpected caller (not CAT origin)");

        // Decode decrypted moves (each is expected to be a single byte 1..3)
        require(decryptedArguments.length == 2, "bad decrypted len");
        Move p1Move = _asMove(decryptedArguments[0]);
        Move p2Move = _asMove(decryptedArguments[1]);

        // Decide winner
        address winner = _winnerOf(p1, p2, p1Move, p2Move);

        // Mark finished and clear flags
        g.finished = true;
        g.pendingCat = false;
        g.expectedCaller = address(0);

        emit WinnerDecided(gameId, winner, p1Move, p2Move);
    }

    // -------------------- Helpers --------------------

    function _asMove(bytes calldata b) private pure returns (Move) {
        require(b.length == 1, "bad move len");
        uint8 v = uint8(b[0]);
        require(v >= uint8(Move.Rock) && v <= uint8(Move.Scissors), "bad move value");
        return Move(v);
    }

    function _winnerOf(
        address p1,
        address p2,
        Move m1,
        Move m2
    ) private pure returns (address) {
        if (m1 == m2) return address(0);
        // Rock(1) beats Scissors(3), Paper(2) beats Rock(1), Scissors(3) beats Paper(2)
        if (
            (m1 == Move.Rock && m2 == Move.Scissors) ||
            (m1 == Move.Paper && m2 == Move.Rock) ||
            (m1 == Move.Scissors && m2 == Move.Paper)
        ) {
            return p1;
        } else {
            return p2;
        }
    }
} 

Rock-Paper-Scissors explanations

This contract demonstrates how BITE Phase 2 enables smart contracts to decrypt data and act automatically via Contract-Action-Transactions (CATs).

The example implements a simple two-player Rock-Paper-Scissors game where each player submits an encrypted move, and once both moves are submitted, the contract automatically schedules a CAT transaction to decrypt the moves and determine the winner.


Game Flow

1. Game Creation

  • A player calls createGame(opponent) to set up a new game.
  • The contract stores:
    • p1 (creator),
    • p2 (opponent),
    • and assigns a gameId.
  • Emits GameCreated event.

2. Submitting Encrypted Moves

  • Each player calls submitEncryptedMove(gameId, encMove) with their encrypted move.
  • Moves are stored in the contract:
    • encMove1 for player 1,
    • encMove2 for player 2.
  • Emits EncryptedMoveSubmitted event.

3. Scheduling CAT Decryption

  • Once both moves are submitted:
    • The contract calls the BITE Phase 2 precompile:
      BITE.decryptAndExecute(encArgs, plainArgs);
      
    • encArgs = [encMove1, encMove2] (encrypted moves).
    • plainArgs = [gameId, p1, p2] (context info to reconstruct the game).
  • This creates a CAT transaction that will:
    • Run in the next block,
    • Call onDecrypt(decryptedMoves, plaintextArgs).

:zap: Important: CAT transactions are not user-submitted. They are automatically inserted into the next block by the protocol, before regular transactions.


4. CAT Execution: onDecrypt

  • In the next block, the runtime:
    1. Decrypts the moves during block finalization,
    2. Invokes the contract’s onDecrypt callback:
      function onDecrypt(
          bytes[] calldata decryptedArguments, // [p1Move, p2Move]
          bytes[] calldata plaintextArguments  // [gameId, p1, p2]
      ) external;
      
  • The contract:
    • Parses moves from decryptedArguments,
    • Reconstructs context (gameId, players) from plaintextArguments,
    • Determines the winner using Rock-Paper-Scissors rules,
    • Marks the game as finished,
    • Emits WinnerDecided event.

Security Controls

  1. Pending CAT flag

    • The contract tracks pendingCat = true when scheduling a CAT.
    • Prevents duplicate scheduling and ensures only one CAT is expected.
  2. Caller verification

    • Ensures that the CAT’s msg.sender matches the original caller of decryptAndExecute.
    • Prevents unauthorized external calls to onDecrypt.
  3. Game state

    • finished flag ensures a game can’t be replayed after a winner is decided.

Events

  • GameCreated(gameId, p1, p2)
    → emitted when a new game is initialized.

  • EncryptedMoveSubmitted(gameId, player)
    → emitted when a player submits their encrypted move.

  • WinnerDecided(gameId, winner, p1Move, p2Move)
    → emitted when the CAT transaction executes and the winner is determined.


Example Sequence

  1. Block N

    • Player 1 submits encrypted move.
    • Player 2 submits encrypted move.
    • Contract calls decryptAndExecute, scheduling a CAT.
  2. Block N+1

    • During finalization, encrypted moves are decrypted.
    • CAT executes onDecrypt, passing [p1Move, p2Move] and context [gameId, p1, p2].
    • Contract decides winner and emits WinnerDecided.

Sealed-Bid Auction Example (BITE Phase 2)

This example demonstrates how to implement a first-price sealed-bid auction using BITE Phase 2.

  • Bidders submit their encrypted bids along with an ETH deposit.
  • Once the bidding period ends, the contract schedules a Contract-Action-Transaction (CAT) that decrypts all bids in the next block.
  • The contract’s onDecrypt callback then determines the highest bidder, finalizes the auction, and transfers the funds.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IBitePhase2 {
    function decryptAndExecute(
        bytes[] calldata encryptedArguments,
        bytes[] calldata plaintextArguments
    ) external;
}

contract SealedBidAuction {
    // -------------------- Config --------------------
    address constant PRECOMPILE_ADDR = 0x0000000000000000000000000000000000000100;
    IBitePhase2 constant BITE = IBitePhase2(PRECOMPILE_ADDR);

    address public seller;
    uint256 public biddingDeadline;
    bool public finalized;

    // -------------------- Storage --------------------
    struct Bid {
        address bidder;
        bytes encBid;    // encrypted bid (decrypted later)
        uint256 deposit; // deposit in ETH
    }

    Bid[] public bids;
    bool public pendingCat;
    address public expectedCaller;

    // -------------------- Events --------------------
    event BidSubmitted(address indexed bidder, uint256 deposit);
    event AuctionFinalized(address winner, uint256 amount);

    // -------------------- Init --------------------
    constructor(uint256 _biddingPeriod) {
        seller = msg.sender;
        biddingDeadline = block.timestamp + _biddingPeriod;
    }

    // -------------------- Bidding --------------------
    function submitEncryptedBid(bytes calldata encBid) external payable {
        require(block.timestamp < biddingDeadline, "bidding closed");
        require(msg.value > 0, "deposit required");

        bids.push(Bid({
            bidder: msg.sender,
            encBid: encBid,
            deposit: msg.value
        }));

        emit BidSubmitted(msg.sender, msg.value);
    }

    // -------------------- Close auction --------------------
    function closeAuction() external {
        require(block.timestamp >= biddingDeadline, "still open");
        require(!pendingCat && !finalized, "already scheduled/finalized");

        // Build arrays for CAT call
        bytes[] memory encArgs = new bytes[](bids.length);
        bytes ; // auction context: total bids

        for (uint256 i = 0; i < bids.length; i++) {
            encArgs[i] = bids[i].encBid;
        }
        plainArgs[0] = abi.encode(bids.length);

        pendingCat = true;
        expectedCaller = msg.sender;

        // Schedule CAT to decrypt all bids in the next block
        BITE.decryptAndExecute(encArgs, plainArgs);
    }

    // -------------------- CAT callback --------------------
    function onDecrypt(
        bytes[] calldata decryptedArguments, // decrypted bid values
        bytes[] calldata plaintextArguments  // [numBids]
    ) external {
        require(pendingCat && !finalized, "no pending auction");
        require(msg.sender == expectedCaller, "unexpected caller");

        uint256 numBids = abi.decode(plaintextArguments[0], (uint256));
        require(numBids == bids.length, "mismatch");

        // Find highest bid
        uint256 highestAmount = 0;
        uint256 winnerIndex = type(uint256).max;

        for (uint256 i = 0; i < numBids; i++) {
            uint256 amount = abi.decode(decryptedArguments[i], (uint256));
            if (amount > highestAmount && bids[i].deposit >= amount) {
                highestAmount = amount;
                winnerIndex = i;
            }
        }

        // Finalize auction
        finalized = true;
        pendingCat = false;
        expectedCaller = address(0);

        if (winnerIndex != type(uint256).max) {
            // Pay seller
            payable(seller).transfer(highestAmount);

            // Refund losers + excess deposit
            for (uint256 i = 0; i < numBids; i++) {
                if (i == winnerIndex) {
                    uint256 refund = bids[i].deposit - highestAmount;
                    if (refund > 0) payable(bids[i].bidder).transfer(refund);
                } else {
                    payable(bids[i].bidder).transfer(bids[i].deposit);
                }
            }

            emit AuctionFinalized(bids[winnerIndex].bidder, highestAmount);
        } else {
            // No valid bids, refund everyone
            for (uint256 i = 0; i < numBids; i++) {
                payable(bids[i].bidder).transfer(bids[i].deposit);
            }
            emit AuctionFinalized(address(0), 0);
        }
    }
}

Auction Flow

Bidding Phase

  • Users call submitEncryptedBid(encBid) with their encrypted bid and ETH deposit.
  • Deposits ensure bidders cannot underfund their bid.

Closing Phase

  • Once the deadline passes, closeAuction() schedules a CAT:
    BITE.decryptAndExecute(encArgs, plainArgs)
    
1 Like

TL;DR - great writeup by Stan. I think BITE Phase 2 is going to unlock incredible innovative applications (see my ideas). I think some small questions regarding limits and economics, but in general a very positive next step.

Hey @kladkogex

Fantastic introduction to the technology and some of the possibilities with BITE Phase II.
A couple of questions and ideas to add from my side.

Questions

1. Is there a risk to not having the encrypted data have a gas limit? I.e is this a vector for a DoS attack in some way?
2. Economically the execution should be positive for the network. I wonder if this should be modeled after Chainlink?

They utilize a system of callbacks where there are two functions: call + callback.

a. During call, initial request → user pays gas fee + BITE fee + callback fee
b. During callback, process works like yours but validators have already been paid for that compute

The interesting part where this system would differ is the encrypted nature i.e we don’t know how much compute is being used. That’s why I’m wondering if there should be a limit and if that cost starts out as flat and covers the whole limit.

I think this is a novel enough feature that have a “basic” pricing model that can be fine tuned later is 100% ok to me as a developer. It’s how most things work until they reach a deep maturity and adoption to both incentivize and simplify developer onboarding.

3. Less of a question to start and more a comment. I think we should again consider modeling off of strong players in the space. Both LayerZero and Chainlink utilize this design for posting information back to the chain. (Question at the end of the next paragraph).

They have their contract i.e FunctionsClient from Chainlink and a function called fulfillRequest. Specifically the function is internal and is overridden. The client contract (which LayerZero operates in a similar manner) ensures that functions are only callable by the right people. So in this case it’s a question of can we do an onDecrypt function that is internal and overridable in your contract and then have a top level contract that is inherited which has an external function that is callable ONLY by committee?

4. Are there throughput limitations for N+1 execution?

Simply put, if there are 10,000 functions that should be executed in the next block how does that impact performance? Can that impact finality of a transaction or the size of a block?

As this is a brand new primitive type of execution in a blockchain VM I don’t doubt there will be nuances. I think catering to the Bite Phase II is critical as new innovative projects should rely heavily on this.

Ideas

Encrypted Triggers for Cross dApp Execution

This is the most generic, yet potentially the most powerful idea.

The concept of a trigger is actually very common in technology and is often hidden behind cron jobs, webhooks, etc. However, they are potentially one of the most missed “things” (you know I hate the word things) in blockchain.

Why? → Triggers make dApps composable for normal people. In case you missed it, I did a writeup on the rise of the machine economy and talk specifically about why I think blockchains are NOT build for humans, but for machines.

Fun Fact → you can chain together many calls to different protocols and dApps by going directly to the smart contracts. Adding triggers I think can help making multiple things more composable for users because it allows people to think in a more traditional sense. This will simultaneously give AI agents super powers.

Encrypted Triggers for Trading (Orders)

Bob wants to buy 10,000 SKL tokens them WHEN the price of BTC hits $130,000 USD.

  1. This can be built offchain (today)
  2. This could be built onchain today using a series of a smart contracts and protocols
  3. This CANNOT be done securely for the user today

How does BITE Phase II solve this?

A dApp can now create triggers based on unique information, i.e price information.

function createTrigger(bytes32 tokenId, uint256 triggerPrice, bytes call data encTrigger) {...}

This provides a set of protections for the user/caller:

  1. If this is being done on an orderbook onchain, those trying to place orders using triggers very close to the mark could be front run. NOT with BITE.
  2. The trigger is public which creates a VERY unique dynamic almost similar to perps/futures where people can see actions stacking up on a price. I.e on BTC hitting $130,000 → there are 2,000 triggers. This can drive excitement around events onchain.
  3. This gives major flexibility for dApps to use any set of information they want for proving
  4. Most importantly, the encrypted execution data is encrypted and private. This means that traders, agents, market makers, etc. can utilize this feature to take positions directly onchain without having to give away strategy. (Speed is cool, but speed can’t compete with this).

Sentiment Based Automation

This is actually a very similar concept to the above, but calls out that there are many ways to make decisions. I’ve been having a variety of conversations with teams around the space and a common theme is using unique data to drive prediction markets or decisions.

Example – if the sentiment on a token, a person, a group, a team, a country, an app swings to some % positive or some amount of time negative then execute some arbitrary code.

This is again very very novel so just an idea but I can imagine that this is almost like the equivalent of a live prediction market that would be much faster paced. One possibility is using Exorde sentiment data to then drive execution like:

If sentiment of X is Y, execute Z.

The unique part is that we can now stack sentiment with other actions. While this can be done today as a basic prediction market, adding FAIR and BITE would increase user security and pricing. However, stacking this with other protocols we can now do things like:

a. If crypto sentiment hits X, execute encrypted action.
b. If public sentiment of X hits Y, sell tokens

Conclusion

I think BITE Phase II is a major step forward for FAIR and the SKALE Ecosystem.
There are some smaller items to discuss architecturally, but in general I think an entire wave of applications will be created on FAIR based on these primitives that are not possible elsewhere.

1 Like