wallet: load keys without keysets (#123)

* wallet: load keys without keysets

* fix cli
This commit is contained in:
calle
2023-03-05 02:51:20 +01:00
committed by GitHub
parent e696e19479
commit a7eef68c97
5 changed files with 89 additions and 38 deletions

View File

@@ -338,6 +338,8 @@ class Ledger:
# Public methods # Public methods
def get_keyset(self, keyset_id: Optional[str] = None): def get_keyset(self, keyset_id: Optional[str] = None):
if keyset_id and keyset_id not in self.keysets.keysets:
raise Exception("keyset does not exist")
keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset
assert keyset.public_keys, Exception("no public keys for this keyset") assert keyset.public_keys, Exception("no public keys for this keyset")
return {a: p.serialize().hex() for a, p in keyset.public_keys.items()} return {a: p.serialize().hex() for a, p in keyset.public_keys.items()}

View File

@@ -43,16 +43,19 @@ async def keys() -> KeysResponse:
name="Keyset public keys", name="Keyset public keys",
summary="Public keys of a specific keyset", summary="Public keys of a specific keyset",
) )
async def keyset_keys(idBase64Urlsafe: str) -> KeysResponse: async def keyset_keys(idBase64Urlsafe: str) -> Union[KeysResponse, CashuError]:
""" """
Get the public keys of the mint from a specific keyset id. Get the public keys of the mint from a specific keyset id.
The id is encoded in idBase64Urlsafe (by a wallet) and is converted back to The id is encoded in idBase64Urlsafe (by a wallet) and is converted back to
normal base64 before it can be processed (by the mint). normal base64 before it can be processed (by the mint).
""" """
try:
id = idBase64Urlsafe.replace("-", "+").replace("_", "/") id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
keyset = ledger.get_keyset(keyset_id=id) keyset = ledger.get_keyset(keyset_id=id)
keys = KeysResponse.parse_obj(keyset) keys = KeysResponse.parse_obj(keyset)
return keys return keys
except Exception as exc:
return CashuError(code=0, error=str(exc))
@router.get( @router.get(

View File

@@ -36,10 +36,10 @@ async def verify_mints(ctx: Context, token: TokenV2):
) )
# make sure that this mint supports this keyset # make sure that this mint supports this keyset
mint_keysets = await keyset_wallet._get_keyset_ids(mint.url) mint_keysets = await keyset_wallet._get_keyset_ids(mint.url)
assert keyset in mint_keysets["keysets"], "mint does not have this keyset." assert keyset in mint_keysets, "mint does not have this keyset."
# we validate the keyset id by fetching the keys from the mint and computing the id locally # we validate the keyset id by fetching the keys from the mint and computing the id locally
mint_keyset = await keyset_wallet._get_keyset(mint.url, keyset) mint_keyset = await keyset_wallet._get_keys_of_keyset(mint.url, keyset)
assert keyset == mint_keyset.id, Exception("keyset not valid.") assert keyset == mint_keyset.id, Exception("keyset not valid.")
# we check the db whether we know this mint already and ask the user if not # we check the db whether we know this mint already and ask the user if not

View File

@@ -91,8 +91,9 @@ def async_set_requests(func):
class LedgerAPI: class LedgerAPI:
keys: Dict[int, PublicKey] keys: WalletKeyset # holds current keys of mint
keyset: str keyset_id: str # holds id of current keyset
public_keys: Dict[int, PublicKey] # holds public keys of
tor: TorProxy tor: TorProxy
db: Database db: Database
s: requests.Session s: requests.Session
@@ -113,7 +114,7 @@ class LedgerAPI:
proofs: List[Proof] = [] proofs: List[Proof] = []
for promise, secret, r in zip(promises, secrets, rs): for promise, secret, r in zip(promises, secrets, rs):
C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) C_ = PublicKey(bytes.fromhex(promise.C_), raw=True)
C = b_dhke.step3_alice(C_, r, self.keys[promise.amount]) C = b_dhke.step3_alice(C_, r, self.public_keys[promise.amount])
proof = Proof( proof = Proof(
id=self.keyset_id, id=self.keyset_id,
amount=promise.amount, amount=promise.amount,
@@ -133,48 +134,60 @@ class LedgerAPI:
"""Returns base64 encoded random string.""" """Returns base64 encoded random string."""
return scrts.token_urlsafe(randombits // 8) return scrts.token_urlsafe(randombits // 8)
async def _load_mint(self, keyset_id: str = ""): async def _load_mint_keys(self, keyset_id: str = ""):
"""
Loads the public keys of the mint. Either gets the keys for the specified
`keyset_id` or loads the most recent one from the mint.
Gets and the active keyset ids of the mint and stores in `self.keysets`.
"""
assert len( assert len(
self.url self.url
), "Ledger not initialized correctly: mint URL not specified yet. " ), "Ledger not initialized correctly: mint URL not specified yet. "
if keyset_id: if keyset_id:
# get requested keyset # get requested keyset
keyset = await self._get_keyset(self.url, keyset_id) keyset = await self._get_keys_of_keyset(self.url, keyset_id)
else: else:
# get current keyset # get current keyset
keyset = await self._get_keys(self.url) keyset = await self._get_keys(self.url)
# store current keyset
assert keyset.public_keys assert keyset.public_keys
assert keyset.id assert keyset.id
assert len(keyset.public_keys) > 0, "did not receive keys from mint." assert len(keyset.public_keys) > 0, "did not receive keys from mint."
# check if current keyset is in db # check if current keyset is in db
keyset_local: Optional[WalletKeyset] = await get_keyset(keyset.id, db=self.db) keyset_local: Optional[WalletKeyset] = await get_keyset(keyset.id, db=self.db)
# if not, store it
if keyset_local is None: if keyset_local is None:
await store_keyset(keyset=keyset, db=self.db) await store_keyset(keyset=keyset, db=self.db)
self.keys = keyset
logger.debug(f"Current mint keyset: {self.keys.id}")
return self.keys
async def _load_mint_keysets(self):
# get all active keysets of this mint # get all active keysets of this mint
mint_keysets = [] mint_keysets = []
try: try:
keysets_resp = await self._get_keyset_ids(self.url) mint_keysets = await self._get_keyset_ids(self.url)
mint_keysets = keysets_resp["keysets"]
# store active keysets
except: except:
assert self.keys.id, "could not get keysets from mint, and do not have keys"
pass pass
self.keysets = mint_keysets if len(mint_keysets) else [keyset.id] self.keysets = mint_keysets or [self.keys.id]
logger.debug(f"Mint keysets: {self.keysets}") logger.debug(f"Mint keysets: {self.keysets}")
logger.debug(f"Current mint keyset: {keyset.id}") return self.keysets
self.keys = keyset.public_keys async def _load_mint(self, keyset_id: str = ""):
self.keyset_id = keyset.id """
Loads the public keys of the mint. Either gets the keys for the specified
`keyset_id` or gets the keys of the active keyset from the mint.
Gets the active keyset ids of the mint and stores in `self.keysets`.
"""
await self._load_mint_keys(keyset_id)
await self._load_mint_keysets()
if keyset_id:
assert keyset_id in self.keysets, f"keyset {keyset_id} not active on mint"
assert self.keys.public_keys
self.public_keys = self.keys.public_keys
assert self.keys.id
self.keyset_id = self.keys.id
@staticmethod @staticmethod
def _construct_outputs(amounts: List[int], secrets: List[str]): def _construct_outputs(amounts: List[int], secrets: List[str]):
@@ -211,6 +224,14 @@ class LedgerAPI:
@async_set_requests @async_set_requests
async def _get_keys(self, url: str): async def _get_keys(self, url: str):
"""API that gets the current keys of the mint
Args:
url (str): Mint URL
Returns:
WalletKeyset: Current mint keyset
"""
resp = self.s.get( resp = self.s.get(
url + "/keys", url + "/keys",
) )
@@ -225,9 +246,16 @@ class LedgerAPI:
return keyset return keyset
@async_set_requests @async_set_requests
async def _get_keyset(self, url: str, keyset_id: str): async def _get_keys_of_keyset(self, url: str, keyset_id: str):
""" """API that gets the keys of a specific keyset from the mint.
keyset_id is base64, needs to be urlsafe-encoded.
Args:
url (str): Mint URL
keyset_id (str): base64 keyset ID, needs to be urlsafe-encoded before sending to mint (done in this method)
Returns:
WalletKeyset: Keyset with ID keyset_id
""" """
keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_") keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_")
resp = self.s.get( resp = self.s.get(
@@ -235,6 +263,7 @@ class LedgerAPI:
) )
resp.raise_for_status() resp.raise_for_status()
keys = resp.json() keys = resp.json()
self.raise_on_error(keys)
assert len(keys), Exception("did not receive any keys") assert len(keys), Exception("did not receive any keys")
keyset_keys = { keyset_keys = {
int(amt): PublicKey(bytes.fromhex(val), raw=True) int(amt): PublicKey(bytes.fromhex(val), raw=True)
@@ -245,6 +274,14 @@ class LedgerAPI:
@async_set_requests @async_set_requests
async def _get_keyset_ids(self, url: str): async def _get_keyset_ids(self, url: str):
"""API that gets a list of all active keysets of the mint.
Args:
url (str): Mint URL
Returns:
KeysetsResponse (List[str]): List of all active keyset IDs of the mint
"""
resp = self.s.get( resp = self.s.get(
url + "/keysets", url + "/keysets",
) )
@@ -252,7 +289,7 @@ class LedgerAPI:
keysets_dict = resp.json() keysets_dict = resp.json()
keysets = KeysetsResponse.parse_obj(keysets_dict) keysets = KeysetsResponse.parse_obj(keysets_dict)
assert len(keysets.keysets), Exception("did not receive any keysets") assert len(keysets.keysets), Exception("did not receive any keysets")
return keysets.dict() return keysets.keysets
@async_set_requests @async_set_requests
async def request_mint(self, amount): async def request_mint(self, amount):

View File

@@ -50,7 +50,8 @@ async def wallet2(mint):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_keys(wallet1: Wallet): async def test_get_keys(wallet1: Wallet):
assert len(wallet1.keys) == MAX_ORDER assert wallet1.keys.public_keys
assert len(wallet1.keys.public_keys) == MAX_ORDER
keyset = await wallet1._get_keys(wallet1.url) keyset = await wallet1._get_keys(wallet1.url)
assert keyset.id is not None assert keyset.id is not None
assert type(keyset.id) == str assert type(keyset.id) == str
@@ -59,24 +60,32 @@ async def test_get_keys(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_keyset(wallet1: Wallet): async def test_get_keyset(wallet1: Wallet):
assert len(wallet1.keys) == MAX_ORDER assert wallet1.keys.public_keys
# ket's get the keys first so we can get a keyset ID that we use later assert len(wallet1.keys.public_keys) == MAX_ORDER
# let's get the keys first so we can get a keyset ID that we use later
keys1 = await wallet1._get_keys(wallet1.url) keys1 = await wallet1._get_keys(wallet1.url)
# gets the keys of a specific keyset # gets the keys of a specific keyset
assert keys1.id is not None assert keys1.id is not None
assert keys1.public_keys is not None assert keys1.public_keys is not None
keys2 = await wallet1._get_keyset(wallet1.url, keys1.id) keys2 = await wallet1._get_keys_of_keyset(wallet1.url, keys1.id)
assert keys2.public_keys is not None assert keys2.public_keys is not None
assert len(keys1.public_keys) == len(keys2.public_keys) assert len(keys1.public_keys) == len(keys2.public_keys)
@pytest.mark.asyncio
async def test_get_nonexistent_keyset(wallet1: Wallet):
await assert_err(
wallet1._get_keys_of_keyset(wallet1.url, "nonexistent"),
"Mint Error: keyset does not exist",
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_keyset_ids(wallet1: Wallet): async def test_get_keyset_ids(wallet1: Wallet):
keyset = await wallet1._get_keyset_ids(wallet1.url) keyset = await wallet1._get_keyset_ids(wallet1.url)
assert type(keyset) == dict assert type(keyset) == list
assert type(keyset["keysets"]) == list assert len(keyset) > 0
assert len(keyset["keysets"]) > 0 assert keyset[-1] == wallet1.keyset_id
assert keyset["keysets"][-1] == wallet1.keyset_id
@pytest.mark.asyncio @pytest.mark.asyncio