Skip to content

A CKB lock script that delegates the actual logic to another script

Notifications You must be signed in to change notification settings

yuqiliu617/Delegate-Lock

Repository files navigation

Delegate Lock

Delegate Lock is a lock script that proxies verification to another script stored in a separate cell. This allows users to update the ownership or unlocking logic of cells without changing the lock script hash, preserving the cell's on-chain identity.

Data Structure

Delegate Lock Args

The args field stores the blake160 hash of the type script of the Type ID cell containing the actual lock script. This truncation follows the same convention as other CKB lock scripts and provides sufficient collision resistance while minimizing on-chain storage.

Witness

Delegate Lock delegates the verification logic. Therefore, the witness structure should follow the format expected by the actual lock script. Delegate Lock itself does not impose additional witness requirements.

Usage Flow

Lock a Cell

First, create a Type ID cell to store the actual lock script:

Inputs:
    Seed Cell
Outputs:
    Type ID Cell:
        Lock: <lock script for Type ID cell itself>
        Type:
            code_hash: <Type ID code hash>
            hash_type: type
            args: <type id>
        Data: <actual lock script, molecule-encoded>
            code_hash: <actual lock script code hash>
            hash_type: <actual lock script hash type>
            args: <actual lock script args>

Second, lock the target cell using Delegate Lock, referencing the Type ID cell:

Outputs:
    Target Cell:
        Lock:
            code_hash: <Delegate Lock code hash>
            hash_type: type
            args: <blake160 hash of the Type ID cell's type script>

Unlock a Cell

To unlock the cell, the transaction must include the Type ID cell and the actual lock script binary cell as dependencies:

CellDeps:
    Type ID Cell:
        Type:
            code_hash: <Type ID code hash>
            hash_type: type
            args: <type id>
        Data: <actual lock script, molecule-encoded>
            code_hash: <actual lock script code hash>
            hash_type: <actual lock script hash type>
            args: <actual lock script args>
    Actual Lock Script Binary Cell:
        Data: <actual lock script binary>
Inputs:
    Delegate Lock Cell:
        Lock:
            code_hash: <Delegate Lock code hash>
            hash_type: type
            args: <blake160 hash of the Type ID cell's type script>
Witnesses:
    <as required by the actual lock script>

Update Ownership

To change the ownership, update the data in the Type ID cell.

Inputs:
    Old Type ID Cell:
        Lock: <lock script for Type ID cell itself>
        Type:
            code_hash: <Type ID code hash>
            hash_type: type
            args: <type id>
        Data: <old actual lock script, molecule-encoded>
Outputs:
    New Type ID Cell:
        Lock: <lock script for Type ID cell itself>
        Type:
            code_hash: <Type ID code hash>
            hash_type: type
            args: <type id>
        Data: <new actual lock script, molecule-encoded>

Chained Delegation

The actual lock script in a Type ID cell can itself be Delegate Lock, enabling multiple levels of delegation where each level can be updated independently.

CellDeps:
    Outer Type ID Cell:
        Type:
            args: <outer type id>
        Data: <Script pointing to Delegate Lock with blake160(inner type script) as args>
    Inner Type ID Cell:
        Type:
            args: <inner type id>
        Data: <actual lock script, molecule-encoded>
    Actual Lock Script Binary Cell:
        Data: <actual lock script binary>
Inputs:
    Delegate Lock Cell:
        Lock:
            code_hash: <Delegate Lock code hash>
            args: <blake160 hash of the Type ID cell's type script>
Witnesses:
    <as required by the actual lock script>

Delegation Convention

A lock script typically loads its arguments via the ckb_load_script syscall. However, when used with Delegate Lock, the actual lock script runs in Delegate Lock's context, so ckb_load_script returns the Delegate Lock script instead of the expected arguments.

To address this, Delegate Lock uses the following convention when invoking the actual lock script:

  • It uses ckb_exec to execute the actual lock script.
  • source is set to CKB_SOURCE_CELL_DEP (3), place is set to 0 (cell data), and bounds is set to 0 (entire data).
  • The lock script arguments from the Type ID cell data are passed via argc and argv parameters of ckb_exec.
    • argc is always 2
    • argv[0] contains the magic string "DELEGATE_LOCK", which the actual lock script must verify to confirm it was invoked by Delegate Lock.
    • argv[1] contains the hex-encoded script args from the Type ID cell data.
  • Delegate lock itself does not impose any additional witness requirements. The actual lock script can load its witness as usual.

Therefore, the actual lock script must read its arguments from argc and argv instead of ckb_load_script. Since most existing lock scripts use ckb_load_script, modified versions are provided in this repository (see Migrated Lock Scripts below).

To adapt an existing lock script to work with Delegate Lock, follow the following migration guide:

// Old way: load from script directly
fn run() -> Result<(), Error> {
    let script = ckb_std::high_level::load_script()?;
    let args: Bytes = script.args().unpack();
    // ...
}
// New way: load from argv
const DELEGATE_LOCK_MAGIC: &[u8] = b"DELEGATE_LOCK";

fn run() -> Result<(), Error> {
    let argv = ckb_std::env::argv();
    if argv.len() != 2 || argv[0].to_bytes() != DELEGATE_LOCK_MAGIC {
        return Err(Error::ArgsInvalid);
    }
    let args_hex = argv[1].to_bytes();
    let args = decode_hex(args_hex)?;
    // ...
}
fn decode_hex(hex: &[u8]) -> Result<Vec<u8>, Error> {
    if hex.len() % 2 != 0 {
        return Err(Error::ArgsInvalid);
    }
    let mut bytes = Vec::with_capacity(hex.len() / 2);
    for chunk in hex.chunks(2) {
        let high = hex_digit_to_value(chunk[0])?;
        let low = hex_digit_to_value(chunk[1])?;
        bytes.push((high << 4) | low);
    }
    Ok(bytes)
}
fn hex_digit_to_value(c: u8) -> Result<u8, Error> {
    match c {
        b'0'..=b'9' => Ok(c - b'0'),
        b'a'..=b'f' => Ok(c - b'a' + 10),
        b'A'..=b'F' => Ok(c - b'A' + 10),
        _ => Err(Error::ArgsInvalid),
    }
}

Migrated Lock Scripts

The following lock scripts have been adapted to work with Delegate Lock by reading arguments from argv instead of ckb_load_script:

Script Original
secp256k1-blake160-sighash-all CKB System Scripts
secp256k1-blake160-multisig-all CKB System Scripts
ccc-btc CCC Locks
ccc-eth CCC Locks
ccc-sol CCC Locks

Security Considerations

  • Type ID Cell Protection: The security of cells locked by Delegate Lock depends entirely on the lock script of the Type ID cell. If an attacker can modify the Type ID cell's data, they gain control over all cells referencing that Type ID. Choose a secure lock script for the Type ID cell.
  • Actual Lock Script Trust: The actual lock script executed via ckb_exec must be trusted. Delegate Lock does not verify the correctness of the delegated script beyond matching the Type ID.

About

A CKB lock script that delegates the actual logic to another script

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published