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 entirelockfield inwitnesses. But in CKB MultiSig Script, it is only a partial clearance. This part is used assignaturesfollowed 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: