Skip to content

Latest commit

 

History

History
49 lines (27 loc) · 4.68 KB

File metadata and controls

49 lines (27 loc) · 4.68 KB

Protocol mechanics

This page covers the parts of LedgerKeep where the design decisions carry weight: how a contract proves ownership to the registry, what the maintenance record contains, and exactly what the vault can and cannot verify.

The registry's authorization model

The registry maps a contract address to a maintenance manifest. The obvious risk is that anyone could publish a false manifest for a contract they do not control, pointing a keeper at the wrong keys.

LedgerKeep prevents this with one line. Registration requires contract.require_auth() — authorization from the address being registered.

The reason this works is a property of Soroban addresses. A contract address has no private key. Nothing outside the contract can produce authorization for it. The only way contract.require_auth() is satisfied is if the call originates from inside that contract's own code. So a contract can register itself, and nothing else can register it.

This is why a protocol adopting the standard needs a small function of its own — something like register_with — that calls the registry from inside the contract. The authorization is satisfied because the call comes from within. See Adopting the standard for how to write it.

The same check guards update and deregister. A published manifest can only be changed or removed by the contract it describes.

What maintenance records

When extend_all runs, it does three things in order:

  1. Extends the contract's instance storage.
  2. Extends each declared persistent key. Extension is conditional — a key already comfortably alive is not touched.
  3. Writes a record: the ledger sequence at which maintenance ran, and the address that called it.

That record is MaintenanceState, and lk_state returns it. It is the only thing the vault reads to decide whether to pay.

Note what step 2 means: after a successful extend_all, a key that was already healthy shows no change. That is correct behaviour, not a failure. It also means the return value of extend_all is the ledger sequence, not a count of keys extended — a distinction that matters when reading the code.

What the vault verifies, and what it cannot

The vault pays a keeper for maintenance. It confirms three things before paying:

  1. Maintenance happened since the last payout. The recorded ledger is later than the vault's last-claim ledger.
  2. The claimant did the work. The recorded keeper address matches the address claiming the tip. This closes the gap where a bystander could claim a tip for someone else's extend_all.
  3. The interval has elapsed. Enough ledgers have passed since the last payout, so a keeper cannot drain the vault with rapid repeat claims.

Here is what it cannot confirm: that the maintenance was necessary. No contract can read time-to-live at runtime, so the vault cannot check whether the extended keys were actually close to expiry. A keeper could call extend_all more often than needed and collect one tip per interval.

This is bounded, not eliminated. The worst case is that a keeper pays its own transaction fees to call extend_all more often than necessary and earns one tip per interval — the vault owner sets the tip and interval, caps total exposure by the vault balance, and can withdraw at any time. It is a rate limit, not a proof of necessity. Stating this plainly is more useful than pretending the vault verifies more than it does. The economic model works through the numbers that make this safe in practice.

The clock runs from payout, not from work

One subtlety in the interval check: after a successful claim, the vault's last-claim ledger is set to the current ledger, not to the ledger at which maintenance was recorded. This means a keeper cannot batch several maintenance runs and then claim several tips in quick succession. The interval always measures from the last payout forward.

Manifest drift

The keys a contract publishes to the registry are advisory. Nothing on-chain forces them to match the keys the contract's compiled macro actually extends. A protocol could publish one list and extend another — through a mistake, or a change to the contract that the manifest was not updated to reflect.

This is called drift, and the keeper detects it. Before maintaining a contract, the keeper simulates extend_all and reads the resulting footprint — the exact set of ledger keys the compiled contract touches. Comparing that against the published manifest catches drift in both directions: keys the manifest declares but the contract ignores, and keys the contract extends but the manifest never declared. The keeper reports drift; it does not try to fix it. See Running a keeper.