Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Delegate/Entry Script

The delegate script is a 53-byte P2SH redeem script that allows funds to ride alongside a permission input. It serves dual duty: it locks deposit outputs for entries and provides bridge reserve inputs for withdrawals.

Purpose

When a user deposits funds (entry action), the funds must be locked in a way that:

  1. Only the covenant system can spend them
  2. The guest can verify the deposit is genuine

When a withdrawal is processed, the permission script needs additional inputs to fund the withdrawal. Delegate inputs provide this liquidity.

Script structure

flowchart TD
    CHECK_IDX["Step 1: OpTxInputIndex > 0?<br/>Self is not at input 0"]
    CHECK_COV["Step 2: Input 0 has<br/>expected covenant_id?"]
    CHECK_SUFFIX["Step 3: Input 0's sig_script<br/>ends with [0x51, 0x75]?"]
    RESULT["OpTrue"]

    CHECK_IDX -->|"Verify"| CHECK_COV
    CHECK_COV -->|"EqualVerify"| CHECK_SUFFIX
    CHECK_SUFFIX -->|"EqualVerify"| RESULT

The script is 53 bytes:

OffsetSizeContent
0-34BIndex check: OpTxInputIndex Op0 OpGreaterThan OpVerify
4-63BCovenant preamble: Op0 OpInputCovenantId OpData32
7-3832BEmbedded covenant_id
391BOpEqualVerify
40-5112BSuffix check: extract last 2 bytes of input 0’s sig_script, compare with [0x51, 0x75]
521BOpTrue

See core/src/p2sh.rs:103-156 for build_delegate_entry_script_bytes.

Three verification steps

Step 1: Not at input 0

OpTxInputIndex  Op0  OpGreaterThan  OpVerify

The delegate script must not be at input index 0. Index 0 is reserved for the permission script (or state verification script). This prevents the delegate from being used as the primary covenant input.

Step 2: Covenant ID match

Op0  OpInputCovenantId  OpData32  <covenant_id>  OpEqualVerify

Checks that input 0 carries the expected covenant_id. This binds the delegate to a specific covenant instance — it cannot be co-spent with a different covenant’s permission script.

Step 3: Permission domain suffix

Op0 Op0 OpTxInputScriptSigLen OpDup Op2 OpSub OpSwap OpTxInputScriptSigSubstr
push-2 0x51 0x75 OpEqualVerify

Extracts the last 2 bytes of input 0’s sig_script and verifies they are [0x51, 0x75] (the permission domain suffix). This ensures the delegate is co-spending with a permission input specifically, not a state verification input.

Deposit SPK verification

When a user creates an entry (deposit) transaction, the output must pay to P2SH(delegate_script(covenant_id)). The guest verifies this:

#![allow(unused)]
fn main() {
/// Verify that an entry (deposit) transaction output SPK is a P2SH wrapping the
/// correct delegate/entry script for the given covenant.
///
/// This ensures deposited funds are actually locked in the covenant, preventing
/// a malicious host from crediting L2 accounts for funds sent to arbitrary addresses.
pub fn verify_entry_output_spk(spk: &[u8], covenant_id: &[u32; 8]) -> bool {
    let delegate = build_delegate_entry_script_bytes(covenant_id);
    verify_p2sh_spk(spk, &delegate)
}
}

This reconstructs the expected delegate script from the covenant_id, hashes it with blake2b, and compares with the output’s P2SH script hash.

Entry input guard

The guest also checks that entry transactions do NOT have a permission script as input 0:

#![allow(unused)]
fn main() {
/// Check if the first input's sig_script ends with the permission domain suffix.
///
/// Returns `true` if input 0's sig_script ends with `[0x51, 0x75]` (`OP_TRUE OP_DROP`),
/// which identifies the permission script domain. This is used by the guest to reject
/// entry (deposit) transactions whose first input is a permission script, preventing
/// delegate change outputs from being counted as new deposits.
///
/// The transaction byte format is the same as documented on [`parse_output_at_index`].
pub fn input0_has_permission_suffix(tx_bytes: &[u8]) -> bool {
    let mut cursor = 0;

    // Skip version (2 bytes)
    cursor += 2;
    if cursor > tx_bytes.len() {
        return false;
    }

    // Read num_inputs
    let num_inputs = match read_u64(tx_bytes, &mut cursor) {
        Some(n) => n,
        None => return false,
    };
    if num_inputs == 0 {
        return false;
    }

    // Skip prev_tx_id (32 bytes) + prev_index (4 bytes)
    cursor += 36;
    if cursor > tx_bytes.len() {
        return false;
    }

    // Read sig_script_len
    let sig_len = match read_u64(tx_bytes, &mut cursor) {
        Some(n) => n as usize,
        None => return false,
    };

    if sig_len < 2 || cursor + sig_len > tx_bytes.len() {
        return false;
    }

    // Check last 2 bytes of sig_script: [0x51, 0x75] = OP_TRUE, OP_DROP
    let sig_end = cursor + sig_len;
    tx_bytes[sig_end - 2] == 0x51 && tx_bytes[sig_end - 1] == 0x75
}
}

This prevents a subtle attack: without this guard, the delegate change output from a withdrawal transaction could be misinterpreted as a new deposit. The permission suffix check distinguishes withdrawal change from genuine deposits.

Design rationale

Why a separate script? The delegate script allows bridge reserve funds to be pre-positioned in UTXOs that can only be spent alongside a permission input. This avoids requiring the entire bridge reserve in a single UTXO.

Why check the suffix? The covenant_id check alone is insufficient — it would allow co-spending with a state verification input (which has suffix [0x00, 0x75]). The suffix check ensures delegates only participate in withdrawal transactions.

Why MAX_DELEGATE_INPUTS = 2? The permission script unrolls the delegate input loop. More inputs mean a larger script. Two delegate inputs are sufficient for the PoC while keeping the script compact.