Token state check with Y (#468)

* Token state check with Y

* remove backwards compat for v1
This commit is contained in:
callebtc
2024-03-12 15:53:18 +01:00
committed by GitHub
parent 4f52973908
commit 150195d66a
9 changed files with 99 additions and 88 deletions

View File

@@ -486,7 +486,7 @@ class PostSplitResponse_Very_Deprecated(BaseModel):
class PostCheckStateRequest(BaseModel): class PostCheckStateRequest(BaseModel):
secrets: List[str] = Field(..., max_items=settings.mint_max_request_length) Ys: List[str] = Field(..., max_items=settings.mint_max_request_length)
class SpentState(Enum): class SpentState(Enum):
@@ -499,7 +499,7 @@ class SpentState(Enum):
class ProofState(BaseModel): class ProofState(BaseModel):
secret: str Y: str
state: SpentState state: SpentState
witness: Optional[str] = None witness: Optional[str] = None

View File

@@ -34,7 +34,8 @@ class LedgerCrud(ABC):
derivation_path: str = "", derivation_path: str = "",
seed: str = "", seed: str = "",
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[MintKeyset]: ... ) -> List[MintKeyset]:
...
@abstractmethod @abstractmethod
async def get_spent_proofs( async def get_spent_proofs(
@@ -42,7 +43,8 @@ class LedgerCrud(ABC):
*, *,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[Proof]: ... ) -> List[Proof]:
...
async def get_proof_used( async def get_proof_used(
self, self,
@@ -50,7 +52,8 @@ class LedgerCrud(ABC):
Y: str, Y: str,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[Proof]: ... ) -> Optional[Proof]:
...
@abstractmethod @abstractmethod
async def invalidate_proof( async def invalidate_proof(
@@ -59,16 +62,18 @@ class LedgerCrud(ABC):
db: Database, db: Database,
proof: Proof, proof: Proof,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ... ) -> None:
...
@abstractmethod @abstractmethod
async def get_proofs_pending( async def get_proofs_pending(
self, self,
*, *,
proofs: List[Proof], Ys: List[str],
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[Proof]: ... ) -> List[Proof]:
...
@abstractmethod @abstractmethod
async def set_proof_pending( async def set_proof_pending(
@@ -77,12 +82,14 @@ class LedgerCrud(ABC):
db: Database, db: Database,
proof: Proof, proof: Proof,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ... ) -> None:
...
@abstractmethod @abstractmethod
async def unset_proof_pending( async def unset_proof_pending(
self, *, proof: Proof, db: Database, conn: Optional[Connection] = None self, *, proof: Proof, db: Database, conn: Optional[Connection] = None
) -> None: ... ) -> None:
...
@abstractmethod @abstractmethod
async def store_keyset( async def store_keyset(
@@ -91,14 +98,16 @@ class LedgerCrud(ABC):
db: Database, db: Database,
keyset: MintKeyset, keyset: MintKeyset,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ... ) -> None:
...
@abstractmethod @abstractmethod
async def get_balance( async def get_balance(
self, self,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> int: ... ) -> int:
...
@abstractmethod @abstractmethod
async def store_promise( async def store_promise(
@@ -112,7 +121,8 @@ class LedgerCrud(ABC):
e: str = "", e: str = "",
s: str = "", s: str = "",
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ... ) -> None:
...
@abstractmethod @abstractmethod
async def get_promise( async def get_promise(
@@ -121,7 +131,8 @@ class LedgerCrud(ABC):
db: Database, db: Database,
B_: str, B_: str,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[BlindedSignature]: ... ) -> Optional[BlindedSignature]:
...
@abstractmethod @abstractmethod
async def store_mint_quote( async def store_mint_quote(
@@ -130,7 +141,8 @@ class LedgerCrud(ABC):
quote: MintQuote, quote: MintQuote,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ... ) -> None:
...
@abstractmethod @abstractmethod
async def get_mint_quote( async def get_mint_quote(
@@ -139,7 +151,8 @@ class LedgerCrud(ABC):
quote_id: str, quote_id: str,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[MintQuote]: ... ) -> Optional[MintQuote]:
...
@abstractmethod @abstractmethod
async def get_mint_quote_by_checking_id( async def get_mint_quote_by_checking_id(
@@ -148,7 +161,8 @@ class LedgerCrud(ABC):
checking_id: str, checking_id: str,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[MintQuote]: ... ) -> Optional[MintQuote]:
...
@abstractmethod @abstractmethod
async def update_mint_quote( async def update_mint_quote(
@@ -157,7 +171,8 @@ class LedgerCrud(ABC):
quote: MintQuote, quote: MintQuote,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ... ) -> None:
...
# @abstractmethod # @abstractmethod
# async def update_mint_quote_paid( # async def update_mint_quote_paid(
@@ -176,7 +191,8 @@ class LedgerCrud(ABC):
quote: MeltQuote, quote: MeltQuote,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ... ) -> None:
...
@abstractmethod @abstractmethod
async def get_melt_quote( async def get_melt_quote(
@@ -186,7 +202,8 @@ class LedgerCrud(ABC):
db: Database, db: Database,
checking_id: Optional[str] = None, checking_id: Optional[str] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Optional[MeltQuote]: ... ) -> Optional[MeltQuote]:
...
@abstractmethod @abstractmethod
async def update_melt_quote( async def update_melt_quote(
@@ -195,7 +212,8 @@ class LedgerCrud(ABC):
quote: MeltQuote, quote: MeltQuote,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ... ) -> None:
...
class LedgerCrudSqlite(LedgerCrud): class LedgerCrudSqlite(LedgerCrud):
@@ -256,9 +274,11 @@ class LedgerCrudSqlite(LedgerCrud):
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[Proof]: ) -> List[Proof]:
rows = await (conn or db).fetchall(f""" rows = await (conn or db).fetchall(
f"""
SELECT * from {table_with_schema(db, 'proofs_used')} SELECT * from {table_with_schema(db, 'proofs_used')}
""") """
)
return [Proof(**r) for r in rows] if rows else [] return [Proof(**r) for r in rows] if rows else []
async def invalidate_proof( async def invalidate_proof(
@@ -289,16 +309,16 @@ class LedgerCrudSqlite(LedgerCrud):
async def get_proofs_pending( async def get_proofs_pending(
self, self,
*, *,
proofs: List[Proof], Ys: List[str],
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[Proof]: ) -> List[Proof]:
rows = await (conn or db).fetchall( rows = await (conn or db).fetchall(
f""" f"""
SELECT * from {table_with_schema(db, 'proofs_pending')} SELECT * from {table_with_schema(db, 'proofs_pending')}
WHERE Y IN ({','.join(['?']*len(proofs))}) WHERE Y IN ({','.join(['?']*len(Ys))})
""", """,
tuple(proof.Y for proof in proofs), tuple(Ys),
) )
return [Proof(**r) for r in rows] return [Proof(**r) for r in rows]
@@ -549,9 +569,11 @@ class LedgerCrudSqlite(LedgerCrud):
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> int: ) -> int:
row = await (conn or db).fetchone(f""" row = await (conn or db).fetchone(
f"""
SELECT * from {table_with_schema(db, 'balance')} SELECT * from {table_with_schema(db, 'balance')}
""") """
)
assert row, "Balance not found" assert row, "Balance not found"
return int(row[0]) return int(row[0])

View File

@@ -886,7 +886,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
logger.debug(f"Loaded {len(spent_proofs_list)} used proofs") logger.debug(f"Loaded {len(spent_proofs_list)} used proofs")
self.spent_proofs = {p.Y: p for p in spent_proofs_list} self.spent_proofs = {p.Y: p for p in spent_proofs_list}
async def check_proofs_state(self, secrets: List[str]) -> List[ProofState]: async def check_proofs_state(self, Ys: List[str]) -> List[ProofState]:
"""Checks if provided proofs are spend or are pending. """Checks if provided proofs are spend or are pending.
Used by wallets to check if their proofs have been redeemed by a receiver or they are still in-flight in a transaction. Used by wallets to check if their proofs have been redeemed by a receiver or they are still in-flight in a transaction.
@@ -895,32 +895,26 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
and which isn't. and which isn't.
Args: Args:
proofs (List[Proof]): List of proofs to check. Ys (List[str]): List of Y's of proofs to check
Returns: Returns:
List[bool]: List of which proof is still spendable (True if still spendable, else False) List[bool]: List of which proof is still spendable (True if still spendable, else False)
List[bool]: List of which proof are pending (True if pending, else False) List[bool]: List of which proof are pending (True if pending, else False)
""" """
states: List[ProofState] = [] states: List[ProofState] = []
proofs_spent_idx_secret = await self._get_proofs_spent_idx_secret(secrets) proofs_spent = await self._get_proofs_spent(Ys)
proofs_pending_idx_secret = await self._get_proofs_pending_idx_secret(secrets) proofs_pending = await self._get_proofs_pending(Ys)
for secret in secrets: for Y in Ys:
if ( if Y not in proofs_spent and Y not in proofs_pending:
secret not in proofs_spent_idx_secret states.append(ProofState(Y=Y, state=SpentState.unspent))
and secret not in proofs_pending_idx_secret elif Y not in proofs_spent and Y in proofs_pending:
): states.append(ProofState(Y=Y, state=SpentState.pending))
states.append(ProofState(secret=secret, state=SpentState.unspent))
elif (
secret not in proofs_spent_idx_secret
and secret in proofs_pending_idx_secret
):
states.append(ProofState(secret=secret, state=SpentState.pending))
else: else:
states.append( states.append(
ProofState( ProofState(
secret=secret, Y=Y,
state=SpentState.spent, state=SpentState.spent,
witness=proofs_spent_idx_secret[secret].witness, witness=proofs_spent[Y].witness,
) )
) )
return states return states
@@ -971,7 +965,9 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
""" """
assert ( assert (
len( len(
await self.crud.get_proofs_pending(proofs=proofs, db=self.db, conn=conn) await self.crud.get_proofs_pending(
Ys=[p.Y for p in proofs], db=self.db, conn=conn
)
) )
== 0 == 0
), TransactionError("proofs are pending.") ), TransactionError("proofs are pending.")

View File

@@ -338,7 +338,7 @@ async def check_state(
) -> PostCheckStateResponse: ) -> PostCheckStateResponse:
"""Check whether a secret has been spent already or not.""" """Check whether a secret has been spent already or not."""
logger.trace(f"> POST /v1/checkstate: {payload}") logger.trace(f"> POST /v1/checkstate: {payload}")
proof_states = await ledger.check_proofs_state(payload.secrets) proof_states = await ledger.check_proofs_state(payload.Ys)
return PostCheckStateResponse(states=proof_states) return PostCheckStateResponse(states=proof_states)

View File

@@ -328,7 +328,7 @@ async def check_spendable_deprecated(
) -> CheckSpendableResponse_deprecated: ) -> CheckSpendableResponse_deprecated:
"""Check whether a secret has been spent already or not.""" """Check whether a secret has been spent already or not."""
logger.trace(f"> POST /check: {payload}") logger.trace(f"> POST /check: {payload}")
proofs_state = await ledger.check_proofs_state([p.secret for p in payload.proofs]) proofs_state = await ledger.check_proofs_state([p.Y for p in payload.proofs])
spendableList: List[bool] = [] spendableList: List[bool] = []
pendingList: List[bool] = [] pendingList: List[bool] = []
for proof_state in proofs_state: for proof_state in proofs_state:

View File

@@ -51,10 +51,7 @@ class LedgerVerification(LedgerSpendingConditions, SupportsKeysets, SupportsDb):
""" """
# Verify inputs # Verify inputs
# Verify proofs are spendable # Verify proofs are spendable
if ( if not len(await self._get_proofs_spent([p.Y for p in proofs])) == 0:
not len(await self._get_proofs_spent_idx_secret([p.secret for p in proofs]))
== 0
):
raise TokenAlreadySpentError() raise TokenAlreadySpentError()
# Verify amounts of inputs # Verify amounts of inputs
if not all([self._verify_amount(p.amount) for p in proofs]): if not all([self._verify_amount(p.amount) for p in proofs]):
@@ -87,11 +84,13 @@ class LedgerVerification(LedgerSpendingConditions, SupportsKeysets, SupportsDb):
# Verify that input keyset units are the same as output keyset unit # Verify that input keyset units are the same as output keyset unit
# We have previously verified that all outputs have the same keyset id in `_verify_outputs` # We have previously verified that all outputs have the same keyset id in `_verify_outputs`
assert outputs[0].id, "output id not set" assert outputs[0].id, "output id not set"
if not all([ if not all(
self.keysets[p.id].unit == self.keysets[outputs[0].id].unit [
for p in proofs self.keysets[p.id].unit == self.keysets[outputs[0].id].unit
if p.id for p in proofs
]): if p.id
]
):
raise TransactionError("input and output keysets have different units.") raise TransactionError("input and output keysets have different units.")
# Verify output spending conditions # Verify output spending conditions
@@ -143,39 +142,34 @@ class LedgerVerification(LedgerSpendingConditions, SupportsKeysets, SupportsDb):
result.append(False if promise is None else True) result.append(False if promise is None else True)
return result return result
async def _get_proofs_pending_idx_secret( async def _get_proofs_pending(self, Ys: List[str]) -> Dict[str, Proof]:
self, secrets: List[str] """Returns a dictionary of only those proofs that are pending.
) -> Dict[str, Proof]: The key is the Y=h2c(secret) and the value is the proof.
"""Returns only those proofs that are pending.""" """
all_proofs_pending = await self.crud.get_proofs_pending( proofs_pending = await self.crud.get_proofs_pending(Ys=Ys, db=self.db)
proofs=[Proof(secret=s) for s in secrets], db=self.db proofs_pending_dict = {p.Y: p for p in proofs_pending}
)
proofs_pending = list(filter(lambda p: p.secret in secrets, all_proofs_pending))
proofs_pending_dict = {p.secret: p for p in proofs_pending}
return proofs_pending_dict return proofs_pending_dict
async def _get_proofs_spent_idx_secret( async def _get_proofs_spent(self, Ys: List[str]) -> Dict[str, Proof]:
self, secrets: List[str] """Returns a dictionary of all proofs that are spent.
) -> Dict[str, Proof]: The key is the Y=h2c(secret) and the value is the proof.
"""Returns all proofs that are spent.""" """
proofs = [Proof(secret=s) for s in secrets] proofs_spent_dict: Dict[str, Proof] = {}
proofs_spent: List[Proof] = []
if settings.mint_cache_secrets: if settings.mint_cache_secrets:
# check used secrets in memory # check used secrets in memory
for proof in proofs: for Y in Ys:
spent_proof = self.spent_proofs.get(proof.Y) spent_proof = self.spent_proofs.get(Y)
if spent_proof: if spent_proof:
proofs_spent.append(spent_proof) proofs_spent_dict[Y] = spent_proof
else: else:
# check used secrets in database # check used secrets in database
async with self.db.connect() as conn: async with self.db.connect() as conn:
for proof in proofs: for Y in Ys:
spent_proof = await self.crud.get_proof_used( spent_proof = await self.crud.get_proof_used(
db=self.db, Y=proof.Y, conn=conn db=self.db, Y=Y, conn=conn
) )
if spent_proof: if spent_proof:
proofs_spent.append(spent_proof) proofs_spent_dict[Y] = spent_proof
proofs_spent_dict = {p.secret: p for p in proofs_spent}
return proofs_spent_dict return proofs_spent_dict
def _verify_secret_criteria(self, proof: Proof) -> Literal[True]: def _verify_secret_criteria(self, proof: Proof) -> Literal[True]:

View File

@@ -654,7 +654,7 @@ class LedgerAPI(LedgerAPIDeprecated, object):
""" """
Checks whether the secrets in proofs are already spent or not and returns a list of booleans. Checks whether the secrets in proofs are already spent or not and returns a list of booleans.
""" """
payload = PostCheckStateRequest(secrets=[p.secret for p in proofs]) payload = PostCheckStateRequest(Ys=[p.Y for p in proofs])
resp = await self.httpx.post( resp = await self.httpx.post(
join(self.url, "/v1/checkstate"), join(self.url, "/v1/checkstate"),
json=payload.dict(), json=payload.dict(),
@@ -667,11 +667,11 @@ class LedgerAPI(LedgerAPIDeprecated, object):
states: List[ProofState] = [] states: List[ProofState] = []
for spendable, pending, p in zip(ret.spendable, ret.pending, proofs): for spendable, pending, p in zip(ret.spendable, ret.pending, proofs):
if spendable and not pending: if spendable and not pending:
states.append(ProofState(secret=p.secret, state=SpentState.unspent)) states.append(ProofState(Y=p.Y, state=SpentState.unspent))
elif spendable and pending: elif spendable and pending:
states.append(ProofState(secret=p.secret, state=SpentState.pending)) states.append(ProofState(Y=p.Y, state=SpentState.pending))
else: else:
states.append(ProofState(secret=p.secret, state=SpentState.spent)) states.append(ProofState(Y=p.Y, state=SpentState.spent))
ret = PostCheckStateResponse(states=states) ret = PostCheckStateResponse(states=states)
return ret return ret
# END backwards compatibility < 0.15.0 # END backwards compatibility < 0.15.0

View File

@@ -54,7 +54,8 @@ async def test_api_keys(ledger: Ledger):
"id": keyset.id, "id": keyset.id,
"unit": keyset.unit.name, "unit": keyset.unit.name,
"keys": { "keys": {
str(k): v.serialize().hex() for k, v in keyset.public_keys.items() # type: ignore str(k): v.serialize().hex()
for k, v in keyset.public_keys.items() # type: ignore
}, },
} }
for keyset in ledger.keysets.values() for keyset in ledger.keysets.values()
@@ -378,7 +379,7 @@ async def test_melt_external(ledger: Ledger, wallet: Wallet):
reason="settings.debug_mint_only_deprecated is set", reason="settings.debug_mint_only_deprecated is set",
) )
async def test_api_check_state(ledger: Ledger): async def test_api_check_state(ledger: Ledger):
payload = PostCheckStateRequest(secrets=["asdasdasd", "asdasdasd1"]) payload = PostCheckStateRequest(Ys=["asdasdasd", "asdasdasd1"])
response = httpx.post( response = httpx.post(
f"{BASE_URL}/v1/checkstate", f"{BASE_URL}/v1/checkstate",
json=payload.dict(), json=payload.dict(),

View File

@@ -357,7 +357,5 @@ async def test_check_proof_state(wallet1: Wallet, ledger: Ledger):
keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10) keep_proofs, send_proofs = await wallet1.split_to_send(wallet1.proofs, 10)
proof_states = await ledger.check_proofs_state( proof_states = await ledger.check_proofs_state(Ys=[p.Y for p in send_proofs])
secrets=[p.secret for p in send_proofs]
)
assert all([p.state.value == "UNSPENT" for p in proof_states]) assert all([p.state.value == "UNSPENT" for p in proof_states])