Covenant breaker
This is a worked example, not a real vault. The address, the balance, and the 10,000 KAS below are made up; nothing here is on any real blockchain.
A covenant is a rule locked into a coin that controls how it can be spent later. This vault holds 10,000 KAS behind four such rules. Get any of it to an address you control without satisfying all four.
Four rules stand between you and the balance below. Try to break one directly, or find the way through that never breaks any of them at all.
Status: the cap, the destination lock, and the delay shown here run on Kaspa mainnet today, using opcodes and covenant identity that activated with the Toccata upgrade (KIP-17 and KIP-20, both status Active). The rule is written below in Argent's syntax; Argent itself is a proposed contract language, still in research, not yet released. Only the notation is illustrative. The enforcement it describes is live.
Try to get money out of the vault
Set up an attempt by hand below, or start from one of these.
Direct attempts, each breaks one rule on purpose
Break no rule at all
≈ 60 minutes
Vault state (made up, for this example)
What the covenant enforces
- Who can signA withdrawal needs the owner's signature. Recovery needs the recovery key instead. No other key works.
- Where it can goA withdrawal may pay only the vault's fixed payout address, at most 500 KAS in this one transaction. Recovery may pay only the fixed recovery address, and always empties the vault.
- What comes nextEvery withdrawal must recreate the vault with the same KIP-20 covenant ID and the updated balance and last-spend score. A transaction that doesn't is spending a lookalike, not the vault.
- How soon againA withdrawal must wait 36,000 DAA score since the vault's last spend (roughly one hour at 10 blocks/second) before it is valid again.
Attempts so far
| # | Attempt | Amount | Sent to | Result |
|---|
Rule definitions and sources
state VaultState {
byte[32] owner_pk_hash;
byte[32] recovery_pk_hash;
byte[32] payout_spk_hash;
byte[32] recovery_spk_hash;
int balance;
int last_spend_daa;
}
actor Vault owns VaultState {
entry withdraw(int amount, sig owner_sig, pubkey owner_pk)
emits next: Vault
{
require(blake2b(owner_pk) == owner_pk_hash);
require(checkSig(owner_sig, owner_pk));
// where it can go: fixed cap, fixed destination
require(amount <= 500 && amount <= balance);
require(tx.outputSpkHash(0) == payout_spk_hash);
require(tx.outputAmount(0) == amount);
// how soon again: time since this covenant's own last spend
require(tx.inputDaaScore(self) - last_spend_daa >= 36000);
// what comes next: same KIP-20 covenant id, updated state
VaultState new_state = {
owner_pk_hash: owner_pk_hash,
recovery_pk_hash: recovery_pk_hash,
payout_spk_hash: payout_spk_hash,
recovery_spk_hash: recovery_spk_hash,
balance: balance - amount,
last_spend_daa: tx.inputDaaScore(self),
};
become next <- Vault(new_state);
}
entry recover(sig recovery_sig, pubkey recovery_pk) {
require(blake2b(recovery_pk) == recovery_pk_hash);
require(checkSig(recovery_sig, recovery_pk));
// no cap, no delay: recovery ignores both
// but the destination is still fixed, and the vault ends
require(tx.outputSpkHash(0) == recovery_spk_hash);
require(tx.outputAmount(0) == balance);
// no `become`: this covenant's lineage ends here
}
}
Pseudocode in Argent's actor/entry/require/become style, a proposed Kaspa contract language still in research and not yet released; see Argent and Silverscript, explained for that project's actual status. Introspection opcodes named against the primitives KIP-17 actually defines: OpTxOutputAmount, OpTxOutputSpk, OpTxInputDaaScore, OpTxLockTime. Both KIP-17 and its continuation-identity companion, KIP-20, are live on Kaspa mainnet as of the Toccata upgrade: a spend either carries the same covenant_id as the UTXO it consumes, or proves a fresh one through the genesis hash, so a forged lookalike UTXO cannot claim an existing covenant's identity. See Toccata explained for how these primitives compose into real applications.