Skip to content

calledAdo/lean-oracle

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lean Oracle

A Pyth/Wormhole price oracle for CKB. Hermes provides signed Pyth price update bytes, Wormhole guardian-set data defines the trust root, and on-chain CKB scripts verify and store the latest accepted price state in oracle cells that downstream protocols can read as cell deps.

Testnet Live — see Live Deployments
Mainnet Inert (API/config surfaces exist; no deployment yet)
SDK lean-oracle-sdk on npm
License MIT
Audit Not audited — see Security

The repository contains the on-chain scripts, a TypeScript SDK, and a deployment toolbox for publishing and operating the oracle.

Why Lean Oracle

  • CKB-native cell model. Each price feed is a single, addressable oracle cell consumers reference as a cell dep. No registry contract, no router — the cell is the feed.
  • Permissionless updates. Anyone can push a fresh Hermes update; the owned_type_bind_lock preserves type continuity and gives the owner an escape path without gating who can publish prices.
  • Minimal trust additions. The oracle inherits Pyth/Wormhole's guardian-set quorum security and adds nothing beyond it. Hermes is an untrusted transport.
  • Lean script footprint. Three small CKB scripts (oracle, guardian-set, bind-lock) with shared parsing in one common crate. No precompiles, no hidden dependencies.
  • Honest scope. The oracle verifies authenticity and monotonicity. Freshness limits and risk policy are explicitly the consumer's responsibility — no false guarantees baked in.

Architecture

        ┌──────────────────────────┐
        │  Pythnet (off-chain)     │  signs price updates
        └──────────────┬───────────┘
                       │  guardian signatures
                       ▼
        ┌──────────────────────────┐
        │  Hermes (untrusted CDN)  │  serves accumulator update blobs
        └──────────────┬───────────┘
                       │  fetched by anyone
                       ▼
   ┌────────────────────────────────────────┐
   │  Update tx submitter (any CKB wallet)  │
   │  - drafts tx via lean-oracle-sdk       │
   │  - rebalances fees                     │
   │  - signs + broadcasts                  │
   └──────────────┬─────────────────────────┘
                  │
                  ▼
   ┌────────────────────────────────────────┐
   │  CKB chain                             │
   │  ┌──────────────────────────────────┐  │
   │  │ oracle_script  (type script)     │  │  verifies:
   │  │  + owned_type_bind_lock (lock)   │  │   - guardian sig quorum
   │  │  + guardian_set cell (cell dep)  │  │   - emitter chain/address
   │  │                                  │  │   - feed id match
   │  │  ⇒ Oracle Cell (price state)     │  │   - publish_time monotonic
   │  └──────────────────────────────────┘  │
   └──────────────┬─────────────────────────┘
                  │  cell dep
                  ▼
        ┌──────────────────────────┐
        │  Consumer dApp / script  │  reads price, applies own
        │  (your protocol)         │  staleness/risk rules
        └──────────────────────────┘

What an Update Proves

A successful update proves that:

  • the update bytes are structurally valid
  • the update is signed by the active guardian set (current-set-only policy)
  • the update comes from the configured emitter chain and emitter address
  • the message contains the expected Pyth price feed id
  • the new publish_time is strictly newer than the existing oracle state
  • the output oracle cell exactly matches the authenticated price message

The oracle verifies authenticity and monotonicity. Freshness limits, deviation bounds, and other application-specific risk rules are the consumer's responsibility.

For Consumers: Reading Prices

Install the SDK and read the latest testnet price for a Pyth feed:

npm install lean-oracle-sdk @ckb-ccc/core
import { LeanOracleTestnetClient } from "lean-oracle-sdk";

const feedId =
  "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; // BTC/USD

const oracle = new LeanOracleTestnetClient();
const state = await oracle.getOracleCellState({ feedId });

if (!state) throw new Error(`No oracle cell for ${feedId}`);

// Apply your own staleness policy — the oracle guarantees monotonicity, not freshness.
const ageSeconds = Math.floor(Date.now() / 1000) - Number(state.data.publishTimeUnix);
if (ageSeconds > 60) throw new Error(`Price stale: ${ageSeconds}s old`);

console.log({
  price: state.data.price,        // bigint, scaled by 10^expo
  expo: state.data.expo,
  publishTimeUnix: state.data.publishTimeUnix,
  outPoint: state.outPoint,       // use as a CKB cell dep from your script
});

On-chain consumers reference the oracle cell by outPoint as a CellDep, decode its data using the layout in crates/lean_oracle/contracts/common/, and apply their own staleness/deviation rules before acting on the price.

No public oracle cell for your feed? Deploy your own.

getOracleCellState returning undefined means no live oracle cell exists for that Pyth feed id under the default public lock. Any feed Pyth signs (ETH/USD, SOL/USD, equities, FX, …) can be brought up by deploying your own oracle cell under a lock script you control — the on-chain scripts are permissionless, so anyone can host a cell for any valid feed:

import { ccc } from "@ckb-ccc/core";
import { leanOracleTestnetPreset } from "lean-oracle-sdk/presets";
import { initiateOracleDeployTx } from "lean-oracle-sdk/tx";
import { rebalanceFuel } from "lean-oracle-sdk/fuel";

const cccClient = new ccc.ClientPublicTestnet();
const signer = new ccc.SignerCkbPrivateKey(cccClient, process.env.CKB_PRIVATE_KEY!);
const myLock = (await signer.getRecommendedAddressObj()).script;

const tx = await initiateOracleDeployTx({
  network: leanOracleTestnetPreset,
  cccClient,
  feedId: "0x...ETH/USD feed id...",
  oracleLockScript: myLock,        // your lock governs subsequent updates
  capacity: 20_000_000_000n,       // shannons; size per cell layout + headroom
});

const balanced = await rebalanceFuel(tx, { cccClient, lockScript: myLock, fuelLimit: 32 });
if (balanced.status !== "ok") throw new Error("insufficient capacity");

await signer.sendTransaction(balanced.mutated);

The cell you deploy is yours: the lock you supply governs who can submit subsequent updates and who can burn the cell. If you want public, permissionless updates instead, deploy under the owned_type_bind_lock preset (live deployments) — it keeps type continuity while letting anyone push fresh prices, with an owner escape path.

See packages/sdk/README.md for the full SDK API, feed-id lookup, update-transaction drafting, and devnet integration.

Live Deployments

Testnet

Guardian set index 6, quorum 13 (Wormhole mainnet guardians).

Component Version Code Hash Deploy Tx
oracle_type v2 (latest) 0x10c9bcc3af00fc3728cb95d5e14ec882716af5f531a010852526ce784f6958ec 0x45f033f0944b50be1e5b80f733c321648ddcfdbe0c183477cf0b77bd0f8312b5
oracle_type v1 (legacy) 0x2277560d62a11a92084654b67848ea893fcf3c1880e20a3ce9c0c19d0ee27dc3 0xf39d3cb5eccab560bdab65529f4e6f86c2dc8c966a4d49a2fd17bb277e75bba2
guardian_set_type v1 0x57bddf3d57ea45c88ab68d0de706bbaecd68895fd6062b099626deb157100119 0x78f83c3967c566c50c783d45c9165af94d23018c5254228b3eb418aa0c5ac37f
owned_type_bind_lock v1 0x5554bc20c9f3dbb8d1d7a6591b1b2ceeb0bbee822804635ee168911a440a111c 0x982a5d5555ebc855a97d9e71a8ac9de9cefc25a62a44ccfc2b6605758c01ba9f

All values, plus oracle/guardian-set cell outpoints and the full version history, are checked in under deployment/artifacts/testnet.*.json. The SDK's leanOracleTestnetPreset consumes these directly — most consumers do not need the hashes by hand.

Mainnet

Not deployed. LeanOracleMainnetClient is exported for API continuity but its deployment values are intentionally inert until a real mainnet deployment is published.

Pythnet / Wormhole Components

Lean Oracle keeps the main pieces of the Pythnet/Wormhole model, but represents them in CKB-native state:

  • Hermes — off-chain transport for Pyth accumulator update blobs. Hermes is not trusted; it only delivers bytes that the scripts verify.
  • Guardian set — the Wormhole guardian addresses and quorum used to verify update signatures. The active set lives in a guardian-set cell.
  • Emitter chain — the Wormhole source chain id expected for Pythnet price messages.
  • Emitter address — the expected Pyth emitter address for the configured source.
  • Feed id — the 32-byte Pyth price feed id (e.g. BTC/USD) that identifies a specific oracle cell.
  • Accumulator update — the signed update payload fetched from Hermes and supplied as witness data during oracle updates.

This implementation uses a current-set-only guardian policy. Only the active canonical guardian set is accepted. After Wormhole guardian rotation, callers must fetch a fresh Hermes update signed by the new active set.

Security & Threat Model

What you trust when consuming a Lean Oracle price:

  1. Wormhole guardian quorum (13-of-19 on the configured set). A guardian-majority compromise can produce a fraudulent update Lean Oracle will accept.
  2. Pythnet emitter at the configured chain id + emitter address — the source of the signed price messages.
  3. CKB consensus for the chain Lean Oracle is deployed on.

What you do not trust:

  • Hermes — pure transport; tampered or stale bytes are rejected by signature verification.
  • The update submitter — anyone can push an update; the script enforces feed-id match, emitter match, signature quorum, and publish_time monotonicity.
  • Update frequency — the oracle does not enforce a maximum age. If you need a freshness bound, enforce it in your consumer (see the snippet above).

Known boundaries:

  • Guardian rotation: the current-set-only policy means that immediately after Wormhole rotates guardians, an update signed by the previous set is rejected. The first update under the new set must rotate the guardian-set cell.
  • No price arbitration: the oracle stores exactly what Pyth signed. If Pyth publishes an aberrant value, Lean Oracle will accept it. Consumers should apply deviation/sanity checks if their use case warrants.
  • No audit yet. This codebase has not been independently audited. Treat the testnet deployment as experimental. Do not use Lean Oracle for mainnet-equivalent value at risk until an audit lands.

Repository Structure

crates/lean_oracle/        Rust workspace for CKB contracts and contract tests
  contracts/common/        Shared parsing, hashing, layouts, and verifier logic
  contracts/oracle_script/ Oracle cell type script
  contracts/guardian_set_script/
                           Guardian-set cell type script
  contracts/owned_type_bind_lock/
                           Public-update lock with owner escape path
  tests/                   Host-side and ckb-testtool integration tests

packages/sdk/              TypeScript SDK published as lean-oracle-sdk

deployment/                TypeScript deployment CLI and network config
  config/                  Checked-in network deployment intent
  artifacts/               Generated deployment outputs (per network)
  src/, tests/             Deployment actions, validators, and tests

docs/superpowers/specs/    Design specs (see below)
reports/                   Weekly progress reports

Design Docs

Development

Contracts

make contracts-build   # build optimized RISC-V CKB binaries
make contracts-test    # host-side test suite + ckb-testtool integration

contracts-test runs against x86_64-unknown-linux-gnu so host test dependencies do not get compiled for the no-std contract target.

SDK

cd packages/sdk
npm install
npm test                 # codecs, discovery, builders, witnesses, exports
npm run release:check    # test + tarball-pack subpath check

See packages/sdk/README.md for devnet integration tests, custom CCC client wiring, and code-version pinning.

Deployment toolbox

cd deployment
npm install
npm run build
npm test

node --enable-source-maps ./dist/index.js <action> \
  --network <testnet|mainnet|devnet>

Typical bootstrap order for a network:

deploy:guardian-set-type → promote:guardian-set-type → deploy:guardian-set
deploy:oracle-type       → promote:oracle-type       → deploy:oracle

See deployment/README.md for required environment variables, dry-run behavior, artifact formats, and local devnet overrides.

Local devnet

The SDK is isolated from the deployment package: for a local devnet, build your own LeanOracleNetworkConfig from local deployment metadata and pass it plus a hand-built CCC client to LeanOracleClient. Do not use the testnet preset for devnet cells. Generated local devnet artifacts should not be committed.

Contributing

Issues and pull requests welcome at https://github.com/calledAdo/lean-oracle/issues.

The repo uses three checks that should pass before opening a PR:

make contracts-test
( cd packages/sdk && npm run release:check )
( cd deployment   && npm test )

License

Lean Oracle is released under the MIT License — see LICENSE. This covers the on-chain Rust contracts, the TypeScript SDK, and the deployment toolbox.

About

A minimal pyth oracle clone on CKB based

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors