@@ -280,17 +280,130 @@ 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 ` 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 ` that appears in the final ` LedgerState ` {.AgdaRecord} of the
293+ ` SUBLEDGER ` {.AgdaDatatype} relation.
294+ + In ` LEDGER-V ` {.AgdaInductiveConstructor}, after the top-level
295+ ` CERTS ` {.AgdaDatatype} produces ` certState₂ ` , the deposits in
296+ ` DirectDepositsOf tx ` are credited the same way, yielding the ` certStateFinal `
297+ that appears in the final ` LedgerState ` {.AgdaRecord} of the
298+ ` LEDGER ` {.AgdaDatatype} relation.
299+
300+ The helper ` certStateWithDDeps ` {.AgdaFunction} performs the per-transaction update.
301+ It uses ` applyDirectDeposits ` {.AgdaFunction} (from the ` Certs ` {.AgdaModule} module),
302+ which adds deposit amounts to the ` DState ` {.AgdaRecord} ` rewards ` {.AgdaField} map
303+ via ` ∪⁺ ` {.AgdaFunction} (union with addition).
304+
305+ ``` agda
306+ certStateWithDDeps : ∀ {ℓ} → Tx ℓ → CertState → CertState
307+ certStateWithDDeps tx cs = record cs { dState = applyDirectDeposits (DirectDepositsOf tx) (DStateOf cs) }
308+ ```
309+
310+ ` certStateWithDDeps ` {.AgdaFunction} is invoked once per transaction in the batch
311+ (once per sub-transaction inside ` SUBLEDGER-V ` {.AgdaInductiveConstructor}, plus
312+ once for the top-level transaction inside ` LEDGER-V ` {.AgdaInductiveConstructor})
313+ on the * post-` CERTS ` {.AgdaDatatype}* ` CertState ` {.AgdaRecord} for that
314+ transaction. (Since ` applyDirectDeposits ` {.AgdaFunction} only modifies
315+ ` rewards ` {.AgdaField}, and leaves ` voteDelegs ` {.AgdaField},
316+ ` stakeDelegs ` {.AgdaField}, ` deposits ` {.AgdaField}, ` pState ` {.AgdaField}, and
317+ ` gState ` {.AgdaField} unchanged, applying it per-transaction is
318+ equivalent to aggregating all direct deposits in the batch via ` ∪⁺ ` {.AgdaFunction}
319+ and applying the sum at the end.)
320+
321+ ` depositsChange ` {.AgdaFunction} is computed from ` certState₀ ` {.AgdaBound},
322+ ` certState₁ ` {.AgdaBound}, and ` certState₂ ` {.AgdaBound} (not from
323+ ` certStateFinal ` {.AgdaBound}) because it measures * protocol* deposit movements
324+ (registration/deregistration of credentials, DReps, pools), which live in the
325+ ` deposits ` {.AgdaField} fields of ` DState ` {.AgdaRecord}/` PState ` {.AgdaRecord}/
326+ ` GState ` {.AgdaRecord} — not in ` rewards ` {.AgdaField}. Since
327+ ` applyDirectDeposits ` {.AgdaFunction} only updates ` rewards ` {.AgdaField}, the two
328+ notions of "deposit" remain cleanly disjoint.
329+
330+ ` rmOrphanDRepVotes ` {.AgdaFunction} in ` LEDGER-V ` {.AgdaInductiveConstructor}
331+ receives ` certStateFinal ` {.AgdaBound} (rather than ` certState₂ ` {.AgdaBound}) so
332+ that it sees the post-deposit ` DRep ` {.AgdaInductiveConstructor} state. In
333+ practice the result is the same either way, since ` applyDirectDeposits ` {.AgdaFunction}
334+ does not modify ` dreps ` {.AgdaField}.
335+
336+ #### Direct deposit registration premise
337+
338+ Each rule additionally requires that the credentials targeted by that
339+ transaction's direct deposits are registered in the * post-` CERTS ` {.AgdaDatatype}*
340+ ` CertState ` {.AgdaRecord}.
341+
342+ + ` SUBLEDGER-V ` {.AgdaInductiveConstructor} requires
343+ ` dom (DirectDepositsOf stx) ⊆ dom (RewardsOf certState₁) ` .
344+ + ` LEDGER-V ` {.AgdaInductiveConstructor} requires
345+ ` dom (DirectDepositsOf tx) ⊆ dom (RewardsOf certState₂) ` .
346+
347+ Without this premise, ` applyDirectDeposits ` {.AgdaFunction} could silently
348+ re-introduce a credential into ` rewards ` {.AgdaField} that had been deregistered
349+ earlier in the same transaction's ` CERTS ` {.AgdaDatatype} step (and thus is no
350+ longer present in ` voteDelegs ` {.AgdaField}, ` stakeDelegs ` {.AgdaField}, or
351+ ` deposits ` {.AgdaField}), producing an inconsistent ` DState ` {.AgdaRecord}. The
352+ domain check rules this out at phase 1. Note that the check is performed
353+ against the post-` CERTS ` {.AgdaDatatype} state of the * same* transaction, so
354+ deregistrations performed by * prior* sub-transactions in the batch are
355+ correctly accounted for; a sub-transaction whose deposit targets a credential
356+ deregistered by an earlier sub-transaction will fail this premise.
357+
358+ ### Design Rationale: Per-transaction Direct Deposit Application
359+
360+ Here we justify the choice to apply direct deposits per-transaction, instead of
361+ aggregating and applying them batch-wide at the end of
362+ ` LEDGER-V ` {.AgdaInductiveConstructor}.
363+
364+ + ** Phantom asset prevention is enforced by ` NoPhantomWithdrawals ` {.AgdaFunction}** .
365+
366+ CIP-159 forbids "phantom asset" attacks in which a sub-transaction's direct
367+ deposit inflates the balance available to a later sub-transaction's withdrawal
368+ within the same batch. This restriction is enforced in the ` Utxo ` {.AgdaModule}
369+ module by the ` NoPhantomWithdrawals ` {.AgdaFunction} predicate, which bounds
370+ * batch-wide* withdrawal totals (per reward address) by the
371+ ` accountBalances ` {.AgdaField} field of ` UTxOEnv ` {.AgdaRecord} and
372+ ` SubUTxOEnv ` {.AgdaRecord} — the * pre-batch* snapshot `RewardsOf
373+ certState₀` {.AgdaBound}. Because ` accountBalances`{.AgdaField} is fixed at the
374+ pre-batch value and never updated by direct deposit application, the CIP-159
375+ phantom-asset prohibition holds regardless of whether deposits are applied
376+ per-transaction or batch-wide.
377+
378+ + ** CIP-118 script context isolation is preserved by ` accountBalances ` {.AgdaField}** .
379+
380+ CIP-118 requires that Plutus scripts in one sub-transaction do not see other
381+ sub-transactions or the top-level transaction in their context. In the current
382+ spec, ` accountBalances ` {.AgdaField} (used for balance-interval checks and any
383+ future Plutus context derived from this field) is held fixed at
384+ ` RewardsOf certState₀ ` {.AgdaBound} across the entire batch, so every
385+ sub-transaction sees the same pre-batch balances regardless of when deposits of
386+ other sub-transactions are applied.
387+
388+ + ** ` depositsChange ` {.AgdaFunction} remains orthogonal** .
389+
390+ ` calculateDepositsChange ` {.AgdaFunction} reads only the ` deposits ` {.AgdaField}
391+ fields of ` DState ` {.AgdaRecord}/` PState ` {.AgdaRecord}/` GState ` {.AgdaRecord}, which
392+ ` applyDirectDeposits ` {.AgdaFunction} does not touch. Whether direct deposits are
393+ applied per-transaction or batch-wide, ` depositsChange ` {.AgdaFunction} is
394+ unaffected.
395+
283396``` agda
284397data _⊢_⇀⦇_,SUBLEDGER⦈_ : SubLedgerEnv → LedgerState → SubLevelTx → LedgerState → Type where
285398
286399 SUBLEDGER-V :
287400 ∙ isTopLevelValid ≡ true
288401 ∙ ⟦ slot , pp , treasury , utxo₀ , isTopLevelValid , allScripts , accountBalances ⟧ ⊢ utxoState₀ ⇀⦇ stx ,SUBUTXOW⦈ utxoState₁
289402 ∙ ⟦ epoch slot , pp , ListOfGovVotesOf stx , WithdrawalsOf stx , allColdCreds govState₀ enactState ⟧ ⊢ certState₀ ⇀⦇ DCertsOf stx ,CERTS⦈ certState₁
403+ ∙ dom (DirectDepositsOf stx) ⊆ dom (RewardsOf certState₁)
290404 ∙ ⟦ TxIdOf stx , epoch slot , pp , ppolicy , enactState , certState₁ , dom (RewardsOf certState₁) ⟧ ⊢ govState₀ ⇀⦇ GovProposals+Votes stx ,GOVS⦈ govState₁
291405 ────────────────────────────────
292- ⟦ slot , ppolicy , pp , enactState , treasury , utxo₀ , isTopLevelValid , allScripts , accountBalances ⟧ ⊢ ⟦ utxoState₀ , govState₀ , certState₀ ⟧ ⇀⦇ stx ,SUBLEDGER⦈ ⟦ utxoState₁ , govState₁ , certState₁ ⟧
293-
406+ ⟦ slot , ppolicy , pp , enactState , treasury , utxo₀ , isTopLevelValid , allScripts , accountBalances ⟧ ⊢ ⟦ utxoState₀ , govState₀ , certState₀ ⟧ ⇀⦇ stx ,SUBLEDGER⦈ ⟦ utxoState₁ , govState₁ , certStateWithDDeps stx certState₁ ⟧
294407 SUBLEDGER-I :
295408 ∙ isTopLevelValid ≡ false
296409 ∙ ⟦ slot , pp , treasury , utxo₀ , isTopLevelValid , allScripts , accountBalances ⟧ ⊢ utxoState₀ ⇀⦇ stx ,SUBUTXOW⦈ utxoState₀
@@ -299,123 +412,8 @@ data _⊢_⇀⦇_,SUBLEDGER⦈_ : SubLedgerEnv → LedgerState → SubLevelTx
299412
300413_⊢_⇀⦇_,SUBLEDGERS⦈_ : SubLedgerEnv → LedgerState → List SubLevelTx → LedgerState → Type
301414_⊢_⇀⦇_,SUBLEDGERS⦈_ = ReflexiveTransitiveClosure {sts = _⊢_⇀⦇_,SUBLEDGER⦈_}
302- ```
303415
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.
312416
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
419417data _⊢_⇀⦇_,LEDGER⦈_ : LedgerEnv → LedgerState → TopLevelTx → LedgerState → Type where
420418
421419 LEDGER-V :
@@ -434,7 +432,7 @@ data _⊢_⇀⦇_,LEDGER⦈_ : LedgerEnv → LedgerState → TopLevelTx → Ledg
434432 ∙ IsValidFlagOf tx ≡ true
435433 ∙ ⟦ slot , ppolicy , pp , enactState , treasury , utxo₀ , IsValidFlagOf tx , allScripts , RewardsOf certState₀ ⟧ ⊢ ⟦ utxoState₀ , govState₀ , certState₀ ⟧ ⇀⦇ SubTransactionsOf tx ,SUBLEDGERS⦈ ⟦ utxoState₁ , govState₁ , certState₁ ⟧
436434 ∙ ⟦ epoch slot , pp , ListOfGovVotesOf tx , WithdrawalsOf tx , allColdCreds govState₁ enactState ⟧ ⊢ certState₁ ⇀⦇ DCertsOf tx ,CERTS⦈ certState₂
437- ∙ dom (allDirectDeposits tx) ⊆ dom (RewardsOf certState₂)
435+ ∙ dom (DirectDepositsOf tx) ⊆ dom (RewardsOf certState₂)
438436 ∙ ⟦ TxIdOf tx , epoch slot , pp , ppolicy , enactState , certState₂ , dom (RewardsOf certState₂) ⟧ ⊢ govState₁ ⇀⦇ GovProposals+Votes tx ,GOVS⦈ govState₂
439437 ∙ ⟦ slot , pp , treasury , utxo₀ , depositsChange , allScripts , RewardsOf certState₀ ⟧ ⊢ utxoState₁ ⇀⦇ tx ,UTXOW⦈ utxoState₂
440438 ────────────────────────────────
0 commit comments