Skip to main content

Omnilock Script

Omnilock is a Lock Script designed for interoperability. It comes with built-in support for verification of transaction signing methods used in Bitcoin, Ethereum, EOS, Dogecoin and more. It also includes a regulation compliance module, which allows an administrator to revoke tokens held by users under certain circumstances.

How Omnilock Works​

An Omnilock Script has the following structure:

Code hash: Omnilock script code hash
Hash type: Omnilock script hash type
Args: <21 byte auth> <Omnilock args>

There are 2 key fields in args:

  • auth: for authentication, generally with a pubkey hash in its content
  • Omnilock args: for extra checking control, allowing different modes to be enabled in the same Omnilock args

The Omnilock args can be without mode (when Omnilock flags = 0), while the auth must be present. The functionality of Omnilock Script without mode is almost the same as a traditional SECP256K1/blake160 Lock Script. The Omnilock Script can be considered as a traditional Lock Script with additional checking/modes.

Authentication​

An authentication (auth) is a 21-byte data structure containing the following components:

<1 byte flag> <20 bytes auth content>

Depending on the value of the flag, the auth content has the following interpretations:

  • 0x0: It represents the blake160 hash of a secp256k1 public key. The Lock Script performs secp256k1 signature verification, the same as the SECP256K1/blake160 lock.

  • 0x01: It follows the unlocking method used by Ethereum. The signing message hash (sighash_all, see reference implementation) is converted as following: "0x" + hex(signing message hash)

    The hex operator converts the binary into a hex string.

  • 0x03: It follows the unlocking method used by Tron. The signing message hash is converted as follows: "0x" + hex(signing message hash)

  • 0x04: It follows the unlocking method used by Bitcoin. The signing message hash is required to be converted as follows: "CKB (Bitcoin Layer) transaction: 0x" + hex(signing message hash)

    This way, it facilitates a neat presentation of messages on wallet interfaces, such as UniSat and OKX.

  • 0x05: It follows the unlocking method used by Dogecoin. The signing message hash is converted as follows: "0x" + hex(signing message hash)

  • 0x12: It follows the unlocking same method as 0x02, with the signing message hash to be converted as follows: "CKB transaction: 0x" + hex(signing message hash)

    This way, it facilitates a neat presentation of messages on wallet interfaces, such as MetaMask.

  • 0x06: It follows the same unlocking method used by CKB MultiSig with a minor modification. When a message is calculated for signing, there is a step to clear the witnesses. In Omnilock, this involves clearing the entire lock field in witnesses. But in CKB MultiSig Script, it is only a partial clearance. This part is used as signatures followed by multisig_script.

  • 0xFC: It represents the blake160 hash of a Lock Script that checks if the current transaction contains an input Cell with a matching Lock Script. Otherwise, it would return with an error, similar to P2SH in BTC.

  • 0xFD: It represents the blake160 hash of a preimage that contains exec information used to delegate signature verification to another Script via exec.

  • 0xFE: It represents the blake160 hash of a preimage that contains dynamic linking information used to delegate signature verification to the dynamic linking Script. The interface described in Swappable Signature Verification Protocol Spec is used here.

Omnilock args​

The structure of Omnilock args is as follows:

<1 byte Omnilock flags> <32 byte AdminList cell Type ID, optional> <2 bytes minimum ckb/udt in ACP, optional> <8 bytes since for time lock, optional> <32 bytes type script hash for supply, optional>
NameFlagsAffected ArgsAffected Args Size (byte)Affected Witness
administrator mode0b00000001AdminList cell Type ID32omni_identity/signature in OmniLockWitnessLock
anyone-can-pay mode0b00000010minimum ckb/udt in ACP2N/A
time-lock mode0b00000100since for timelock8N/A
supply mode0b00001000type script hash for supply32N/A

You can check out the details for different modes at RFC.

Witness Structure​

When unlocking an Omnilock, the corresponding witness must be a proper WitnessArgs data structure in Molecule format. In the lock field of the WitnessArgs, an OmniLockWitnessLock structure must be present as follows:

import xudt_rce;

array Auth[byte; 21];

table Identity {
identity: Auth,
proofs: SmtProofEntryVec,
}
option IdentityOpt (Identity);

// the data structure used in lock field of witness
table OmniLockWitnessLock {
signature: BytesOpt,
omni_identity: IdentityOpt,
preimage: BytesOpt,
}

To learn the detail of witness in Omnilock, check out the RFC

Script Deployed Info​

The Omnilock spec has been deployed to CKB's Mainnet Mirana and Testnet Pudge. The following are the deployment details:

Mainnet​

parametervalue
code_hash0x9b819793a64463aed77c615d6cb226eea5487ccfc0783043a587254cda2b6f26
hash_typetype
tx_hash0xc76edf469816aa22f416503c38d0b533d2a018e253e379f134c3985b3472c842
index0x0
dep_typecode

Testnet​

parametervalue
code_hash0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb
hash_typetype
tx_hash0x3d4296df1bd2cc2bd3f483f61ab7ebeac462a2f336f2b944168fe6ba5d81c014
index0x0
dep_typecode

Offchain SDK Examples​

Rust SDK​

The following example demonstrates creating, signing, and sending a transaction using the Omnilock Script with the Rust SDK.

omnilock-example.rs
use ckb_sdk::{
constants::ONE_CKB,
transaction::{
builder::{CkbTransactionBuilder, SimpleTransactionBuilder},
handler::{omnilock, HandlerContexts},
input::InputIterator,
signer::{SignContexts, TransactionSigner},
TransactionBuilderConfiguration,
},
unlock::OmniLockConfig,
Address, CkbRpcClient, NetworkInfo,
};
use ckb_types::{
h256,
packed::CellOutput,
prelude::{Builder, Entity, Pack},
};
use std::{error::Error as StdErr, str::FromStr};

fn main() -> Result<(), Box<dyn StdErr>> {
let network_info = NetworkInfo::testnet();
let sender = Address::from_str("ckt1qrejnmlar3r452tcg57gvq8patctcgy8acync0hxfnyka35ywafvkqgqgpy7m88v3gxnn3apazvlpkkt32xz3tg5qq3kzjf3")?;
let receiver = Address::from_str("ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqv5dsed9par23x4g58seaw58j3ym5ml2hs8ztche")?;

let configuration = TransactionBuilderConfiguration::new_with_network(network_info.clone())?;

let iterator = InputIterator::new_with_address(&[sender.clone()], configuration.network_info());
let mut builder = SimpleTransactionBuilder::new(configuration, iterator);

let output = CellOutput::new_builder()
.capacity((128 * ONE_CKB).pack())
.lock((&receiver).into())
.build();
builder.add_output_and_data(output.clone(), ckb_types::packed::Bytes::default());
builder.set_change_lock((&sender).into());

let omni_cfg = OmniLockConfig::from_addr(&sender).unwrap();
let context = omnilock::OmnilockScriptContext::new(omni_cfg.clone(), network_info.url.clone());

let mut contexts = HandlerContexts::default();
contexts.add_context(Box::new(context) as Box<_>);

let mut tx_with_groups = builder.build(&mut contexts)?;

let json_tx = ckb_jsonrpc_types::TransactionView::from(tx_with_groups.get_tx_view().clone());
println!("tx: {}", serde_json::to_string_pretty(&json_tx).unwrap());

let private_key = h256!("0x6c9ed03816e3111e49384b8d180174ad08e29feb1393ea1b51cef1c505d4e36a");
TransactionSigner::new(&network_info).sign_transaction(
&mut tx_with_groups,
&SignContexts::new_omnilock(
[secp256k1::SecretKey::from_slice(private_key.as_bytes())?].to_vec(),
omni_cfg,
),
)?;

let json_tx = ckb_jsonrpc_types::TransactionView::from(tx_with_groups.get_tx_view().clone());
println!("tx: {}", serde_json::to_string_pretty(&json_tx).unwrap());

let tx_hash = CkbRpcClient::new(network_info.url.as_str())
.send_transaction(json_tx.inner, None)
.expect("send transaction");
// example tx: 0xc0c9954a3299b540e63351146a301438372abf93682d96c7cce691c334dd5757
println!(">>> tx {} sent! <<<", tx_hash);

Ok(())
}

Other example:

JavaScript/TypeScript (Lumos)​

Omnilock-Solana Example​

# Build Lumos
git clone https://github.com/ckb-js/lumos.git
cd lumos
pnpm install
pnpm run build

# Check if it is working
npx ts-node misc/config-manager.ts

# Start to work on Omnilock-Solana
npm run build
cd examples/omni-lock-solana
npm start

Checkout lib.ts file to learn the detail:

lib.ts
import { BI, helpers, Indexer, RPC, config, commons } from "@ckb-lumos/lumos";
import { common, omnilock } from "@ckb-lumos/lumos/common-scripts";
import { blockchain, bytify, hexify } from "@ckb-lumos/lumos/codec";
import { Config } from "@ckb-lumos/lumos/config";

const CKB_RPC_URL = "https://testnet.ckb.dev";
const rpc = new RPC(CKB_RPC_URL);
const indexer = new Indexer(CKB_RPC_URL);

export const CONFIG: Config = config.TESTNET;

config.initializeConfig(CONFIG);

declare global {
interface Window {
phantom: {
solana: omnilock.solana.Provider;
};
}
}

export const solana = window.phantom.solana;

export function asyncSleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

interface Options {
from: string;
to: string;
amount: string;
}

export async function transfer(options: Options): Promise<string> {
let txSkeleton = helpers.TransactionSkeleton({ cellProvider: indexer });

txSkeleton = await common.transfer(
txSkeleton,
[options.from],
options.to,
options.amount
);
txSkeleton = await common.payFeeByFeeRate(txSkeleton, [options.from], 1000);
txSkeleton = commons.omnilock.prepareSigningEntries(txSkeleton);

const signedMessage = await omnilock.solana.signMessage(
txSkeleton.signingEntries.get(0)!.message,
window.phantom.solana
);

const signedWitness = hexify(
blockchain.WitnessArgs.pack({
lock: commons.omnilock.OmnilockWitnessLock.pack({
signature: bytify(signedMessage),
}),
})
);

txSkeleton = txSkeleton.update("witnesses", (witnesses) =>
witnesses.set(0, signedWitness)
);

const signedTx = helpers.createTransactionFromSkeleton(txSkeleton);
const txHash = await rpc.sendTransaction(signedTx, "passthrough");

return txHash;
}

export async function capacityOf(address: string): Promise<BI> {
const collector = indexer.collector({
lock: helpers.parseAddress(address),
});

let balance = BI.from(0);
for await (const cell of collector.collect()) {
balance = balance.add(cell.cellOutput.capacity);
}

return balance;
}

Omnilock-Metamask Example​

# Build Lumos
git clone https://github.com/ckb-js/lumos.git
cd lumos
pnpm install
pnpm run build

# Check if it is working
npx ts-node misc/config-manager.ts

# Start to work on Omnilock-Metamask
npm run build
npm run build-release
cd examples/omni-lock-metamask
npm start

Checkout lib.ts file to learn the detail:

lib.ts
import {
BI,
Cell,
helpers,
Indexer,
RPC,
config,
commons,
} from "@ckb-lumos/lumos";
import { blockchain, bytify, hexify } from "@ckb-lumos/lumos/codec";

const CKB_RPC_URL = "https://testnet.ckb.dev/rpc";
const rpc = new RPC(CKB_RPC_URL);
const indexer = new Indexer(CKB_RPC_URL);
// prettier-ignore
interface EthereumRpc {
(payload: { method: 'personal_sign'; params: [string /*from*/, string /*message*/] }): Promise<string>;
}

// prettier-ignore
export interface EthereumProvider {
selectedAddress: string;
isMetaMask?: boolean;
enable: () => Promise<string[]>;
addListener: (event: 'accountsChanged', listener: (addresses: string[]) => void) => void;
removeEventListener: (event: 'accountsChanged', listener: (addresses: string[]) => void) => void;
request: EthereumRpc;
}
// @ts-ignore
export const ethereum = window.ethereum as EthereumProvider;

export function asyncSleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

interface Options {
from: string;
to: string;
amount: string;
}
const SECP_SIGNATURE_PLACEHOLDER = hexify(
new Uint8Array(
commons.omnilock.OmnilockWitnessLock.pack({
signature: new Uint8Array(65).buffer,
}).byteLength
)
);

export async function transfer(options: Options): Promise<string> {
const CONFIG = config.getConfig();
let tx = helpers.TransactionSkeleton({});
const fromScript = helpers.parseAddress(options.from);
const toScript = helpers.parseAddress(options.to);

// additional 0.001 ckb as tx fee
// tx fee is calculated by tx size
// this is just a simple example
const neededCapacity = BI.from(options.amount).add(100000);
let collectedSum = BI.from(0);
const collectedCells: Cell[] = [];
const collector = indexer.collector({ lock: fromScript, type: "empty" });
for await (const cell of collector.collect()) {
collectedSum = collectedSum.add(cell.cellOutput.capacity);
collectedCells.push(cell);
if (BI.from(collectedSum).gte(neededCapacity)) break;
}

if (collectedSum.lt(neededCapacity)) {
throw new Error(
`Not enough CKB, expected: ${neededCapacity}, actual: ${collectedSum} `
);
}

const transferOutput: Cell = {
cellOutput: {
capacity: BI.from(options.amount).toHexString(),
lock: toScript,
},
data: "0x",
};

const changeOutput: Cell = {
cellOutput: {
capacity: collectedSum.sub(neededCapacity).toHexString(),
lock: fromScript,
},
data: "0x",
};

tx = tx.update("inputs", (inputs) => inputs.push(...collectedCells));
tx = tx.update("outputs", (outputs) =>
outputs.push(transferOutput, changeOutput)
);
tx = tx.update("cellDeps", (cellDeps) =>
cellDeps.push(
// omni lock dep
{
outPoint: {
txHash: CONFIG.SCRIPTS.OMNILOCK.TX_HASH,
index: CONFIG.SCRIPTS.OMNILOCK.INDEX,
},
depType: CONFIG.SCRIPTS.OMNILOCK.DEP_TYPE,
},
// SECP256K1 lock is depended by omni lock
{
outPoint: {
txHash: CONFIG.SCRIPTS.SECP256K1_BLAKE160.TX_HASH,
index: CONFIG.SCRIPTS.SECP256K1_BLAKE160.INDEX,
},
depType: CONFIG.SCRIPTS.SECP256K1_BLAKE160.DEP_TYPE,
}
)
);

const witness = hexify(
blockchain.WitnessArgs.pack({ lock: SECP_SIGNATURE_PLACEHOLDER })
);

// fill txSkeleton's witness with placeholder
for (let i = 0; i < tx.inputs.toArray().length; i++) {
tx = tx.update("witnesses", (witnesses) => witnesses.push(witness));
}

tx = commons.omnilock.prepareSigningEntries(tx, { config: CONFIG });

let signedMessage = await ethereum.request({
method: "personal_sign",
params: [ethereum.selectedAddress, tx.signingEntries.get(0).message],
});

let v = Number.parseInt(signedMessage.slice(-2), 16);
if (v >= 27) v -= 27;
signedMessage =
"0x" + signedMessage.slice(2, -2) + v.toString(16).padStart(2, "0");

const signedWitness = hexify(
blockchain.WitnessArgs.pack({
lock: commons.omnilock.OmnilockWitnessLock.pack({
signature: bytify(signedMessage).buffer,
}),
})
);

tx = tx.update("witnesses", (witnesses) => witnesses.set(0, signedWitness));

const signedTx = helpers.createTransactionFromSkeleton(tx);
const txHash = await rpc.sendTransaction(signedTx, "passthrough");

return txHash;
}

export async function capacityOf(address: string): Promise<BI> {
const collector = indexer.collector({
lock: helpers.parseAddress(address),
});

let balance = BI.from(0);
for await (const cell of collector.collect()) {
balance = balance.add(cell.cellOutput.capacity);
}

return balance;
}

Other examples: