Programmable, self-enforcing transaction rules on Kaspa, built in raw script and verified against live consensus on testnet-10, ahead of the Toccata mainnet activation (June 30, 2026). The headline artifact is an agent-budget wallet whose rules — a rolling per-period budget, a per-spend cap, a destination allowlist, an exact protocol fee, value conservation, agent authentication, and an owner recovery path — are enforced by the network, not by the wallet's software.
The same foundation — introspection output-constraints, self-templating state, unforgeable
timelocks, hashlocks, multi-signature paths, and bounded delegated authority — is then reused
to build two more primitives, each proven on-chain: an HTLC atomic swap (swaps/)
and an arbitrated escrow (escrow/) in which the arbiter can settle a
dispute but is structurally unable to steal.
The result that matters: six transactions that each break one rule, every one signed
with the legitimate agent key and submitted directly to a node, were all rejected by the
covenant script itself. A compromised agent that holds the key still cannot exceed its
limits. See ARTICLE.md for the full writeup.
Status: research / demonstration. Built on testnet-10, unaudited. Not for mainnet value as-is.
This repo grew out of a build log: every covenant mechanism was implemented as the smallest possible script, confirmed on-chain, and only then composed. The staged proofs are kept because they're the clearest way to understand each primitive in isolation.
kaspa-covenants/
├── ARTICLE.md # the writeup (publishable)
├── README.md # this file
├── LICENSE
├── probes/ # consensus-semantics verification
│ ├── kaspa_sdk_discover.py # SDK surface discovery
│ ├── kaspa_primitive_probe.py # opcode enumeration
│ ├── kaspa_p2sh_covenant_probe.py # P2SH hash recipe + covenant-id model
│ └── kaspa_locktime_probe.py # lock_time = DAA clock semantics
├── covenant/ # the staged build
│ ├── kaspa_selftemplate_counter.py # 1 self-templating state (count++)
│ ├── kaspa_budget_step2a.py # 2a budget decrement + floor
│ ├── kaspa_budget_step2b1.py # 2b-1 two-field state
│ ├── kaspa_rolling_budget.py # 2b-2 rolling budget + calendar reset
│ ├── kaspa_agent_covenant_step3a.py # 3a + agent auth + owner escape
│ └── kaspa_agent_covenant_step3b.py # 3b + cap + allowlist + fee + conservation ← full covenant
└── adversarial/
├── kaspa_agent_covenant_3b_adversarial.py # 6-attack budget battery
└── kaspa_escrow_adversarial.py # 4-attack escrow battery
Plus two primitives built on the same foundation:
├── swaps/ # HTLC atomic-swap primitive
│ ├── kaspa_htlc_swap.py # hashlock claim + timelocked refund
│ └── README.md # writeup + on-chain anatomy
└── escrow/ # arbitrated escrow
├── kaspa_escrow.py # two-sig settle + bounded-arbiter resolve
├── kaspa_escrow_adversarial.py # (or under adversarial/) the 4-attack battery
└── README.md # writeup + bounded-arbiter proof
(The files currently sit flat; move them into the folders above when you initialize the repo.)
Toccata (KIP-17) adds transaction-introspection opcodes: a script can read the very
transaction spending it — output amounts and destinations, input value, lock time — and
refuse to validate unless they satisfy its conditions. State is carried by self-templating:
the mutable state sits at the front of the script, the immutable logic tail is read back from
the spending input at runtime, the successor script is reconstructed and hashed, and the
continuation output is bound to that hash. The reset clock uses lock_time interpreted as a
DAA score, made unforgeable by the fact that consensus won't mine a future-dated transaction,
plus a relative-timelock check that forces the lock time to be enforced. Full explanation in
ARTICLE.md.
Requires Python 3 and the kaspa SDK, and RPC access to a node on the current Toccata /
testnet-10 build (the script's RPC_URL constant — point it at your node).
pip install kaspa
# build the full hardened covenant (new keypairs -> new address)
python3 covenant/kaspa_agent_covenant_step3b.py build \
--full-tkas 20 --period-daa 600 --cap-tkas 10 --dev-fee-tkas 2.0 \
--whitelist <approved_payee_address> \
--dev-addr <fee_address>
# fund the printed address, then:
python3 covenant/kaspa_agent_covenant_step3b.py info
python3 covenant/kaspa_agent_covenant_step3b.py spend <approved_payee_address> 8
python3 covenant/kaspa_agent_covenant_step3b.py sweep <your_own_address> # owner escape
# prove the limits are enforced by consensus, not by the client:
python3 adversarial/kaspa_agent_covenant_3b_adversarial.pyExpected adversarial output: 6/6 blocked by the covenant script itself.
Each step isolates one new mechanism and confirms it on-chain before the next builds on it.
| Step | Script | Proves |
|---|---|---|
| Probes | probes/*.py |
P2SH hash recipe, opcode set, lock_time DAA semantics |
| 1 | kaspa_selftemplate_counter.py |
self-templating: a coin that rebuilds itself with count+1 |
| 2a | kaspa_budget_step2a.py |
budget decrement with a >= 0 floor |
| 2b-1 | kaspa_budget_step2b1.py |
two-field state carried through the rebuild |
| 2b-2 | kaspa_rolling_budget.py |
calendar reset: refill-per-period via the lock_time clock |
| 3a | kaspa_agent_covenant_step3a.py |
two-path branch: agent auth + owner escape |
| 3b | kaspa_agent_covenant_step3b.py |
cap + allowlist + exact fee + value conservation |
| Attack | adversarial/kaspa_agent_covenant_3b_adversarial.py |
every budget rule rejected by consensus, not the client |
| Swap | swaps/kaspa_htlc_swap.py |
hashlock claim + timelocked refund — the atomic-swap leg |
| Escrow | escrow/kaspa_escrow.py |
two-signature settle + bounded-arbiter resolve (can decide, can't steal) |
| Attack | escrow/kaspa_escrow_adversarial.py |
every escrow rule rejected by consensus, not the client |
HTLC atomic swap — swaps/. Funds locked to two paths: a hashlock claim
(reveal a secret whose blake2b equals a committed hash, plus the claimant's signature) and
a timelocked refund. Claiming writes the secret on-chain, which is what lets a counterparty
unlock the mirror leg of a cross-chain swap. Both paths are confirmed on testnet-10; the
writeup annotates the live claim transaction opcode-by-opcode.
Arbitrated escrow — escrow/. Buyer, seller, and arbiter, two paths: a
settle path where buyer and seller co-sign (a two-signature branch — N-of-M without
OpCheckMultiSig), and a resolve path where the arbiter signs alone but introspection pins
the payout to exactly one output, to the buyer's or seller's address, at full value. Same key,
the arbiter can confirm a payment to a legitimate party and is rejected when aiming anywhere
else — proven by an adversarial battery that signs four distinct attacks with the genuine
arbiter key and watches the covenant refuse each.
- Fee rate. At Toccata the minimum fee rate rises to 100 sompi/gram. A transaction that's accepted to a node's mempool can still fail to mine if it underpays for its mass.
- Storage mass (KIP-9). Cost scales with the sum of
1/output_value, so small outputs are expensive. A tiny fee output inflates mass sharply. Production pattern: accrue small fees inside the covenant and pay them out in larger batches, so routine spends emit only large outputs. - Push encoding. Redeem scripts over 75 bytes need
OP_PUSHDATA1/OP_PUSHDATA2; a bare length byte silently corrupts the signature script. - CSV pops its argument on Kaspa (unlike Bitcoin's non-popping CLTV/CSV) — no trailing
OP_DROP. - Declare the sig-op budget. Each input commits to a signature-operation count, and the
engine rejects a script that runs more checks than committed (
script units exceeded the amount committed in the input). A path doing N signature checks must setsig_op_count = Non the input before signing — the field is part of the sighash. Single-signature paths ride the default of 1; the escrow's two-signature settle path is the first to need 2. - Combine booleans with
OpAdd, not bitwiseOpOr.OpEqualpushes0x01for true and empty for false, whose byte-lengths differ; bitwiseOpOrrequires equal-length operands and hard-errors (OR operands must be of equal length) the moment a multi-entry allowlist matches a non-first entry. Sum the equality results withOpAdd(length-agnostic) and verify the nonzero total instead. This is a real latent bug that an all-rejecting test never reveals. - Don't bake editable parameters into the address. Anything in the redeem script (fees, caps) defines the P2SH address; changing such a constant after building orphans the coin. Keep tunables in config.
The limits rest on Kaspa consensus, demonstrated adversarially: rule-violating transactions, validly signed and submitted directly, are rejected by the script. This is a claim about the rules, not about key custody, availability, or the surrounding software. The owner key is a deliberate full bypass for recovery.
Toccata activates on Kaspa mainnet on June 30, 2026 (DAA 474,165,565), adding native L1 covenants and transaction introspection (KIP-17), covenant IDs (KIP-20), ZK verification opcodes (KIP-16), and sequencing commitments (KIP-21). SilverScript is the official higher-level compiler; this repo works in raw opcodes for transparency and to verify each primitive directly. The rusty-kaspa Toccata guide is the authoritative node/operator reference.
This project is licensed under the MIT License — use at your own risk; unaudited research code.
The Kaspa core developers behind Toccata, covenants, and SilverScript. The kaspa Python SDK.
And the consensus engine itself, which rejected every attack exactly as it should.