BITE Phase 2 MVP Specification
Authors: Oleh Nikolaev, Stan Kladko
With BITE Phase 2, each block can include Conditional Transactions (CTXs) — transactions initiated by smart contracts execution in the previous block.
CTXs enable smart contracts to decrypt data and 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.
- Encrypted Automation: I think the pure automation piece is very strong but adding in the encryption makes it even more relevant to those building complicated actions
- Efficiency: Decryption is done in the same batch as BITE Phase 1, so no extra performance overhead.
- Determinism: Execution happens in a predictable order (CTXs run before regular transactions in block N+1).
Conditional Transactions (CTXs)
- 
A Smart Contract (SC) in block N calls decryptAndExecute precompile passing an encryptedAruments array and an plaintextArguments array of plaintext arguments and gasLimit for the future CTX transaction. 
- 
A CTX transaction is added to the next block. CTX transactions are placed in front of regular transactions in the block. They are not subject to the block gas limit. 
- 
CTX transactions have the SC’s address that originated them as tx.origin. Therefore, SC issued a CTX is charged for it. 
- 
CTX transaction to field is the SC that originated it. The SC sends a transaction to itself. 
- 
CTX transaction always calls onDecrypt function of the SC that originated them. 
- 
CTX 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 to BITE Phase 1. 
decryptAndExecute Precompile
This function creates a CTX transaction
```solidity
/**
Create a CTX 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.
*  @param gasLimit An integer representing the gas limit that will be set for transaction execution after decrypting all arguments.
 */
function decryptAndExecute(
    bytes[] calldata encryptedArguments,
    bytes[] calldata plaintextArguments,
    uint256 gasLimit
) external;
Precompile gas cost
Price for executing decryptAndExecute precompiled contract should be a linear function of the size of encryptedArguments (aka P * N + S, where N - number of encrypted arguments, P - price for decrypting 1 argument, S - price for inserting a txn into txn queue). Exact values for P and S can be decided later.
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.
```solidity
/**
  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;
Encrypted argument spec
Each encrypted argument will have the same RLP format as for BITE Phase 1 encrypted data field. When the data is decrypted and is passed to skaled for execution, skaled will verify that txn.to == decryptedData.to, otherwise such transaction will not be executed.
CTX flow
The CTX flow will be the following:
- user P creates a payload and encrypts it
- user P pays fees for their txn to contract SC
- if a CTX is created, gasLimit and gasPrice values are set for it. SC, that created a CTX, will be charged for it.
- its SC owner’s responsibility to ensure that SC has a sufficient balance to issue CTXs.
- if SC does not have enough balance to be charged for precompiled contract interaction and future CTX execution, CTX will not be added to txn queue
Charging funds for CTXs
SC issued a CTX is charged for it. Skaled should use default gasPrice (eth_gasPrice) to set it for CTXs. GasLimit parameter for CTXs is passed from the original contract. SC developer can pre-charge users who submit encrypted data so that the SC has always enough funds to submit CTXs.
Implementation
Database
All CTXs added during the execution of block N should be written to the blocks_and_extras database under the key lastBlockCTXs. This is needed to keep the system’s integrity in case any of the nodes suddenly go offline (or in case of scheduled restarts).
Read-only calls
Calling decryptAndExecute precompiled SC should be explicitly denied from readonly context to avoid DoS attacks.
Transaction queue
New private calls insertCTX(const Transaction& _ctx) and pendingCTXs() should be added to the transaction queue to support inserting CTXs by decryptAndExecute precompiled SC and fetching them by consensus.
Skaled-Consensus interface
Skaled-consensus interface is modified to pass CTX 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& _ctxTransactions
#endif
    );
Consensus
CTXs require a different way for processing because they don’t have any signatures associated with them.
CTXs inserted into a block by protocol itself. CTXs cannot be downloaded or broadcasted during block proposal stage. Decryption shares for CTXs generated in the same way as for BITE Phase 1 transactions. CTXs should be included into the block when it is requested via catchup.
Display CTXs via JSON-RPC calls
CTXs should still be present in response for eth_getBlock call and others. To prevent potential issues when integrating with external partners, all CTXs will be assigned a random ECDSA signature to them. Internally they will not have any signatures, signature validation will be skipped for CTXs. BITE2 transactions’s RLP will include MAGIC_NUMBER to differentiate them among standard transactions when read from the database.
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
```solidity
// 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 CTX 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 CTX callback is expected
        bool pendingCtx;
        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 CTX yet, schedule it now.
        if (g.p1Submitted && g.p2Submitted && !g.pendingCtx) {
            g.pendingCtx = true;
            g.expectedCaller = msg.sender; // per spec, CTX 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 CTX; no return value
            BITE.decryptAndExecute(encArgs, plain);
        }
    }
    /**
     * @notice CTX callback (executed in Block N+1). Receives decrypted moves and our plaintext context.
     * Security notes for MVP:
     *  - We gate by `pendingCtx` and by `expectedCaller` (the account that scheduled the CTX).
     *  - In production, consider adding a CTX 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.pendingCtx, "no pending CTX");
        require(msg.sender == g.expectedCaller, "unexpected caller (not CTX 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.pendingCtx = 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 (CTXs).
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 CTX transaction to decrypt the moves and determine the winner.
Game Flow
- Game Creation
 A player callscreateGame(opponent)to set up a new game.
 The contract stores:
 p1(creator),
 p2(opponent),
 and assign agameId.
- Emits GameCreated event.
- Submitting Encrypted Moves
 Each player callssubmitEncryptedMove(gameId, encMove)with their encrypted move.
 Moves are stored in the contract:
 encMove1for player 1,
 encMove2for player 2.
- Emits EncryptedMoveSubmitted event.
- Scheduling CTX Decryption
 Once both moves are submitted:
 The contract calls the BITE Phase 2 precompile:
```solidity
    BITE.decryptAndExecute(encArgs, plainArgs);
    ```
  - `encArgs` = `[encMove1, encMove2]` (encrypted moves).
  - `plainArgs` = `[gameId, p1, p2]` (context info to reconstruct the game).
This creates a CTX transaction that will:
- Run in the next block,
- Call onDecrypt(decryptedMoves, plaintextArgs).
Important: CTX transactions are not user-submitted. They are automatically inserted into the next block by the protocol, before regular transactions.
- CTX Execution: onDecrypt
- In the next block, the runtime:
- Decrypts the moves during block finalization,
- Invokes the contract’s onDecryptcallback:
 
solidity
     function onDecrypt(
         bytes[] calldata decryptedArguments, // [p1Move, p2Move]
         bytes[] calldata plaintextArguments  // [gameId, p1, p2]
     ) external;
 
The contract:
- Parses moves from decryptedArguments,
- Reconstructs context (gameId, players) fromplaintextArguments,
- Determines the winner using Rock-Paper-Scissors rules,
- Marks the game as finished,
- Emits WinnerDecided event.
Security Controls
- 
Pending CTX flag - The contract tracks pendingCtx = truewhen scheduling a CTX.
- Prevents duplicate scheduling and ensures only one CTX is expected.
 
- The contract tracks 
- 
Caller verification - Ensures that the CTX’s msg.sendermatches the original caller ofdecryptAndExecute.
- Prevents unauthorized external calls to onDecrypt.
 
- Ensures that the CTX’s 
- 
Game state - 
finishedflag 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 CTX transaction executes and the winner is determined.
Example Sequence
- 
Block N - Player 1 submits an encrypted move.
- Player 2 submits an encrypted move.
- Contract calls decryptAndExecute, scheduling a CTX.
 
- 
Block N+1 - During finalization, encrypted moves are decrypted.
- CTX 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 (CTX) that decrypts all bids in the next block.
- The contract’s onDecryptcallback then determines the highest bidder, finalizes the auction, and transfers the funds.
solidity
// 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 pendingCtx;
    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(!pendingCtx && !finalized, "already scheduled/finalized");
        // Build arrays for CTX 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);
        pendingCtx = true;
        expectedCaller = msg.sender;
        // Schedule CTX to decrypt all bids in the next block
        BITE.decryptAndExecute(encArgs, plainArgs);
    }
    // -------------------- CTX callback --------------------
    function onDecrypt(
        bytes[] calldata decryptedArguments, // decrypted bid values
        bytes[] calldata plaintextArguments  // [numBids]
    ) external {
        require(pendingCtx && !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;
        pendingCtx = 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 CTX:
solidity
  BITE.decryptAndExecute(encArgs, plainArgs)