Skip to content
Draft
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
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,16 @@ MINT_TRANSACTION_RATE_LIMIT_PER_MINUTE=20
# MINT_AUTH_RATE_LIMIT_PER_MINUTE=5
# Maximum number of blind auth tokens per authentication request
# MINT_AUTH_MAX_BLIND_TOKENS=100

# Proof of Liabilities (PoL) Settings
# Time window in seconds for each synchronized publication epoch (default: 86400, i.e., 24 hours)
MINT_POL_EPOCH_SECONDS=86400

# If TRUE, generate fake OpenTimestamps receipts offline without making network requests (default: FALSE)
MINT_POL_MOCK_OTS=FALSE

# Probability (0.0 to 1.0) of randomly forgetting to include some proofs/promises in the PoL trees for testing/debugging
MINT_POL_FORGET_PROBABILITY=0.0

# Probability (0.0 to 1.0) of randomly altering the value of included proofs/promises in the PoL trees for testing/debugging
MINT_POL_CHEAT_VALUE_PROBABILITY=0.0
20 changes: 20 additions & 0 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class Proof(BaseModel):
melt_id: Union[None, str] = (
None # holds the id of the melt operation that destroyed this proof
)
pol_receipt: Optional["PolReceipt"] = None

def __init__(self, **data):
super().__init__(**data)
Expand All @@ -158,6 +159,19 @@ def from_dict(cls, proof_dict: dict):
else:
# overwrite the empty string with None
proof_dict["dleq"] = None

pol_receipt_val = proof_dict.get("pol_receipt")
if pol_receipt_val:
try:
proof_dict["pol_receipt"] = PolReceipt.model_validate_json(pol_receipt_val)
except Exception:
try:
proof_dict["pol_receipt"] = PolReceipt.model_validate(pol_receipt_val)
except Exception:
proof_dict["pol_receipt"] = None
else:
proof_dict["pol_receipt"] = None

c = cls(**proof_dict)
return c

Expand Down Expand Up @@ -254,6 +268,11 @@ def p2pksigs(self) -> List[str]:
return P2PKWitness.from_witness(self.witness).signatures


class PolReceipt(BaseModel):
target_epoch: int
signature: str


class BlindedSignature(BaseModel):
"""
Blinded signature or "promise" which is the signature on a `BlindedMessage`
Expand All @@ -263,6 +282,7 @@ class BlindedSignature(BaseModel):
amount: int
C_: str # Hex-encoded signature
dleq: Optional[DLEQ] = None # DLEQ proof
pol_receipt: Optional[PolReceipt] = None

@classmethod
def from_row(cls, row: Row):
Expand Down
3 changes: 2 additions & 1 deletion cashu/core/models/melt_quote.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pydantic import BaseModel, Field

from cashu.core.base import BlindedSignature, MeltQuote
from cashu.core.base import BlindedSignature, MeltQuote, PolReceipt
from cashu.core.constants import MAX_PAYMENT_REQUEST_LEN, MAX_UNIT_LEN


Expand Down Expand Up @@ -54,6 +54,7 @@ class PostMeltQuoteResponse(BaseModel):
expiry: Optional[int] # expiry of the quote
payment_preimage: Optional[str] = None # payment preimage
change: Union[List[BlindedSignature], None] = None # NUT-08 change
spent_receipts: Optional[List[PolReceipt]] = None

@classmethod
def from_melt_quote(cls, melt_quote: MeltQuote) -> "PostMeltQuoteResponse":
Expand Down
2 changes: 2 additions & 0 deletions cashu/core/models/swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
BlindedMessage,
BlindedMessage_Deprecated,
BlindedSignature,
PolReceipt,
Proof,
)
from cashu.core.settings import settings
Expand All @@ -20,6 +21,7 @@ class PostSwapRequest(BaseModel):

class PostSwapResponse(BaseModel):
signatures: List[BlindedSignature]
spent_receipts: Optional[List[PolReceipt]] = None


# deprecated since 0.13.0
Expand Down
24 changes: 24 additions & 0 deletions cashu/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,30 @@ class MintSettings(CashuSettings):
description="Interval (in seconds) for running regular tasks like the invoice checker.",
)

mint_pol_epoch_seconds: int = Field(
default=86400,
title="PoL Epoch seconds",
description="Time window for Proof of Liability epochs.",
)

mint_pol_mock_ots: bool = Field(
default=True,
title="Mock OTS",
description="If True, generate mock OTS receipts without making network requests.",
)

mint_pol_forget_probability: float = Field(
default=0.0,
title="PoL Forget Probability",
description="Probability (0.0 to 1.0) of randomly forgetting to include some proofs/promises in the Merkle-Sum Trees for testing and debugging.",
)

mint_pol_cheat_value_probability: float = Field(
default=0.0,
title="PoL Cheat Value Probability",
description="Probability (0.0 to 1.0) of randomly altering (adding/subtracting) the value of included proofs/promises in the Merkle-Sum Trees for testing and debugging.",
)

mint_retry_exponential_backoff_base_delay: int = Field(default=1)
mint_retry_exponential_backoff_max_delay: int = Field(default=10)

Expand Down
11 changes: 7 additions & 4 deletions cashu/mint/crud.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import time
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Tuple

Expand Down Expand Up @@ -352,8 +353,8 @@ async def store_blinded_message(
await (conn or db).execute(
f"""
INSERT INTO {db.table_with_schema("promises")}
(amount, b_, id, created, mint_quote, melt_quote, swap_id, order_index)
VALUES (:amount, :b_, :id, :created, :mint_quote, :melt_quote, :swap_id, :order_index)
(amount, b_, id, created, mint_quote, melt_quote, swap_id, order_index, pol_sequence)
VALUES (:amount, :b_, :id, :created, :mint_quote, :melt_quote, :swap_id, :order_index, :pol_sequence)
""",
{
"amount": amount,
Expand All @@ -364,6 +365,7 @@ async def store_blinded_message(
"melt_quote": melt_id,
"swap_id": swap_id,
"order_index": order_index,
"pol_sequence": time.time_ns(),
},
)

Expand Down Expand Up @@ -500,8 +502,8 @@ async def invalidate_proof(
await (conn or db).execute(
f"""
INSERT INTO {db.table_with_schema("proofs_used")}
(amount, c, secret, y, id, witness, created, melt_quote)
VALUES (:amount, :c, :secret, :y, :id, :witness, :created, :melt_quote)
(amount, c, secret, y, id, witness, created, melt_quote, pol_sequence)
VALUES (:amount, :c, :secret, :y, :id, :witness, :created, :melt_quote, :pol_sequence)
""",
{
"amount": proof.amount,
Expand All @@ -512,6 +514,7 @@ async def invalidate_proof(
"witness": proof.witness,
"created": db.to_timestamp(db.timestamp_now_str()),
"melt_quote": quote_id,
"pol_sequence": time.time_ns(),
},
)

Expand Down
16 changes: 16 additions & 0 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
from .events.events import LedgerEventManager
from .features import LedgerFeatures
from .keysets import LedgerKeysets
from .pol import update_pol_manifests
from .tasks import LedgerTasks
from .verification import LedgerVerification
from .watchdog import LedgerWatchdog
Expand Down Expand Up @@ -147,6 +148,7 @@ async def startup_ledger(self) -> None:
await self._startup_keysets()
await self._check_backends()
self.regular_tasks.append(asyncio.create_task(self._run_regular_tasks()))
self.regular_tasks.append(asyncio.create_task(self._run_pol_regular_tasks()))
self.invoice_listener_tasks = await self.dispatch_listeners()
if settings.mint_watchdog_enabled:
self.watchdog_tasks = await self.dispatch_watchdogs()
Expand All @@ -171,6 +173,20 @@ async def _run_regular_tasks(self) -> None:
logger.error(f"Ledger regular task failed: {e}")
await asyncio.sleep(60)

async def _run_pol_regular_tasks(self) -> None:
"""
Runs periodic PoL epoch checks and manifest updates.
"""
logger.info("Starting ledger PoL regular tasks loop")
interval = settings.mint_pol_epoch_seconds or 86400
while True:
try:
await update_pol_manifests(self)
await asyncio.sleep(interval)
except Exception as e:
logger.error(f"Ledger PoL regular task failed: {e}")
await asyncio.sleep(60)

async def _check_backends(self) -> None:
for method in self.backends:
for unit in self.backends[method]:
Expand Down
35 changes: 35 additions & 0 deletions cashu/mint/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -1261,3 +1261,38 @@ async def m035_add_last_checked_to_mint_quotes(db: Database):
ADD COLUMN last_checked TIMESTAMP NULL
"""
)


async def m036_pol_epochs(db: Database):
"""
Create pol_epochs table to store historical PoL roots and OTS receipts.
"""
async with db.connect() as conn:
await conn.execute(
f"""
CREATE TABLE IF NOT EXISTS {db.table_with_schema("pol_epochs")} (
keyset_id TEXT NOT NULL,
epoch_index INTEGER NOT NULL,
timestamp TIMESTAMP NOT NULL,
previous_global_digest TEXT NOT NULL,
issued_mmr_size {db.big_int} NOT NULL,
root_issued_hash TEXT NOT NULL,
root_issued_sum {db.big_int} NOT NULL,
spent_mmr_size {db.big_int} NOT NULL,
root_spent_hash TEXT NOT NULL,
root_spent_sum {db.big_int} NOT NULL,
outstanding_balance {db.big_int} NOT NULL,
ots_receipt TEXT NOT NULL,
signature TEXT NOT NULL,
PRIMARY KEY (keyset_id, epoch_index)
);
"""
)
await conn.execute(
f"ALTER TABLE {db.table_with_schema('promises')} "
f"ADD COLUMN pol_sequence {db.big_int}"
)
await conn.execute(
f"ALTER TABLE {db.table_with_schema('proofs_used')} "
f"ADD COLUMN pol_sequence {db.big_int}"
)
Loading