0Prerequisites — 5 minutes, once
You need a Rust toolchain (stable) and the kaspulse source. The onchain feature
pulls in the pinned rusty-kaspa transaction stack — the first build takes minutes,
not seconds; every later build is incremental.
# clone the kaspulse repo (linked in the footer), then:
cd kaspulse
cargo build --features onchain # first build compiles the Kaspa stack — go make coffee
You also need a funded testnet-10 key at ~/.kaspulse/tn10.key (a 64-hex Schnorr
secret key — testnet funds only, worth nothing). The easiest path is the one the
kascov guide uses: kascov-lab keygen
prints a fresh key's address, and kaspulse's tools read kascov-lab's key file automatically as
a fallback — if you already did that guide, you are done with this step.
# in the kascov repo (github.com/Knitser/kascov) — prints your TN10 address:
cargo run -p kascov-lab -- keygen
address: kaspatest:qq…
# pin the key where kaspulse's tools look first:
mkdir -p ~/.kaspulse && cp /tmp/kascov-lab-key.hex ~/.kaspulse/tn10.key
Fund the printed address at faucet-testnet.kaspanet.io — it is Cloudflare-gated, so use a browser, not curl. Then confirm it arrived:
cargo run -p kascov-lab -- balance
balance: 100.00000000 TKAS
In a hurry? One command runs this entire guide — generates the demo committee, deploys the price gate, waits for confirmation, and spends it with a live signed price:
cargo run --bin gate --features onchain -- demo --strike 0.02 --value 3
The rest of this page does the same thing one step at a time, so you can see every moving part.
1Get a signed price — and verify it
Ask the oracle for KAS/USD. $BASE is whatever origin serves this page —
http://localhost:8080 if you run the oracle locally
(cargo run --bin oracle), or the hosted origin you are reading this on.
BASE=http://localhost:8080 # or this page's own origin
curl -s $BASE/v1/feed/KAS-USD | jq '{pair, price, mant, expo, signed_ts, message}'
{
"pair": "KAS/USD",
"price": 0.02786,
"mant": 278600000,
"expo": -10,
"signed_ts": 1784428033,
"message": "kaspulse/v2|KAS/USD|278600000|-10|1784428033|69"
}
The price is exact integers, not a float: mant × 10^expo =
278600000 × 10−10 = $0.02786. The message is the
exact ASCII string all 5 committee nodes signed —
kaspulse/v2|PAIR|mant|expo|ts|round. Any 3 of the 5 signatures make the price
valid. Now verify it, three ways, escalating in paranoia:
Level 1 — one click. Open the KAS/USD feed page and press verify. Your browser recomputes the blake2b digest and checks all 5 BIP340 Schnorr signatures itself, in ~120 lines of dependency-free JS. No server involved in the verdict.
Level 2 — one file, your machine. The same verifier as a zero-dependency
script (clients/js/kaspulse.mjs, or clients/py/kaspulse.py for
Python). It self-tests its crypto against official BIP340 vectors before it will verify
anything:
node clients/js/kaspulse.mjs verify KAS/USD $BASE
KAS/USD http://localhost:8080 signed_round 93
node 0 ✓ 0dd71bf25a98da9ff89a661537997fc9b3f3ae5eb37b23778f11aaec086b8f4a
node 1 ✓ d6ce84b11b72c0e4eaccf1070b96d90611d6ba8c8d43022a84a4268b43bf3bd2
node 2 ✓ 40ffebd6b99e56b258d58574b41d493842c94152c8798827bd1bc4b4adc5d514
node 3 ✓ 7d4e175938b3b2cc0883f11bfd805eec6c92e7efba1a52af47edfc326cc48864
node 4 ✓ 458220ba8737cdd5702ba602239a294d00ef1dbc60078d458d35a2bb066fdd0b
bound=true (signed message fields == JSON fields)
✓ VALID — 5/5 signatures verify (threshold 3), price = 0.027875
(bound=true matters: it checks the signed message's pair/mant/expo/ts
fields equal the JSON fields next to it — so the API cannot show you one price while the
signatures cover another.)
Level 3 — trust nothing, re-derive everything.
cargo run --bin verify checks every feed's signatures, then goes around the
oracle entirely: it re-fetches the exchanges itself, recomputes the median, and re-reads the
KRC-20 DEX pools on-chain:
cargo run --bin verify
kaspulse verify — pulling all feeds from http://127.0.0.1:8080/api/feed
signatures (need 3 of each feed's nodes):
✓ KAS/USD 5/5 valid
✓ BTC/USD 5/5 valid
✓ ETH/USD 5/5 valid
… one line per feed (24 feeds at this run) …
independent KAS/USD check (re-fetching exchanges myself):
Kraken: $0.027870
KuCoin: $0.027900
Gate.io: $0.027850
Bybit: $0.027890
MEXC: $0.027872
my median $0.027872 vs feed $0.027890 (0.06% drift) ✓
independent KRC-20 check (re-reading the DEX pools myself):
20 tokens re-read on-chain · 20 reproduce the feed (within TWAP tolerance)
VERDICT: honest — every feed signed by the threshold, KAS matches the market,
and the KRC-20 pools reproduce on-chain. No trust required.
You just audited the oracle. Everything after this point makes Kaspa L1 itself do that audit — at spend time, in script.
2The covenant Kaspa will enforce
The price gate is a P2SH covenant: a coin only spendable by presenting a price that
(a) is at or above a strike and (b) carries a valid Schnorr
signature from every committee key — verified by the node's script engine with
post-Toccata's OpCheckSigFromStack, not by any off-chain service. The redeem
script (built by kaspulse-sdk's covenant::price_gate_redeem, the
byte-identical script consumer_live proved on TN10):
| opcodes | what the Kaspa VM does |
|---|---|
OpDup ‹strike_e8› OpGreaterThanOrEqual OpVerify |
duplicate the spender's price_bytes, push the strike
(e.g. $0.02 → 2000000), numeric compare — abort unless
price ≥ strike |
OpBlake2b OpToAltStack |
hash the price bytes — this 32-byte digest is exactly what the committee signed — and stash it on the alt stack |
OpFromAltStack OpDup OpToAltStack ‹pk2› OpCheckSigFromStack OpVerify |
restore the digest (keeping a copy), verify node 2's signature over it — abort on failure |
OpFromAltStack OpDup OpToAltStack ‹pk1› OpCheckSigFromStack OpVerify |
same for node 1 |
OpFromAltStack ‹pk0› OpCheckSigFromStack |
node 0's check leaves the final true/false |
price_bytes — the on-chain price encoding. The price
(price_e8, dollars × 108) is pushed as a minimal little-endian
script number, because OpGreaterThanOrEqual compares script numbers.
Real values, which you can recompute yourself:
| price | price_e8 | price_bytes (hex) |
|---|---|---|
| $0.02 (the strike below) | 2000000 | 80841e |
| $0.02786 (the captured live price) | 2786000 | d0822a |
| $0.02955 (an earlier capture) | 2955000 | f8162d |
The demo committee signs schnorr(blake2b-256(price_bytes)) — for
d0822a that digest starts 338a2f50…. This is a different,
simpler encoding than the hosted committee's pipe-delimited message from step 1 — which
is exactly why this guide generates a local demo committee (see the
honest box).
3Deploy it
Generate the 3-key demo committee (written as gate-node-0.key …
gate-node-2.key in the current directory):
cargo run --bin gate --features onchain -- keygen
wrote ./gate-node-0.key
wrote ./gate-node-1.key
wrote ./gate-node-2.key
node 0 x-only pubkey: ‹64 hex — yours›
node 1 x-only pubkey: ‹64 hex — yours›
node 2 x-only pubkey: ‹64 hex — yours›
Look at the covenant before funding it — address prints the redeem script and
the testnet-10 P2SH address that commits to it:
cargo run --bin gate --features onchain -- address --strike 0.02
redeem (120 bytes): ‹your redeem hex — deterministic from your 3 pubkeys + the strike›
TN10 P2SH address: kaspatest:‹…›
Watch your covenant get born on kascov → paste the redeem hex into kascov.io/decode and see it disassembled opcode-by-opcode — you'll recognize every line of the table from step 2, with your own pubkeys and strike in the pushes.
Now fund it — 3 TKAS from your ~/.kaspulse/tn10.key, locked behind the gate:
cargo run --bin gate --features onchain -- deploy --strike 0.02 --value 3
connected to TN10 · covenant P2SH kaspatest:‹…›
🎟️ DEPLOYED — 3.00 TKAS locked behind the price gate
tx ‹your deploy txid›
https://explorer-tn10.kaspa.org/txs/‹your deploy txid›
next (after ~25s confirm): gate spend --strike 0.02
Click the printed explorer link: that is your coin, on the real chain, spendable by no signature at all — only by a price that satisfies the script.
4The traps
Four things break first runs. gate handles all four for you — but you will hit
every one of them the moment you build your own consumer, so here they are with their exact
symptoms.
(a) ComputeBudget — three OpCheckSigFromStacks are not free.
Post-Toccata, every input of a v1 transaction commits a per-input
computeBudget (1 unit = 10,000 script units), and one Schnorr signature check
costs 100,000 units — so three CSFS need ≥ 30 units before counting witness bytes, which are
charged 1:1 on top. Commit too little and the node rejects with:
failed to verify the signature script: script units exceeded
the amount committed in the input: used=100000, limit=9999
(limit=9999 is the free allowance you get when your committed budget is
effectively 0 — the classic symptom of tooling that silently drops the field.) The budget is
covered by the signature hash, so you cannot patch it in after signing.
gate commits ComputeBudget(120) on the covenant input and
50 on the fee input; committed budget is charged as mass whether used or not, so
the fee scales with it — generous, not 65535. The full anatomy of this trap is
in the kascov guide, step 3.
(b) The ~25-second confirmation wait. Kaspa blocks are ~100 ms, but the
public node's UTXO index needs a moment. Run spend too soon and you get the
honest recovery message, not a failure:
coin not in UTXO set yet — wait ~25s for confirm, then re-run `gate spend`
Nothing is lost — the coin is deployed; re-run and it spends. (gate demo
polls for the coin so you never see this.)
(c) The second input still needs a plain signature. The spend has
two inputs: input 0 is the covenant coin (unlocked by the witness, no key), input 1
is a plain fee-paying UTXO from your wallet — and that one must be signed
SIG_HASH_ALL like any normal spend, after the covenant witness is in
place (the sighash covers it). Forget input 1's signature — the one that "never needed
anything" in single-input tools — and the whole transaction is rejected.
(d) Non-minimal price_bytes breaks everything, silently at first.
Script numbers must be minimally encoded: $0.02786 is d0822a, never
d0822a00. Pad it and two things break at once — the numeric comparison rejects
the push, and the blake2b digest changes so the committee signatures no longer match.
Use the SDK's price_bytes() (zero is the empty push; a high top bit
gets a 0x00 pad byte; negatives set 0x80) rather than hand-rolling.
5Spend it
spend fetches the live price from the oracle, verifies it first
(signatures + field binding + freshness, via the SDK — it refuses unverified prices), has the
demo committee sign blake2b(price_bytes), builds the witness, and broadcasts:
cargo run --bin gate --features onchain -- spend --strike 0.02
connected to TN10 · covenant P2SH kaspatest:‹…›
oracle KAS/USD = $0.027860 (verified: 3-of-5 sigs + field binding) · strike $0.020000
witness: [3 sigs, price_bytes(2786000), redeem] = 321 bytes
💸 UNLOCKED — released ‹payout› TKAS: Kaspa L1 verified 3 committee sigs + price ≥ strike.
tx ‹your spend txid›
https://explorer-tn10.kaspa.org/txs/‹your spend txid›
The witness pushes, bottom→top: [sig0, sig1, sig2, price_bytes, redeem]. The
node ran the whole table from step 2 — three independent signatures plus the price
condition, verified by L1 consensus, no oracle server in the loop at spend time. (If the
price had been below the strike, gate refuses before broadcasting — and if you
force it, the chain refuses too. That's the point.)
How fast does a signed price become a spendable on-chain fact? Measured on live TN10 via a
public node: 1.39 s average tick→UTXO (1510/1154/1507 ms over three
rounds; own-node path is sub-second) — see KASPA-EDGE.md in the repo, reproduce with
cargo run --bin latency --features onchain.
6Prove it to yourself
Don't take this page's word for what just happened — the proof is all public.
Read the witness off the chain. Open your spend txid on the TN10 explorer and expand input 0's signature script: you will see your five pushes — three 64-byte signatures, your 3-byte price, your 120-byte redeem script.
Re-verify the three signatures yourself — same one-file client from step 1, no oracle involved:
python3 - <<'EOF'
import sys; sys.path.insert(0, 'clients/py')
from kaspulse import blake2b256, bip340_verify
price_bytes = bytes.fromhex('d0822a') # the price push from YOUR witness
digest = blake2b256(price_bytes) # what the demo committee signed
pubkeys = ['‹node 0 pubkey›', '‹node 1 pubkey›', '‹node 2 pubkey›'] # from `gate keygen`
sigs = ['‹sig 0›', '‹sig 1›', '‹sig 2›'] # the three 64-byte pushes from the witness
for i, (pk, sig) in enumerate(zip(pubkeys, sigs)):
print(f'node {i}:', bip340_verify(bytes.fromhex(pk), digest, bytes.fromhex(sig)))
EOF
node 0: True
node 1: True
node 2: True
Replay the script on kascov. Paste your spend txid into the
kascov.io search (network: testnet-10) and hit
“⧉ replay this spend” — it runs your captured witness through the real
TxScriptEngine and streams the opcode-by-opcode trace: the strike comparison,
the blake2b, all three OpCheckSigFromStacks, the final verdict. The same trace
is one curl away: curl -s https://kascov.io/data/testnet-10/debug/‹your spend txid› | jq .verdict.
7Variations — and what keeps nodes honest
Flip the gate: a liquidation trigger
The gate compares in one direction. Flipping one opcode —
OpGreaterThanOrEqual → OpLessThanOrEqual — turns the payout into a
liquidation/stop trigger that releases only when the price falls to the strike. In
the SDK that is one argument:
// release iff price ≥ strike (this guide):
price_gate_redeem_dir(&committee, strike_e8, Gate::AtOrAbove)
// release iff price ≤ strike (liquidations, stop conditions):
price_gate_redeem_dir(&committee, strike_e8, Gate::AtOrBelow)
Range settle
range_settle_redeem(&committee, low_e8, high_e8) releases only when
low ≤ price ≤ high — two OpDup-compare-OpVerify blocks
in front of the same committee tail, same witness shape. Options expiries, bounded payouts,
"settle inside the band" contracts.
What keeps nodes honest: the equivocation bond
A price gate is only as good as the committee's honesty — so kaspulse's economic-security
story is a covenant too. Alongside every round, a node signs a fixed 24-byte
attestation record: blake2b(pair)[0..8] ‖ round_be ‖ mant_be. Sign two
different prices for the same slot and those two records are a
cryptographic proof of double-signing — and a bond covenant
(covenant::bond::bond_redeem) lets anyone who holds that proof seize
the node's bond, verified purely by Kaspa L1 script. Real records, which you can recompute
(blake2b-256("KAS/USD") starts b84ad8389aa2ebb0…):
| slot: pair-id ‖ round 42 | price (mant, big-endian) | |
|---|---|---|
rec1 ($0.029) | b84ad8389aa2ebb0 000000000000002a | 00000000002c4020 |
rec2 ($0.058) | b84ad8389aa2ebb0 000000000000002a | 0000000000588040 |
Same first 16 bytes, different last 8 → slashable. The script checks exactly four things
(two OpCheckSigFromStacks, an OpSubstr slot-equality, an
OpSubstr price-inequality) — which yields four cases, and only one of them
slashes. This run is the local Kaspa script engine executing the real bond covenant:
cargo run --bin slash --features onchain
bonded oracle node key: af36ed050dd1919b7c1aaefe586232c6b8ba9fb7b88fcbcd70ae64766f295163
[1] real equivocation (slot=KAS/USD#42, $0.029 vs $0.058) → bond SLASHED: true
[2] different rounds (honest update) → NOT slashable: true
[3] same price twice (no conflict) → NOT slashable: true
[4] forged 2nd sig (attacker's key) → NOT slashable: true
✅ Script-enforced slashing WORKS. A node that double-signs a price loses its bond to
whoever catches it — verified purely by Kaspa L1 (OpCat/OpSubstr/OpCheckSigFromStack),
no committee and no governance.
And it ran on the real chain, not just the local engine: slash_live posts a real
3-TKAS bond on TN10 and then slashes it with a real double-signing proof — we slashed our own
bond, on purpose. Reproduce it end-to-end (your own bond, your own fresh txids):
cargo run --bin slash_live --features onchain
🔒 BONDED — 3.00 TKAS posted by the node behind the slashing covenant
tx ‹your bond txid›
⚠️ caught: the node signed KAS/USD round 42 as BOTH $0.029 AND $0.058 — equivocation.
💥 SLASHED — the bond was seized on-chain by proving the double-signing, verified by Kaspa L1.
tx ‹your slash txid›
One honest limitation, stated in the code itself: the bond covenant ships its
slashing path only — the honest node's reclaim-timelock branch is marked unproven in
src/slash.rs, so the SDK does not expose it until it is proven. The operator's
view of bonds, monitoring, and running a signer of your own:
docs/OPERATOR.md in the repo (also linked from the docs hub).