Skip to content

fix(security,#433): reconcile pre-existing ledgerless backing on top-up - #456

Merged
dcccrypto merged 1 commit into
dcccrypto:mainfrom
Bayyan16:fix/433-ledgerless-backing-reconciliation
Sep 2, 2026
Merged

fix(security,#433): reconcile pre-existing ledgerless backing on top-up#456
dcccrypto merged 1 commit into
dcccrypto:mainfrom
Bayyan16:fix/433-ledgerless-backing-reconciliation

Conversation

@Bayyan16

@Bayyan16 Bayyan16 commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Follow-up for #433.

This PR reconciles backing-domain accounting for pre-existing funded domains whose canonical BackingDomainLedger did not exist yet when TopUpBackingBucket first creates it.

The creation/existence half is already handled on the top-up path. The remaining accounting gap was that a newly-created ledger started with:

total_principal_atoms = 0

even when the engine bucket already contained historical backing.

That made the canonical ledger authoritative only for deposits observed after its creation, rather than for the provider's actual outstanding principal.

This PR reconstructs the migration baseline from the existing engine bucket before applying the new top-up.


Problem

For a legacy funded domain with no canonical backing ledger, the bucket can already contain principal across:

fresh_unliened
+ valid_liened
+ consumed_liened
+ impaired_liened

Creating the canonical ledger alone is therefore not sufficient.

Example:

legacy backing before ledger creation:

fresh     = 60
consumed  = 40
total     = 100

new top-up = 20

Without reconciliation, the newly-created ledger would account for only the new 20, even though the engine bucket already represents historical principal.

There is also an accounting constraint:

available
  = total_principal
    - (cumulative_loss - cumulative_recovery)

Because of that, historical principal cannot safely be seeded by itself.

Historical consumed/impaired backing must also be represented as unavailable principal. Otherwise, creating the ledger late could incorrectly convert previously consumed or impaired backing into withdrawable principal.


Implementation

The reconciliation is intentionally migration-specific to TopUpBackingBucket.

It does not change the generic behavior of:

read_or_new_backing_domain_ledger(...)

That helper is also used by LP-vault accounting paths where an uninitialized ledger can legitimately represent zero contribution for a domain that has never funded the vault.

For a newly-created/uninitialized ledger on the top-up path, the migration baseline is reconstructed from the pre-top-up bucket snapshot:

gross principal
  = fresh_unliened
  + valid_liened
  + consumed_liened
  + impaired_liened

using the existing BOUND_SCALE conversion semantics.

The migrated ledger is seeded as:

total_principal_atoms
    = gross historical principal

cumulative_loss_atoms
    = currently unavailable principal

cumulative_recovery_atoms
    = 0

last_observed_unavailable_principal_atoms
    = same pre-top-up unavailable snapshot

This preserves:

available
  = total_principal
    - (cumulative_loss - cumulative_recovery)

as the bucket's currently recoverable principal at migration time.

Consumed/impaired backing therefore does not become newly available simply because the canonical ledger was initialized late.


Existing Ledgers Stay on the Normal Sync Path

The migration applies only when the canonical ledger is still uninitialized.

Existing initialized ledgers continue through the normal synchronization path:

if initialized {
    sync_backing_domain_ledger(&mut ledger, &bucket)?;
} else {
    seed_legacy_backing_domain_ledger(&mut ledger, &bucket)?;
}

This keeps the reconciliation narrowly scoped to legacy ledger initialization and avoids changing accounting semantics for already-established ledgers.


Non-Zero Top-Up Handling

For a legacy ledgerless domain receiving new capital, reconciliation occurs against the bucket before the engine top-up mutation.

The sequence is:

read pre-top-up bucket
        ↓
reconstruct historical ledger baseline
        ↓
perform engine top-up
        ↓
add new deposit to ledger principal/deposited counters
        ↓
re-baseline unavailable watermark
        ↓
validate engine shape
        ↓
write/init canonical ledger

The post-top-up watermark adjustment is important.

A top-up can refill provider_receivable and reduce consumed backing. That reduction represents new capital, not recovery of historical loss.

Without re-baselining the unavailable-principal watermark after the refill, the next normal synchronization could incorrectly book that reduction a second time as:

cumulative_recovery_atoms

This PR prevents that double-booking.


Zero-Capital Migration Path

TopUpBackingBucket(amount = 0) can also reconcile a still-ledgerless funded domain without requiring the provider to contribute additional capital solely to trigger migration.

The zero-amount path performs:

canonical PDA exists / is created
        ↓
authority is validated
        ↓
existing bucket is snapshotted
        ↓
historical accounting baseline is reconstructed
        ↓
engine shape is validated
        ↓
canonical ledger is initialized

No engine backing-deposit mutation is performed for amount == 0.

This provides an explicit migration trigger for historical backing without requiring a non-zero deposit.


Regression Coverage

Two compiled-BPF regressions were added for the legacy ledgerless state.

1. Zero-Top-Up Migration + Withdrawal

v16_bpf_legacy_ledgerless_backing_zero_topup_reconciles_before_withdraw

Fixture:

historical principal = 100

bucket:
  fresh    = 60
  consumed = 40

canonical ledger:
  absent

The test verifies that a zero-amount top-up:

  • creates/reconciles the canonical ledger;
  • reconstructs gross principal as 100;
  • preserves the existing 40 unavailable amount as the loss baseline;
  • does not make historical consumed backing newly withdrawable;
  • allows the recoverable 60 principal to be withdrawn;
  • leaves the remaining ledger principal consistent with the remaining vault balance.

2. Non-Zero Top-Up Does Not Become Recovery

v16_bpf_legacy_ledgerless_nonzero_topup_does_not_book_refill_as_recovery

Starting from:

fresh    = 60
consumed = 40

a new top-up of 20 can refill provider receivable.

The regression verifies that after reconciliation:

principal = historical principal + new deposit
loss      = historical unavailable baseline
recovery  = 0

and that a subsequent normal SyncBackingDomainLedger does not book the new top-up again as historical recovery.


Non-Vacuous Harness Coverage

The legacy fixtures deliberately avoid the V16CuEnv backing helpers that route through:

canonical_backing_domain_ledger_account

Those helpers set_account the canonical ledger into existence and would make the migration proof vacuous.

Instead, the legacy backing state is injected directly while keeping the related engine consumption counters coherent, including:

consumed_liened_backing_num
spent_backing_num
provider_receivable_num

The production TopUpBackingBucket path then performs the normal engine validation before committing the migrated state.

The relevant instructions are exercised against compiled program bytes.


Watched Negative Control

A watched negative control was performed to verify that the regression actually observes the reconciliation logic.

Procedure:

  1. build the good implementation;
  2. verify the legacy zero-top-up migration regression passes;
  3. remove only the reconciliation call from the zero-top-up migration path;
  4. delete the previous SBF artifact;
  5. rebuild a fresh negative-control SBF binary;
  6. rerun the exact same regression.

The negative-control build failed exactly at:

migration must reconstruct gross outstanding principal

left:  0
right: 100

The known-good source was then restored byte-for-byte, rebuilt, and the positive regression passed again.

This confirms that the test is observing the migration itself rather than passing because the harness silently pre-created the canonical ledger.


Verification

Compiled SBF / LiteSVM verification:

v16_bpf_backing_topup_then_withdraw_works_without_an_lp_vault
PASS

v16_bpf_legacy_ledgerless_backing_zero_topup_reconciles_before_withdraw
PASS

v16_bpf_legacy_ledgerless_nonzero_topup_does_not_book_refill_as_recovery
PASS

The existing no-LP-vault top-up → withdrawal proof-of-life remains green.

Full v16_cu run after building the sibling percolator-match BPF:

105 passed
1 failed

The only failure was:

v16_bpf_stale_full_14_leg_tradenocpi_is_under_tx_limit

with the transaction exhausting the 1,400,000 CU meter.

The same test was reproduced against a clean upstream/main worktree at the PR base using the same SBF toolchain and matcher binary.

The clean upstream baseline failed identically.

Therefore, that CU-bound TradeNoCpi failure is pre-existing baseline behavior and is not introduced by this backing-ledger reconciliation change.


Scope

Changed files:

src/v16_program.rs
tests/v16_cu.rs

This PR does not change:

  • generic read_or_new_backing_domain_ledger semantics;
  • unrelated LP-vault accounting;
  • withdrawal authorization;
  • trade/CPI paths;
  • compute-unit limits;
  • engine data layout.

The patch is intentionally limited to the legacy backing-ledger reconciliation lifecycle and its regression coverage.


Deployment-Window Note

There is one state this PR intentionally does not attempt to infer.

If a domain:

  1. was already funded while ledgerless;
  2. then received a non-zero top-up after the ledger-creation change was deployed;
  3. and therefore already has an initialized but historically under-seeded ledger,

the current bucket alone does not safely identify how much of the initialized ledger is historical versus legitimately observed after initialization.

This PR therefore reconstructs the accounting baseline only for still-uninitialized legacy ledgers.

It does not heuristically rewrite an already-initialized ledger.

If any initialized-under-seeded domains exist in that deployment window, they should be identified explicitly and handled through a bounded one-time repair/migration path rather than inferred from current bucket state.


Relation to #433

This implements the legacy-reconciliation follow-up discussed in #433:

create/require canonical ledger on top-up
        ↓
reconcile pre-existing ledgerless backing
        ↓
make ledger principal authoritative for migrated domains

I am intentionally using Refs #433 rather than automatically closing the issue, pending maintainer/ops confirmation on the initialized-under-seeded deployment-window case described above.

Refs #433

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: ea54daa6-4308-4e9f-adf5-eb339f3b2f86


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5a79e60a7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/v16_program.rs
write_or_init_backing_domain_ledger(data, ledger, *initialized)?;
}
}
if amount == 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow zero-capital reconciliation after resolution

When a legacy ledgerless domain is already in Resolved mode, this migration branch is unreachable because the shared preflight rejects every non-Live market at line 10537. WithdrawBackingBucket explicitly supports mode 1 but now requires the ledger account, so backing in such a market cannot be reconciled or withdrawn. Permit this zero-capital initialization in the resolved wind-down state, or provide an equivalent ledger-creation path during withdrawal.

Useful? React with 👍 / 👎.

Comment thread src/v16_program.rs
Comment on lines +10105 to +10107
ledger.total_principal_atoms = gross_principal_atoms;
ledger.cumulative_loss_atoms = unavailable_atoms;
ledger.cumulative_recovery_atoms = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Seed outstanding legacy backing earnings

When a legacy ledgerless bucket already has nonzero utilization_fee_earnings, this migration reconstructs principal and impairment but leaves total_earnings_atoms at zero while read_or_new_backing_domain_ledger has already pinned the earnings watermark to the bucket's current value. Subsequent syncs therefore never recognize those earnings; withdrawing them instead increments total_earnings_withdrawn_atoms above total earnings and leaves the canonical ledger inconsistent. Seed the currently outstanding earnings as part of the migration baseline.

Useful? React with 👍 / 👎.

@dcccrypto
dcccrypto merged commit be8c8dd into dcccrypto:main Sep 2, 2026
2 of 3 checks passed
@dcccrypto

Copy link
Copy Markdown
Owner

Merged. Reviewed against the merge result rather than the diff, and the tests pass once the environment is right.

The fix

seed_legacy_backing_domain_ledger reconstructs the accounting baseline for a domain funded before its ledger existed — the migration case my #433 fix (e8acd708) created by requiring the ledger going forward.

Two things in it are the reason this merged without changes:

Seeding principal and impairment together, not principal alone:

Seeding principal alone would make consumed/impaired backing appear available under: available = principal - (loss - recovery)

Pinning the watermark to the same snapshot, so the first normal sync cannot book the migration baseline as a newly observed loss — and then re-baselining after the top-up, because a refill that satisfies provider receivable would otherwise read as a recovery. That is a double-count in each direction, and both are closed.

Keeping read_or_new_backing_domain_ledger's zero-principal semantics for other callers, rather than changing it globally, is also right: for LP-vault accounting an uninitialized ledger genuinely means that vault has not funded the domain.

Two things I checked that looked wrong and were not

1. The diff appears to delete three tests..._trade_fee_policy_is_marketauth_gated..., ..._update_base_unit_mints_rejects_mismatched_decimals, ..._rebalance_reduce_is_blocked_once_resolve_has_matured. Those are the tests from #450 and #455, which merged after this branch was cut. The true diff (merge-base..head) removes zero tests, and all three are present in the merge result. Diff-against-main artifact, not scope creep.

2. Both of this PR's new tests FAILED locallyCustom(3) and an assertion mismatch. Not the code: target/deploy/percolator_prog.so was built 1 Sep, so the LiteSVM tests were exercising new behaviour against a binary that predates it.

After cargo build-sbf: v16_cu 106 passed / 0 failed.

That is worth flagging for anyone else running these locally — a stale .so fails in a way that looks like a logic bug, and the check-the-environment-first instinct is what resolves it.

Suite

  • v16_cu 106 passed / 0 failed (baseline 104/0 — this PR's two new tests)
  • v16_wrapper 227 passed / 19 failed — the same 19 that fail on untouched origin/main, verified by comparing failing test names, not counts

CI's build + test is red, but it is red on main too and has been for many consecutive commits — a pre-existing condition, not this PR's.

Merged, not deployed. Pending release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants