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 contentOmnilock args
: for extra checking control, allowing different modes to be enabled in the sameOmnilock 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 entirelock
field inwitnesses
. But in CKB MultiSig Script, it is only a partial clearance. This part is used assignatures
followed bymultisig_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>
Name | Flags | Affected Args | Affected Args Size (byte) | Affected Witness |
---|---|---|---|---|
administrator mode | 0b00000001 | AdminList cell Type ID | 32 | omni_identity/signature in OmniLockWitnessLock |
anyone-can-pay mode | 0b00000010 | minimum ckb/udt in ACP | 2 | N/A |
time-lock mode | 0b00000100 | since for timelock | 8 | N/A |
supply mode | 0b00001000 | type script hash for supply | 32 | N/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
parameter | value |
---|---|
code_hash | 0x9b819793a64463aed77c615d6cb226eea5487ccfc0783043a587254cda2b6f26 |
hash_type | type |
tx_hash | 0xc76edf469816aa22f416503c38d0b533d2a018e253e379f134c3985b3472c842 |
index | 0x0 |
dep_type | code |
Testnet
parameter | value |
---|---|
code_hash | 0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb |
hash_type | type |
tx_hash | 0x3d4296df1bd2cc2bd3f483f61ab7ebeac462a2f336f2b944168fe6ba5d81c014 |
index | 0x0 |
dep_type | code |
Offchain SDK Examples
Rust SDK
The following example demonstrates creating, signing, and sending a transaction using the Omnilock Script with the Rust SDK.
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:
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:
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: