Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/aleph/db/accessors/balances.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,16 @@ def _consume_address_credits(
in place.

Emission order matches the historical FIFO: ``(message_timestamp, credit_ref,
credit_index) ASC``. ``message_timestamp`` is the cutoff for "still valid":
only lots with ``expiration_date IS NULL OR expiration_date > message_timestamp``
are eligible. Using the message timestamp (not wall-clock now) keeps eager
writes consistent with the repair replay, which uses the historical timestamp
when reconstructing state from ``credit_history``.
credit_index) ASC``. ``message_timestamp`` bounds eligibility on both ends:
a lot is drainable only if it was granted at or before the expense
(``lot.message_timestamp <= message_timestamp``) and is not yet expired
(``expiration_date IS NULL OR expiration_date > message_timestamp``). The
lower bound prevents a backdated expense from draining a grant that did
not yet exist at the expense's instant; messages can arrive out of order
in the P2P pipeline, so a future-dated grant may already be in the cache
when an older expense lands. Using the message timestamp (not wall-clock
now) keeps eager writes consistent with the repair replay, which walks
history chronologically and enforces both bounds by construction.

Lots are locked ``FOR UPDATE`` to serialise concurrent writers for the same
address. Over-draw silently drops the excess, matching the prior FIFO
Expand All @@ -300,6 +305,7 @@ def _consume_address_credits(
.where(
AlephCreditBalanceDb.address == address,
AlephCreditBalanceDb.amount_remaining > 0,
AlephCreditBalanceDb.message_timestamp <= message_timestamp,
(
AlephCreditBalanceDb.expiration_date.is_(None)
| (AlephCreditBalanceDb.expiration_date > message_timestamp)
Expand Down
42 changes: 42 additions & 0 deletions tests/db/test_credit_balances.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,48 @@ def test_balance_fix_doesnt_affect_valid_credits(session_factory: DbSessionFacto
assert balance > 0


def test_backdated_expense_cannot_drain_future_grant(
session_factory: DbSessionFactory,
):
"""A backdated expense must not deduct from a lot whose grant happened
after the expense's ``message_timestamp``.

Messages can arrive out of order in the P2P pipeline, so the eager writer
may see a future-dated grant already in the lot cache when an older
expense lands. The drain filter must exclude any lot whose
``message_timestamp`` is strictly greater than the expense's, matching
what the repair replay would derive from credit_history.
"""

grant_timestamp = dt.datetime(2024, 6, 1, 12, 0, 0, tzinfo=dt.timezone.utc)
expense_timestamp = dt.datetime(2024, 5, 1, 12, 0, 0, tzinfo=dt.timezone.utc)

with session_factory() as session:
_give_credits(
session=session,
address="0xbackdated",
amount=1000,
expiration_ms=None,
msg_hash="future_grant",
msg_ts=grant_timestamp,
)
session.commit()

update_credit_balances_expense(
session=session,
credits_list=[
{"address": "0xbackdated", "amount": 400, "ref": "backdated_expense"}
],
message_hash="backdated_expense_msg",
message_timestamp=expense_timestamp,
)
session.commit()

# The grant survives in full (1000 * 10_000 precision multiplier).
balance = get_credit_balance(session, "0xbackdated")
assert balance == 10_000_000


def test_fifo_scenario_1_non_expiring_first_equals_0_remaining(
session_factory: DbSessionFactory,
):
Expand Down
7 changes: 5 additions & 2 deletions tests/message_processing/test_process_posts.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ async def test_credit_transfer_non_whitelisted_sender(
)

with session_factory() as session:
# Give the regular sender some credits via a whitelisted distribution
# Seed the sender before the transfer message's ``time`` field
# (1651050219.0 = 2022-04-27 09:43:39 UTC) so the lot's
# message_timestamp precedes the transfer's, otherwise the eager-write
# drain correctly refuses to deduct from a future-dated grant.
update_credit_balances_distribution(
session=session,
credits_list=[
Expand All @@ -218,7 +221,7 @@ async def test_credit_transfer_non_whitelisted_sender(
token="ALEPH",
chain="ETH",
message_hash="init_dist_hash_abc",
message_timestamp=dt.datetime(2023, 1, 1, tzinfo=dt.timezone.utc),
message_timestamp=dt.datetime(2022, 1, 1, tzinfo=dt.timezone.utc),
)
session.commit()

Expand Down
Loading