Documentation Index
Fetch the complete documentation index at: https://docs.usewraith.xyz/llms.txt
Use this file to discover all available pages before exploring further.
CKB scripts (smart contracts) for stealth address operations on the Nervos CKB network. Written in Rust and compiled to RISC-V binaries that run on CKB-VM.
The Cell Model — Why CKB Is Different
CKB uses a UTXO-based Cell model, not accounts. Every piece of on-chain state is a Cell:
Cell {
capacity: 200_00000000, // 200 CKB (in shannons)
lock: { // who can spend this Cell
code_hash: "0xabc...", // hash of the lock script code
hash_type: "data2",
args: "0x...", // arguments passed to the lock script
},
type: null, // optional type script
data: "0x", // arbitrary data
}
No Separate Announcer
On EVM chains, stealth payments require two operations: transfer funds and emit an announcement event. On CKB, the Cell itself is the announcement. The lock script args contain both the ephemeral public key and the stealth address hash:
lock.args = ephemeral_pubkey (33 bytes) || blake160(stealth_pubkey) (20 bytes)
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
announcement data stealth address identifier
This 53-byte args field serves as both the lock condition and the announcement. No separate contract call, no event indexing infrastructure — just create a Cell and the recipient can find it.
Scanning = Querying Cells
CKB has a built-in indexer with an RPC method get_cells that filters by lock script code hash. To scan for incoming payments, query all live Cells using the stealth-lock code hash. Each Cell’s args contains the ephemeral key needed for the ECDH check.
Spending = Consuming Cells
To withdraw funds, build a transaction that:
- Inputs: The stealth Cell being consumed
- Outputs: A new Cell at the destination address
- Witness: secp256k1 signature with the stealth private key
The stealth lock script verifies the signature against the pubkey hash in the args.
Script Set
| Script | Type | Purpose |
|---|
| wraith-stealth-lock | Lock Script | Verifies secp256k1 signatures for stealth Cells |
| wraith-names-type | Type Script | .wraith name registration and resolution |
wraith-stealth-lock handles stealth payments. wraith-names-type handles .wraith name registration.
wraith-stealth-lock
A lock script that verifies secp256k1 signatures against the stealth public key hash embedded in the script args.
53 bytes total:
[0:33] — ephemeral_pubkey (33-byte compressed secp256k1 public key)
[33:53] — blake160(stealth_pub) (20-byte blake2b hash of stealth public key)
Verification Flow
fn program_entry() -> i8 {
let script = load_script();
let args = script.args().raw_data();
// 1. Args must be exactly 53 bytes
if args.len() != 53 {
return ERROR_ARGS_LENGTH;
}
// 2. Extract the stealth pubkey hash
let pubkey_hash = &args[33..53]; // blake160 of stealth pubkey
// 3. Load signature from witness (65 bytes: r || s || v)
let witness_args = load_witness_args(0, Source::GroupInput);
let signature = witness_args.lock();
// 4. Load transaction hash as the message to verify
let tx_hash = load_tx_hash();
// 5. Recover public key from signature
// 6. Compute blake160(recovered_pubkey)
// 7. Compare with pubkey_hash from args
verify_secp256k1(tx_hash, signature, pubkey_hash)
}
The script uses ckb-std crypto syscalls or ckb-auth for secp256k1 signature recovery and verification.
blake160
CKB’s standard address hashing:
fn blake160(data: &[u8]) -> [u8; 20] {
let hash = blake2b_256_with_personalization(data, b"ckb-default-hash");
hash[0..20]
}
The "ckb-default-hash" personalization is required. Without it, hashes won’t match CKB’s standard address format.
How It Differs from Standard CKB Locks
CKB’s default lock script (secp256k1-blake160-sighash-all) uses 20-byte blake160 args containing the owner’s pubkey hash. The stealth lock extends this to 53 bytes by prepending the ephemeral public key. The verification logic is the same — recover the public key from the signature and check the blake160 hash.
Usage
import { generateStealthAddress, getDeployment } from "@wraith-protocol/sdk/chains/ckb";
const deployment = getDeployment("ckb");
const stealth = generateStealthAddress(spendingPubKey, viewingPubKey);
// Create a Cell with stealth-lock
const output = {
capacity: "0x" + (200_00000000n).toString(16), // 200 CKB
lock: {
codeHash: deployment.contracts.stealthLockCodeHash,
hashType: "data2",
args: stealth.lockArgs, // 53 bytes: ephemeral_pub || blake160(stealth_pub)
},
};
wraith-names-type
A Type Script for .wraith name registration on CKB. Each name is stored as a live Cell. The type script validates creation, update, and destruction of name Cells.
Deployed Code Hash
0xc133817d433f72ea16a2404adaf961524e9572c8378829a21968710d6182e20d
Cell Dep
tx: 0x9acd640d35eadd893b358dddd415f4061fe81cb249e8ace51a866fee314141b8
index: 0
Cell Layout
A .wraith name Cell has three meaningful fields:
| Field | Content |
|---|
type.args | 32-byte blake2b hash of the name string (personalization: "ckb-default-hash") |
data | 66 bytes: spendingPubKey (33 bytes) || viewingPubKey (33 bytes) |
lock | Owner’s lock script — whoever can spend this Cell owns the name |
The type script args uniquely identify the name. Two Cells cannot have the same type script (same code hash + same args) because the type script enforces uniqueness at creation time.
How It Works
The type script runs on every transaction that creates, updates, or destroys a name Cell. It inspects the transaction’s inputs and outputs to determine the operation:
- Create: The type script appears in an output but not in any input. A new name is being registered.
- Update: The type script appears in both an input and an output. The name’s data is being changed.
- Destroy: The type script appears in an input but not in any output. The name is being released.
Validation Rules
Data must be exactly 66 bytes. The data field holds two 33-byte compressed secp256k1 public keys (spending + viewing). The type script rejects any Cell with data that is not exactly 66 bytes. This applies to both creation and update.
Create: When a new name Cell is created, the type script verifies:
- No existing live Cell has the same type script (same code hash and args). CKB enforces this natively — two live Cells cannot share the same type script.
- The output data is exactly 66 bytes.
Update: When a name Cell is consumed and recreated in the same transaction:
- The new output data must be exactly 66 bytes.
- The lock script on the input Cell must be satisfied (the owner authorized the transaction).
Destroy: When a name Cell is consumed without being recreated:
- The lock script on the input Cell must be satisfied (the owner authorized the destruction).
- No further validation — the name is released and can be re-registered.
Ownership Model
On EVM and Solana, name ownership is proven by a signature from the spending key embedded in the meta-address. The contract verifies this signature on every register/update/release call.
On CKB, ownership is proven by the Cell’s lock script. The lock script determines who can spend (consume) the Cell. This is more flexible:
- A standard secp256k1 lock means a single key controls the name.
- A multisig lock means multiple parties must agree to update or transfer the name.
- Any custom lock script can control a name.
To transfer a name, consume the name Cell and create a new one with the same type script but a different lock script. The new lock script’s owner now controls the name.
Verification Flow
fn program_entry() -> i8 {
let script = load_script();
let mut has_input = false;
let mut has_output = false;
// Check inputs for Cells with this type script
for i in 0.. {
match load_cell_type(i, Source::GroupInput) {
Ok(Some(t)) if t.as_slice() == script.as_slice() => {
has_input = true;
break;
}
Err(SysError::IndexOutOfBound) => break,
_ => continue,
}
}
// Check outputs for Cells with this type script
for i in 0.. {
match load_cell_type(i, Source::GroupOutput) {
Ok(Some(t)) if t.as_slice() == script.as_slice() => {
has_output = true;
// Validate data is exactly 66 bytes
let data = load_cell_data(i, Source::GroupOutput);
if data.len() != 66 {
return ERROR_DATA_LENGTH;
}
break;
}
Err(SysError::IndexOutOfBound) => break,
_ => continue,
}
}
// Create: output exists, no input -> OK (CKB prevents duplicate type scripts)
// Update: both exist -> OK (lock script on input already verified)
// Destroy: input exists, no output -> OK (lock script on input already verified)
0
}
Usage
import {
hashName,
buildRegisterName,
buildResolveName,
metaAddressFromNameData,
getDeployment,
} from "@wraith-protocol/sdk/chains/ckb";
const deployment = getDeployment("ckb");
// Register a name
const { typeScript, data } = buildRegisterName({
name: "alice",
spendingPubKey: keys.spendingPubKey,
viewingPubKey: keys.viewingPubKey,
});
const output = {
capacity: "0x" + (130_00000000n).toString(16),
lock: walletLockScript,
type: {
codeHash: typeScript.codeHash,
hashType: typeScript.hashType,
args: typeScript.args,
},
};
// Cell dep: { txHash: "0x9acd640d...", index: 0, depType: "code" }
// Output data: data (66 bytes)
// Resolve a name
const { typeScript: resolveType } = buildResolveName({ name: "alice" });
// Query get_cells with script_type: "type" and the resolveType script
// Parse the Cell's output_data with metaAddressFromNameData()
Project Structure
contracts/
ckb/
Cargo.toml
contracts/
wraith-stealth-lock/
Cargo.toml
src/main.rs
wraith-names-type/
Cargo.toml
src/main.rs
tests/
src/tests.rs
Deployment
Build
Compile to RISC-V binary:
cargo build --target riscv64imac-unknown-none-elf --release
Deploy
Deploy the compiled binary as a Cell on CKB testnet. The Cell’s data hash becomes the code_hash used in stealth lock scripts.
# Deploy using ckb-cli or a deployment script
ckb-cli deploy gen-txs \
--deployment-config deployment.toml \
--migration-dir migrations
Record Deployment Info
After deployment, record:
- Code hash: The blake2b hash of the deployed binary (used as
lock.code_hash in stealth Cells)
- Cell dep: The transaction hash and index where the script binary Cell lives (required in every transaction that uses the lock)
Update the SDK’s deployments.ts:
export const DEPLOYMENTS = {
ckb: {
network: "testnet",
rpcUrl: "https://testnet.ckbapp.dev",
explorerUrl: "https://pudge.explorer.nervos.org",
contracts: {
stealthLockCodeHash: "0x...",
namesTypeCodeHash: "0xc133817d433f72ea16a2404adaf961524e9572c8378829a21968710d6182e20d",
},
cellDeps: {
stealthLock: {
txHash: "0x...",
index: 0,
},
namesType: {
txHash: "0x9acd640d35eadd893b358dddd415f4061fe81cb249e8ace51a866fee314141b8",
index: 0,
},
},
},
};
Testing
Tests use CKB’s native testing framework with ckb-testtool:
wraith-stealth-lock:
- Args validation — rejects args that aren’t exactly 53 bytes
- Signature verification — valid stealth key signature passes
- Wrong key rejection — signature from a different key fails
- blake160 correctness — hash matches CKB’s standard implementation
wraith-names-type:
- Data validation — rejects data that isn’t exactly 66 bytes
- Create — allows creating a name Cell with valid 66-byte data
- Update — allows updating data when the owner’s lock is satisfied
- Destroy — allows destroying a name Cell when the owner’s lock is satisfied
- Invalid data on create — rejects Cell creation with wrong data length
- Invalid data on update — rejects Cell update with wrong data length
Differences from Other Chains
| Aspect | EVM (Solidity) | Stellar (Soroban) | Solana (Anchor) | CKB |
|---|
| Model | Account | Account | Account | UTXO (Cell) |
| Language | Solidity | Rust | Rust (Anchor) | Rust (RISC-V) |
| Announcement | Separate event | Separate event | Separate event | Embedded in Cell |
| Scanning source | Subgraph | Soroban RPC | Program logs | get_cells RPC |
| Script count | 4 contracts | 4 contracts | 3 programs | 2 scripts (lock + type) |
| Address hash | keccak256 | N/A | N/A | blake2b (“ckb-default-hash”) |
| Shared secret hash | keccak256 | SHA-256 | SHA-256 | SHA-256 |
| Min balance | None | 1 XLM | ~0.00089 SOL | 61 CKB |
| Names | On-chain contract | On-chain contract | On-chain program | Type Script (Cell-based) |
The key architectural difference: CKB needs only two scripts to support stealth payments and names. The stealth lock handles payments (announcements are implicit in the Cell), and the names type script handles .wraith name registration. Other chains need 3-4 separate contracts for the same functionality.