Skip to content

Commit cc27e6c

Browse files
committed
Apply CIP-159 direct deposits per-transaction, not batch-wide
Each transaction's `txDirectDeposits` are now applied to the threaded `CertState` immediately after that transaction's `CERTS` step, replacing the batch-wide application introduced earlier on this branch. * `SUBLEDGER-V` applies `DirectDepositsOf stx` to `certState₁` after `CERTS` and emits the result. * `LEDGER-V` still applies the top-level transaction's deposits to `certState₂` after the top-level `CERTS`, but the premise now bounds `dom (DirectDepositsOf tx)` rather than the batch aggregate `dom (allDirectDeposits tx)`. * `certStateWithDDeps` is generalised to `∀ {ℓ} → Tx ℓ → CertState → CertState` so both rules can invoke it with the local `DirectDepositsOf`. The original draft of this branch argued that batch-wide application made CIP-159's phantom-asset prohibition manifest in the rule structure. Per-transaction application is equally safe: the prohibition is enforced separately by `NoPhantomWithdrawals` in the `Utxo` module, which bounds batch-wide withdrawal totals against the `accountBalances` field of `UTxOEnv`/`SubUTxOEnv` — fixed at the pre-batch snapshot `RewardsOf certState₀` regardless of when (or how) deposits are applied to the threaded `CertState`. Per-transaction application also mirrors the Haskell ledger executor's sequential per-transaction processing, which simplifies the conformance proof and avoids a structural gap between the spec and the implementation. A direct-deposit registration premise is added to each rule to prevent `applyDirectDeposits` from silently re-introducing a credential into `rewards` after deregistration earlier in the same transaction: * `SUBLEDGER-V` requires `dom (DirectDepositsOf stx) ⊆ dom (RewardsOf certState₁)`. * `LEDGER-V` requires `dom (DirectDepositsOf tx) ⊆ dom (RewardsOf certState₂)`. The check is local to each transaction's post-`CERTS` state, so a sub-transaction whose deposit targets a credential deregistered by an earlier sub-transaction in the same batch will fail the premise. `Computational-SUBLEDGER` and `Computational-LEDGER` are updated to dispatch on these decidable premises in `computeProof` and `completeness`. `depositsChange` is unaffected: it reads only the protocol-deposit fields of `DState`/`PState`/`GState`, which `applyDirectDeposits` does not touch. Refs: #1122
1 parent d1691c9 commit cc27e6c

3 files changed

Lines changed: 263 additions & 311 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### WIP
66

7-
- Apply batch-wide direct deposits to `CertState` in `LEDGER-V` output (CIP-159).
7+
- Apply per-transaction direct deposits to `CertState` after each `CERTS` step in `SUBLEDGER-V` and `LEDGER-V` (CIP-159).
88
- Forbid CIP-159 fields (`txDirectDeposits`, `txBalanceIntervals`) in legacy mode via explicit `UTXOW-legacy` premises.
99
- Allow partial withdrawals in `PRE-CERT` rule; define `applyWithdrawals` and `_≤ᵐ_` (CIP-159).
1010
- Extend `TxInfo` with `txDirectDeposits` and `txBalanceIntervals` fields (CIP-159).

src/Ledger/Dijkstra/Specification/Ledger.lagda.md

Lines changed: 123 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -280,17 +280,137 @@ UTxOEnv{.AgdaDatatype}/SubUTxOEnv{.AgdaDatatype}.
280280
for both enacted CC credentials and any newly proposed CC actions that appear in
281281
the final (post-subtransaction processing) `GovState`{.AgdaRecord}.
282282

283+
### Direct Deposit Application (CIP-159)
284+
285+
Each transaction's direct deposits (`txDirectDeposits`{.AgdaField}) are applied to
286+
the threaded `CertState`{.AgdaRecord} immediately after that transaction's
287+
`CERTS`{.AgdaDatatype} step, before the rule emits its output state:
288+
289+
+ In `SUBLEDGER-V`{.AgdaInductiveConstructor}, after `CERTS`{.AgdaDatatype} produces
290+
`certState₁`, the deposits in `DirectDepositsOf stx` are credited to the
291+
`rewards`{.AgdaField} map of `certState₁`'s `dState`{.AgdaField}, yielding the
292+
`certStateFinal` carried in the `SUBLEDGER`{.AgdaDatatype} output.
293+
+ In `LEDGER-V`{.AgdaInductiveConstructor}, after the top-level
294+
`CERTS`{.AgdaDatatype} produces `certState₂`, the deposits in
295+
`DirectDepositsOf tx` are credited the same way, yielding the `certStateFinal`
296+
carried in the `LEDGER`{.AgdaDatatype} output.
297+
298+
The helper `certStateWithDDeps`{.AgdaFunction} performs the per-transaction update.
299+
It uses `applyDirectDeposits`{.AgdaFunction} (from the `Certs`{.AgdaModule} module),
300+
which adds deposit amounts to the `DState`{.AgdaRecord} `rewards`{.AgdaField} map
301+
via `∪⁺`{.AgdaFunction} (union-with-addition):
302+
303+
```agda
304+
certStateWithDDeps : ∀ {ℓ} → Tx ℓ → CertState → CertState
305+
certStateWithDDeps tx cs = record cs { dState = applyDirectDeposits (DirectDepositsOf tx) (DStateOf cs) }
306+
```
307+
308+
`certStateWithDDeps`{.AgdaFunction} is invoked once per transaction in the batch
309+
(once per sub-transaction inside `SUBLEDGER-V`{.AgdaInductiveConstructor}, plus
310+
once for the top-level transaction inside `LEDGER-V`{.AgdaInductiveConstructor})
311+
on the *post-`CERTS`{.AgdaDatatype}* `CertState`{.AgdaRecord} for that
312+
transaction. Because `applyDirectDeposits`{.AgdaFunction} only modifies
313+
`rewards`{.AgdaField} and leaves `voteDelegs`{.AgdaField},
314+
`stakeDelegs`{.AgdaField}, `deposits`{.AgdaField}, `pState`{.AgdaField}, and
315+
`gState`{.AgdaField} unchanged, applying it transaction-by-transaction is
316+
equivalent to aggregating all direct deposits in the batch via `∪⁺`{.AgdaFunction}
317+
and applying the sum at the end.
318+
319+
`depositsChange`{.AgdaFunction} is computed from `certState₀`{.AgdaBound},
320+
`certState₁`{.AgdaBound}, and `certState₂`{.AgdaBound} (not from
321+
`certStateFinal`{.AgdaBound}) because it measures *protocol* deposit movements
322+
(registration/deregistration of credentials, DReps, pools), which live in the
323+
`deposits`{.AgdaField} fields of `DState`{.AgdaRecord}/`PState`{.AgdaRecord}/
324+
`GState`{.AgdaRecord} — not in `rewards`{.AgdaField}. Since
325+
`applyDirectDeposits`{.AgdaFunction} only updates `rewards`{.AgdaField}, the two
326+
notions of "deposit" remain cleanly disjoint without case analysis.
327+
328+
`rmOrphanDRepVotes`{.AgdaFunction} in `LEDGER-V`{.AgdaInductiveConstructor}
329+
receives `certStateFinal`{.AgdaBound} (rather than `certState₂`{.AgdaBound}) so
330+
that it sees the post-deposit `DRep`{.AgdaInductiveConstructor} state. In
331+
practice the result is the same either way (`applyDirectDeposits`{.AgdaFunction}
332+
does not modify `dreps`{.AgdaField}), but using `certStateFinal`{.AgdaBound} is
333+
the correct expression of intent.
334+
335+
#### Direct deposit registration premise
336+
337+
Each rule additionally requires that the credentials targeted by that
338+
transaction's direct deposits are registered in the *post-`CERTS`{.AgdaDatatype}*
339+
`CertState`{.AgdaRecord}:
340+
341+
+ `SUBLEDGER-V`{.AgdaInductiveConstructor} requires
342+
`dom (DirectDepositsOf stx) ⊆ dom (RewardsOf certState₁)`.
343+
+ `LEDGER-V`{.AgdaInductiveConstructor} requires
344+
`dom (DirectDepositsOf tx) ⊆ dom (RewardsOf certState₂)`.
345+
346+
Without this premise, `applyDirectDeposits`{.AgdaFunction} could silently
347+
re-introduce a credential into `rewards`{.AgdaField} that had been deregistered
348+
earlier in the same transaction's `CERTS`{.AgdaDatatype} step (and thus is no
349+
longer present in `voteDelegs`{.AgdaField}, `stakeDelegs`{.AgdaField}, or
350+
`deposits`{.AgdaField}), producing an inconsistent `DState`{.AgdaRecord}. The
351+
domain check rules this out at phase 1. Note that the check is performed
352+
against the post-`CERTS`{.AgdaDatatype} state of the *same* transaction, so
353+
deregistrations performed by *prior* sub-transactions in the batch are
354+
correctly accounted for: a sub-transaction whose deposit targets a credential
355+
deregistered by an earlier sub-transaction will fail this premise.
356+
357+
### Design Rationale: Per-transaction Direct Deposit Application
358+
359+
Direct deposits are applied per-transaction (above), not aggregated and applied
360+
batch-wide at the end of `LEDGER-V`{.AgdaInductiveConstructor}.
361+
362+
+ **Phantom asset prevention is enforced by `NoPhantomWithdrawals`{.AgdaFunction}, not by deposit placement**.
363+
364+
CIP-159 forbids "phantom asset" attacks in which a sub-transaction's direct
365+
deposit inflates the balance available to a later sub-transaction's withdrawal
366+
within the same batch. This restriction is enforced in the
367+
`Utxo`{.AgdaModule} module by the `NoPhantomWithdrawals`{.AgdaFunction}
368+
predicate, which bounds *batch-wide* withdrawal totals (per reward address)
369+
by the `accountBalances`{.AgdaField} field of `UTxOEnv`{.AgdaRecord} and
370+
`SubUTxOEnv`{.AgdaRecord} — the *pre-batch* snapshot
371+
`RewardsOf certState₀`{.AgdaBound}. Because `accountBalances`{.AgdaField} is
372+
fixed at the pre-batch value and never updated by direct deposit
373+
application, the CIP-159 phantom-asset prohibition holds regardless of
374+
whether deposits are applied per-transaction or batch-wide.
375+
376+
+ **Per-transaction application matches the Haskell implementation**.
377+
378+
The Haskell ledger executor processes each transaction in a batch
379+
sequentially and applies that transaction's effects (including direct
380+
deposits) before moving on to the next transaction. Per-transaction
381+
application in the formal specification mirrors this execution model
382+
directly, which simplifies the conformance proof and avoids a structural
383+
gap between the spec and the implementation.
384+
385+
+ **CIP-118 script context isolation is preserved by `accountBalances`{.AgdaField}, not by deposit placement**.
386+
387+
CIP-118 requires that Plutus scripts in one sub-transaction do not see
388+
other sub-transactions or the top-level transaction in their context. In
389+
the current spec, `accountBalances`{.AgdaField} (used for balance-interval
390+
checks and any future Plutus context derived from this field) is held fixed
391+
at `RewardsOf certState₀`{.AgdaBound} across the entire batch, so every
392+
sub-transaction sees the same pre-batch balances regardless of when other
393+
sub-transactions' deposits are applied.
394+
395+
+ **`depositsChange`{.AgdaFunction} remains orthogonal**.
396+
397+
`calculateDepositsChange`{.AgdaFunction} reads only the
398+
`deposits`{.AgdaField} fields of `DState`{.AgdaRecord}/`PState`{.AgdaRecord}/
399+
`GState`{.AgdaRecord}, which `applyDirectDeposits`{.AgdaFunction} does not
400+
touch. Whether direct deposits are applied per-transaction or batch-wide,
401+
`depositsChange`{.AgdaFunction} is unaffected.
402+
283403
```agda
284404
data _⊢_⇀⦇_,SUBLEDGER⦈_ : SubLedgerEnv → LedgerState → SubLevelTx → LedgerState → Type where
285405
286406
SUBLEDGER-V :
287407
∙ isTopLevelValid ≡ true
288408
∙ ⟦ slot , pp , treasury , utxo₀ , isTopLevelValid , allScripts , accountBalances ⟧ ⊢ utxoState₀ ⇀⦇ stx ,SUBUTXOW⦈ utxoState₁
289409
∙ ⟦ epoch slot , pp , ListOfGovVotesOf stx , WithdrawalsOf stx , allColdCreds govState₀ enactState ⟧ ⊢ certState₀ ⇀⦇ DCertsOf stx ,CERTS⦈ certState₁
410+
∙ dom (DirectDepositsOf stx) ⊆ dom (RewardsOf certState₁)
290411
∙ ⟦ TxIdOf stx , epoch slot , pp , ppolicy , enactState , certState₁ , dom (RewardsOf certState₁) ⟧ ⊢ govState₀ ⇀⦇ GovProposals+Votes stx ,GOVS⦈ govState₁
291412
────────────────────────────────
292-
⟦ slot , ppolicy , pp , enactState , treasury , utxo₀ , isTopLevelValid , allScripts , accountBalances ⟧ ⊢ ⟦ utxoState₀ , govState₀ , certState₀ ⟧ ⇀⦇ stx ,SUBLEDGER⦈ ⟦ utxoState₁ , govState₁ , certState₁ ⟧
293-
413+
⟦ slot , ppolicy , pp , enactState , treasury , utxo₀ , isTopLevelValid , allScripts , accountBalances ⟧ ⊢ ⟦ utxoState₀ , govState₀ , certState₀ ⟧ ⇀⦇ stx ,SUBLEDGER⦈ ⟦ utxoState₁ , govState₁ , certStateWithDDeps stx certState₁ ⟧
294414
SUBLEDGER-I :
295415
∙ isTopLevelValid ≡ false
296416
∙ ⟦ slot , pp , treasury , utxo₀ , isTopLevelValid , allScripts , accountBalances ⟧ ⊢ utxoState₀ ⇀⦇ stx ,SUBUTXOW⦈ utxoState₀
@@ -299,123 +419,8 @@ data _⊢_⇀⦇_,SUBLEDGER⦈_ : SubLedgerEnv → LedgerState → SubLevelTx
299419
300420
_⊢_⇀⦇_,SUBLEDGERS⦈_ : SubLedgerEnv → LedgerState → List SubLevelTx → LedgerState → Type
301421
_⊢_⇀⦇_,SUBLEDGERS⦈_ = ReflexiveTransitiveClosure {sts = _⊢_⇀⦇_,SUBLEDGER⦈_}
302-
```
303422
304-
**Direct Deposit Application (CIP-159)**. After all sub-rule transitions
305-
(`SUBLEDGERS`{.AgdaDatatype}, `CERTS`{.AgdaDatatype}, `GOVS`{.AgdaDatatype},
306-
`UTXOW`{.AgdaDatatype}), batch-wide direct deposits are applied to the final
307-
`CertState`{.AgdaRecord}. The function `applyDirectDeposits`{.AgdaFunction} (from
308-
the `Certs`{.AgdaModule} module) adds deposit amounts to the `DState`
309-
`rewards` map (stake credential reward account balances) via `∪⁺`,
310-
and `allDirectDeposits`{.AgdaFunction} (from the `Transaction`{.AgdaModule} module)
311-
aggregates direct deposits across the top-level transaction and all sub-transactions.
312423
313-
Direct deposits are applied *after* withdrawal processing (in `CERTS`{.AgdaDatatype})
314-
to ensure that withdrawals are checked against pre-batch balances. This prevents
315-
phantom asset attacks where a deposit from one sub-transaction inflates the balance
316-
available for withdrawal by another sub-transaction in the same batch.
317-
318-
```agda
319-
certStateWithDDeps : TopLevelTx → CertState → CertState
320-
certStateWithDDeps tx cs = record cs { dState = applyDirectDeposits (allDirectDeposits tx) (DStateOf cs) }
321-
```
322-
323-
`depositsChange`{.AgdaFunction} is computed from `certStateᵢ` (`i ∈ {0,1,2}`)
324-
(not `certStateFinal`) since it represents net deposit change across the batch
325-
(not direct deposit value transfers) and reflects registration/deregistration.
326-
327-
`rmOrphanDRepVotes` uses `certStateFinal` (not `certState₂`) so it sees
328-
the post-deposit `DRep` state. (In practice, `applyDirectDeposits`{.AgdaFunction}
329-
only modifies rewards, so `rmOrphanDRepVotes` would produce the same result either
330-
way, but using `certStateFinal` is semantically correct.)
331-
332-
**Deposits target post-batch registered accounts**. `SUBUTXO`{.AgdaDatatype}
333-
and `UTXO`{.AgdaDatatype} check that direct-deposit targets are registered in the
334-
*pre-batch* balances, but they cannot account for deregistrations that occur during
335-
the batch. Therefore, we add a new premise, `dom (allDirectDeposits tx) ⊆ dom (RewardsOf
336-
certState₂)`, to check that `applyDirectDeposits` does not silently re-introduce a
337-
deregistered credential into the `rewards` map without re-registering it via
338-
`voteDelegs`, `stakeDelegs`, or `deposits`, as that would result in an inconsistent
339-
`DState`. The premise rules this out at phase 1.
340-
341-
### Design Rationale: Batch-wide Direct Deposit Application
342-
343-
A natural alternative to applying direct deposits batch-wide (as above) is to
344-
interleave them with the per-sub-transaction processing performed by
345-
`SUBLEDGERS`{.AgdaDatatype}; that is, in each `SUBLEDGER-V`{.AgdaInductiveConstructor}
346-
step, apply that sub-transaction's `DirectDepositsOf stx` to the threaded
347-
`CertState`{.AgdaRecord} after running `CERTS`{.AgdaDatatype}/`GOVS`{.AgdaDatatype}
348-
for the sub-transaction.
349-
350-
**The CIP forbids this alternative, and the rule structure
351-
adopted here makes the prohibition manifest rather than imposing it as an extra
352-
premise.**
353-
354-
+ **CIP-159 rules out per-sub-transaction application**.
355-
356-
CIP-159 states that, "to prevent ... *phantom asset* attacks, transactions can
357-
only withdraw funds that exist in the account *before* the overall transaction
358-
is run. This means later sub-txs cannot withdraw assets that were deposited
359-
by prior sub-txs in the same overall transaction." Per-sub-transaction
360-
application would let sub-transaction `N`'s `PRE-CERT`{.AgdaDatatype} step
361-
authorize withdrawals against a `rewards`{.AgdaField} map already inflated by
362-
sub-transactions `1..N−1`'s deposits — exactly the situation the CIP forbids.
363-
364-
+ **Phantom asset prevention follows structurally**.
365-
366-
Because `applyDirectDeposits`{.AgdaFunction} never runs against the
367-
`CertState`{.AgdaRecord} threaded through `SUBLEDGERS`{.AgdaDatatype}, every
368-
`PRE-CERT`{.AgdaDatatype} step in the batch checks each withdrawal `amt` against
369-
`rewards`{.AgdaField} as updated only by `applyWithdrawals`{.AgdaFunction} and
370-
by registration/deregistration certificates. Summed across the batch this
371-
yields, for every reward address, total withdrawals bounded by the pre-batch
372-
balance (i.e., the `NoPhantomWithdrawals`{.AgdaFunction} property) *as a
373-
consequence of the rule structure* rather than as a separate premise that would
374-
need to be added and discharged.
375-
376-
+ **Alignment with CIP-118 script context isolation**.
377-
378-
CIP-118 requires that "Plutus scripts in one sub-transaction do not see other
379-
sub-transactions or the top-level transaction in their context." Holding the
380-
`accountBalances`{.AgdaField} field of `SubLedgerEnv`{.AgdaRecord} and
381-
`SubUTxOEnv`{.AgdaRecord} fixed at the pre-batch snapshot `RewardsOf certState₀`
382-
ensures that every sub-transaction's balance-interval check (and any future Plutus
383-
context derived from this field) sees the same balances independently of the
384-
surrounding sub-transactions. Per-sub-transaction application would make these
385-
views order-dependent across sub-transactions.
386-
387-
+ **Direct deposits are a diff, not a sequence of state updates**.
388-
389-
CIP-159 specifies that the `direct_deposits` field carries only the diff to be
390-
applied (e.g., `0.1 ADA`, not the resulting `100.1 ADA` balance). Aggregating
391-
each sub-transaction's diff via `∪⁺` (union-with-addition) and applying the
392-
sum once is the natural reading: the batch-level
393-
`allDirectDeposits`{.AgdaFunction} is the diff from pre-batch to post-batch.
394-
Per-sub-transaction application would impose an artificial sequential
395-
interpretation on what is logically a commutative aggregation.
396-
397-
+ **Determinism and reorderability**.
398-
399-
CIP-159 motivates direct deposits in part by their freedom from contention:
400-
deposits into the same account from different parties can be processed "in any
401-
order and fully parallel." Batch-wide application makes this manifest at the
402-
rule level: the order of sub-transactions in the batch does not affect the
403-
`accountBalances`{.AgdaField} visible to any sub-transaction's
404-
`SUBUTXO`{.AgdaDatatype} or `CERTS`{.AgdaDatatype} step, nor the final
405-
`rewards`{.AgdaField} reached after the deposit step.
406-
407-
+ **`depositsChange`{.AgdaFunction} remains orthogonal**.
408-
409-
`calculateDepositsChange`{.AgdaFunction} measures *protocol* deposit movements
410-
(registration/deregistration of credentials, DReps, pools), which live in the
411-
`deposits`{.AgdaField} fields of `DState`{.AgdaRecord}/`PState`{.AgdaRecord}/
412-
`GState`{.AgdaRecord} — not in `rewards`{.AgdaField}. Because
413-
`applyDirectDeposits`{.AgdaFunction} touches only `rewards`{.AgdaField} and
414-
runs *after* `calculateDepositsChange`{.AgdaFunction}, the two notions of
415-
"deposit" remain cleanly disjoint without case analysis.
416-
417-
418-
```agda
419424
data _⊢_⇀⦇_,LEDGER⦈_ : LedgerEnv → LedgerState → TopLevelTx → LedgerState → Type where
420425
421426
LEDGER-V :
@@ -434,7 +439,7 @@ data _⊢_⇀⦇_,LEDGER⦈_ : LedgerEnv → LedgerState → TopLevelTx → Ledg
434439
∙ IsValidFlagOf tx ≡ true
435440
∙ ⟦ slot , ppolicy , pp , enactState , treasury , utxo₀ , IsValidFlagOf tx , allScripts , RewardsOf certState₀ ⟧ ⊢ ⟦ utxoState₀ , govState₀ , certState₀ ⟧ ⇀⦇ SubTransactionsOf tx ,SUBLEDGERS⦈ ⟦ utxoState₁ , govState₁ , certState₁ ⟧
436441
∙ ⟦ epoch slot , pp , ListOfGovVotesOf tx , WithdrawalsOf tx , allColdCreds govState₁ enactState ⟧ ⊢ certState₁ ⇀⦇ DCertsOf tx ,CERTS⦈ certState₂
437-
∙ dom (allDirectDeposits tx) ⊆ dom (RewardsOf certState₂)
442+
∙ dom (DirectDepositsOf tx) ⊆ dom (RewardsOf certState₂)
438443
∙ ⟦ TxIdOf tx , epoch slot , pp , ppolicy , enactState , certState₂ , dom (RewardsOf certState₂) ⟧ ⊢ govState₁ ⇀⦇ GovProposals+Votes tx ,GOVS⦈ govState₂
439444
∙ ⟦ slot , pp , treasury , utxo₀ , depositsChange , allScripts , RewardsOf certState₀ ⟧ ⊢ utxoState₁ ⇀⦇ tx ,UTXOW⦈ utxoState₂
440445
────────────────────────────────

0 commit comments

Comments
 (0)