mandatory
키셋은 민트 Bob이 생성하여 사용자와 공유하는 공개키들의 집합이다.
이는 민트가 지원하는 각 금액 단위(예: 1, 2, 4, 8, ...)에 각각 대응하는 공개키들의 집합을 의미한다.
각 키셋은 id, 통화 단위(unit), 활성 상태(active), 그리고 ecash를 사용할 때 적용되는 수수료(input_fee_ppk)를 포함한다.
하나의 민트는 동시에 여러 개의 키셋을 가질 수 있다. 예를 들어, 각 통화 단위(unit)별로 하나의 키셋을 둘 수 있다.
지갑은 여러 키셋을 지원해야 하며, 각 키셋의 active 및 input_fee_ppk 속성을 반드시 준수해야 한다.
키셋 id는 특정 키셋을 식별하기 위한 식별자다.
이 ID는 민트의 공개키 집합을 알고 있는 누구나 도출할 수 있다.
지갑은 주어진 키셋의 ID를 직접 계산하여 민트가 올바른 키셋 ID를 제공하고 있는지 검증할 수 있다.
각 Proof에는 키셋 id가 포함되어 있어, 해당 증명이 어떤 민트와 키셋에서 생성된 것인지 식별할 수 있다.
BlindedMessages 및 BlindSignatures에도 id 필드가 포함된다. (참조: NUT-00)
민트는 동시에 여러 개의 키셋을 가질 수 있지만, 최소한 하나 이상의 active 키셋을 반드시 가져야 한다. (NUT-01)
active 속성은 해당 키셋에서 새로운 ecash를 생성할 수 있는지를 결정한다.
active=false인 비활성 키셋의 Proof는 여전히 입력으로 사용할 수 있지만, 새로운 출력(BlindedMessages, BlindSignatures)은 활성 키셋에서만 생성해야 한다.
키셋을 교체하기 위해 민트는 새로운 활성 키셋을 만들고 이전 키셋을 비활성화할 수 있다.
이전 키셋의 active 플래그를 false로 설정하면, 해당 키셋으로 새로운 ecash를 발행할 수 없으며, 지갑이 순차적으로 ecash를 교환함에 따라 해당 키셋의 통화량은 자연스럽게 순환에서 제거된다.
지갑은 비활성 키셋의 Proof를 우선적으로 교환(swap) 하여 빠르게 제거하는 것을 권장한다. (NUT-03)
비활성 키셋이 감지되면, 지갑은 즉시 전체 잔액을 활성 키셋으로 교환할 수도 있다.
거래 출력을 구성할 때 지갑은 활성 키셋만 선택해야 한다. (NUT-00)
키셋에는 input_fee_ppk 속성이 있으며, 이는 해당 키셋의 Proof를 거래 입력으로 사용할 때 부과되는 수수료를 의미한다.
단위는 천분율(parts per thousand, ppk)이며, 키셋의 unit 단위를 기준으로 한다.
거래의 총 수수료는 각 입력의 수수료 합계를 계산하고, 다음 큰 정수로 올림하여 산출된다.
예를 들어, 단위가 sat이고 input_fee_ppk=100인 키셋에서 3개의 입력(Proof)을 사용하는 거래를 구성한다고 하자.
100 ppk는 입력당 0.1 sat의 수수료를 의미한다.
총 수수료는 3개 입력 × 100 ppk = 300 ppk이며, 올림 후 1 sat이 부과된다.
즉, 입력이 110개일 때는 1 sat, 1120개일 때는 2 sat이 된다.
ecash 입력을 사용하는 거래(/v1/swap 또는 /v1/melt)를 구성할 때,
지갑은 입력에 수수료를 추가하거나, 혹은 출력에서 수수료를 차감해야 한다.
민트는 다음 식을 검사한다:
sum(inputs) - fees == sum(outputs)여기서 sum(inputs)와 sum(outputs)는 각각 입력과 출력의 총합을 의미한다.
fees는 각 입력의 키셋에 따른 input_fee_ppk를 합산 후 천분율 기준으로 올림하여 계산한다.
def fees(inputs: List[Proof]) -> int:
sum_fees = 0
for proof in inputs:
sum_fees += keysets[proof.id].input_fee_ppk
return (sum_fees + 999) // 1000// 연산자는 정수 나눗셈(floor division)을 의미하며,
(sum_fees + 999) // 1000은 올림(ceil) 효과를 정수 연산으로 구현한 것이다.
부동소수점 연산(ceil(sum_fees / 1000))은 비결정적이므로 권장되지 않는다.
하나의 거래가 여러 키셋에서 온 입력들을 사용할 수 있으므로,
수수료 합계는 각 키셋 ID별로 개별적으로 계산된다.
키셋 ID에는 버전 바이트(두 자리 16진수)가 존재한다. 현재 사용 중인 버전은 00이다.
민트와 지갑은 동일한 방식으로 키셋 ID를 계산할 수 있다.
키셋 ID는 소문자 16진 문자열이다.
도출 과정은 다음과 같다:
1 - 금액 기준으로 공개키들을 오름차순 정렬
2 - 모든 공개키를 바이트 배열로 이어붙임
3 - SHA256 해시 계산
4 - 해시의 16진 표현 중 앞 14자 추출
5 - 버전 바이트를 앞에 붙임
Python 예시:
def derive_keyset_id(keys: Dict[int, PublicKey]) -> str:
sorted_keys = dict(sorted(keys.items()))
pubkeys_concat = b"".join([p.serialize() for p in sorted_keys.values()])
return "00" + hashlib.sha256(pubkeys_concat).hexdigest()[:14]지갑은 GET /v1/keysets 엔드포인트를 호출해 민트의 모든 키셋을 요청할 수 있다.
Alice의 요청:
GET https://mint.host:3338/v1/keysetscurl 예시:
curl -X GET https://mint.host:3338/v1/keysetsBob의 응답 (GetKeysetsResponse):
{
"keysets": [
{
"id": <hex_str>,
"unit": <str>,
"active": <bool>,
"input_fee_ppk": <int|null>,
},
...
]
}id: 키셋 IDunit: 단위 문자열 (예:"sat")active: 새로운 ecash 발행 가능 여부input_fee_ppk: 입력 1개당 천분율 수수료 (없으면 0으로 간주)
{
"keysets": [
{
"id": "009a1f293253e41e",
"unit": "sat",
"active": true,
"input_fee_ppk": 100
},
{
"id": "0042ade98b2a370a",
"unit": "sat",
"active": false,
"input_fee_ppk": 100
},
{
"id": "00c074b96c7e2b0e",
"unit": "usd",
"active": true,
"input_fee_ppk": 100
}
]
}지갑은 특정 키셋의 공개키를 얻기 위해
GET /v1/keys/{keyset_id} 엔드포인트를 호출할 수 있다.
Alice가 키셋 009a1f293253e41e의 키를 요청한다고 하자.
GET https://mint.host:3338/v1/keys/009a1f293253e41ecurl 예시:
curl -X GET https://mint.host:3338/v1/keys/009a1f293253e41eBob의 응답 (참조: NUT-01)
{
"keysets": [{
"id": "009a1f293253e41e",
"unit": "sat",
"keys": {
"1": "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104",
"2": "03b0f36d6d47ce14df8a7be9137712c42bcdd960b19dd02f1d4a9703b1f31d7513",
"4": "0366be6e026e42852498efb82014ca91e89da2e7a5bd3761bdad699fa2aec9fe09",
"8": "0253de5237f189606f29d8a690ea719f74d65f617bb1cb6fbea34f2bc4f930016d",
...
},
}, ...
]
}지갑은 시작 시 민트에 GET /v1/keysets를 요청하여 키셋 목록을 가져오고,
자신의 DB에 있는 토큰 중 해당 민트가 지원하는 키셋 ID만 로드할 수 있다.
이를 통해 민트가 새로운 키셋을 추가했는지, 혹은 기존 키셋의 active 상태를 변경했는지를 감지할 수 있다.
유용한 흐름은 다음과 같다:
- 민트의 키를 아직 가지고 있지 않다면
GET /v1/keys로 모든 키셋 키를 가져와 저장한다. GET /v1/keysets로 모든 키셋 목록을 요청한다.- 새로 추가된 키셋이 있으면
GET /v1/keys/{keyset_id}로 가져와 저장한다. - 키셋의
active플래그가 변경된 경우 DB에 반영하고 해당 상태에 맞게 사용한다.