@@ -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
284404data _⊢_⇀⦇_,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
419424data _⊢_⇀⦇_,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