-
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathiscc_id.py
More file actions
304 lines (242 loc) · 11 KB
/
Copy pathiscc_id.py
File metadata and controls
304 lines (242 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# -*- coding: utf-8 -*-
"""*A globally unique, owned, and short identifier for digital assets.*
The **ISCC-ID** is a 64-bit identifier constructed from a timestamp and a HUB-ID:
- First 52 bits: UTC time in microseconds since UNIX epoch (1970-01-01T00:00:00Z)
- Last 12 bits: ID of the timestamping HUB (0-4095)
With this structure:
- A single HUB can issue up to 1 million timestamps per second until the year 2112
- The system supports up to 4096 timestamp HUBs (IDs 0-4095)
- Timestamps are globally unique and support total ordering in both integer and base32hex forms
- The theoretical maximum throughput is ~4 billion unique timestamps per second
ISCC-IDs are issued and digitally signed by authoritative ISCC-HUB servers in a
federated system. A valid ISCC-ID is guaranteed to be bound to an owner represented by a
cryptographic public key. The rules by which ISCC-IDs can be verified and resolved are defined
by the `ISCC Discovery Protocol` (IDP).
The module also contains legacy support for the older v0 ISCC-ID format that was based on
blockchain wallet addresses and similarity-hashes of ISCC-CODE units.
"""
import time
from hashlib import sha256
from typing import Optional
import uvarint
import iscc_core as ic
__all__ = [
"gen_iscc_id",
"gen_iscc_id_v0",
"gen_iscc_id_v1",
"iscc_id_incr",
"iscc_id_incr_v0",
"alg_simhash_from_iscc_id",
]
def gen_iscc_id(timestamp=None, hub_id=0, realm_id=0):
# type: (Optional[int], int, int) -> dict
"""
Generate ISCC-ID from microsecond `timestamp` with the latest standard algorithm.
:param int timestamp: Microseconds since 1970-01-01T00:00:00Z (must be < 2^52)
:param int hub_id: HUB-ID that issued the ISCC-ID (0-4095)
:param int realm_id: Realm ID for the ISCC-ID (0 for testnet, 1 for mainnet, default: 0)
:return: Dictionary with the ISCC-ID under the key 'iscc'
:rtype: dict
:raises ValueError: If an input is invalid
"""
return gen_iscc_id_v1(timestamp, hub_id, realm_id)
####################################################################################################
# ISCC-IDv1 - Timestamp/HUB-ID based ISCC-ID #
####################################################################################################
def gen_iscc_id_v1(timestamp=None, hub_id=0, realm_id=0):
# type: (Optional[int], int, int) -> dict
"""
Generate an ISCC-ID from a timestamp and a HUB-ID with algorithm v1.
If no arguments are provided, a new ISCC-ID is generated with the current system time and both
hub_id and realm_id set to 0 (testnet).
The ISCC-IDv1 is a 64-bit identifier constructed from a timestamp and a HUB-ID:
- First 52 bits: UTC time in microseconds since UNIX epoch (1970-01-01T00:00:00Z)
- Last 12 bits: ID of the timestamping HUB (0-4095)
With this structure:
- A single HUB can issue up to 1 million timestamps per second until the year 2112
- The system supports up to 4096 timestamp HUBs (IDs 0-4095)
- Timestamps are globally unique and support total ordering in both integer and base32hex forms
- The theoretical maximum system throughput is ~4 billion unique timestamps per second
If the ID space becomes crowded, it can be extended by introducing additional REALMS via
ISCC-HEADER SUBTYPEs.
## Issuing ISCC-IDs
ISCC-IDv1s are issued and digitally signed by authoritative ISCC-HUB servers in a
federated system. A valid ISCC-IDv1 is guaranteed to be bound to an owner represented by a
cryptographic public key. The rules by which ISCC-IDv1 can be verified and resolved are defined
by the `ISCC Discovery Protocol` (IDP).
## Timestamp Requirements
Timestamp issuing requires:
- A time source with at least microsecond precision
- Strictly monotonic (always increasing) integer timestamps
- Measures to prevent front-running of actual time
## Realm ID Reservations
Realm-ID `0` is reserved for testnet purposes. An ISCC-IDv1 with Realm-ID 0:
- Is intended for testing and development
- Should not be used in production systems
- May not guarantee global uniqueness
Realm-ID `1` is the first operational mainnet Realm-ID for production use.
## Technical Format
The ISCC-IDv1 has the following format:
- Scheme Prefix: `ISCC:`
- Base32-Encoded concatenation of:
- 16-bit header:
- MAINTYPE = "0110" (ISCC-ID)
- SUBTYPE = "0000" (REALM, configurable via realm_id)
- VERSION = "0001" (V1)
- LENGTH = "0000" (64-bit)
- 52-bit timestamp: Microseconds since 1970-01-01T00:00:00Z
- 12-bit HUB-ID: The HUB ID (0-4095)
:param int timestamp: Microseconds since 1970-01-01T00:00:00Z (must be < 2^52)
:param int hub_id: HUB-ID that issued the ISCC-ID (0-4095)
:param int realm_id: Realm ID for the ISCC-ID (0 for testnet, 1 for mainnet, default: 0)
:return: Dictionary with the ISCC-ID under the key 'iscc'
:rtype: dict
:raises ValueError: If an input is invalid
"""
if timestamp is None:
timestamp = time.time_ns() // 1000
if timestamp >= 2**52: # Ensure timestamp fits in 52 bits
raise ValueError("Timestamp overflow")
if hub_id >= 2**12: # Ensure HUB-ID fits in 12 bits
raise ValueError("HUB-ID overflow")
if realm_id not in (0, 1): # Currently support REALM 0 (test) and REALM 1 (operational)
raise ValueError("Realm-ID must be 0 (test) or 1 (operational)")
# Shift timestamp left by 12 bits and combine with HUB ID
body = (timestamp << 12) | hub_id
# Pack the 64-bit body into 8 bytes
digest = body.to_bytes(8, byteorder="big")
iscc_id = ic.encode_component(
mtype=ic.MT.ID,
stype=realm_id,
version=ic.VS.V1,
bit_length=64,
digest=digest,
)
iscc = "ISCC:" + iscc_id
return dict(iscc=iscc)
####################################################################################################
# ISCC-IDv0 - Legacy experimental ISCC-IDv0 kept for backward compatibility #
####################################################################################################
def gen_iscc_id_v0(iscc_code, chain_id, wallet, uc=0):
# type: (str, int, str, Optional[int]) -> dict
"""
Generate an ISCC-ID from an ISCC-CODE with uniqueness counter 'uc' with
algorithm v0.
:param str iscc_code: The ISCC-CODE from which to mint the ISCC-ID.
:param int chain_id: Chain-ID of blockchain from which the ISCC-ID is minted.
:param str wallet: The wallet address that signes the ISCC declaration
:param int uc: Uniqueness counter of ISCC-ID.
:return: ISCC object with an ISCC-ID
:rtype: dict
"""
iscc_id_digest = soft_hash_iscc_id_v0(iscc_code, wallet, uc)
iscc_id_len = len(iscc_id_digest) * 8
iscc_id = ic.encode_component(
mtype=ic.MT.ID,
stype=chain_id,
version=ic.VS.V0,
bit_length=iscc_id_len,
digest=iscc_id_digest,
)
iscc = "ISCC:" + iscc_id
return dict(iscc=iscc)
def soft_hash_iscc_id_v0(iscc_code, wallet, uc=0):
# type: (str, str, int) -> bytes
"""
Calculate ISCC-ID hash digest from ISCC-CODE with algorithm v0.
Accepts an ISCC-CODE or any sequence of ISCC-UNITs.
:param str iscc_code: ISCC-CODE
:param str wallet: The wallet address that signes the ISCC declaration
:param int uc: Uniqueness counter for ISCC-ID.
:return: Digest for ISCC-ID without header but including uniqueness counter.
:rtype: bytes
"""
components = ic.iscc_decompose(iscc_code)
decoded = [ic.decode_base32(c) for c in components]
unpacked = [ic.decode_header(d) for d in decoded]
digests = []
if len(unpacked) == 1 and unpacked[0][0] == ic.MT.INSTANCE:
# Special case if iscc_code is a singular Instance-Code
digests.append(decoded[0][:1] + unpacked[0][-1][:7])
else:
for dec, unp in zip(decoded, unpacked):
mt = unp[0]
if mt == ic.MT.INSTANCE:
continue
if mt == ic.MT.ID:
raise ValueError("Cannot create ISCC-ID from ISCC-ID")
# first byte of header + first 7 bytes of body
digests.append(dec[:1] + unp[-1][:7])
iscc_id_digest = ic.alg_simhash(digests)
# XOR with sha2-256 of wallet
wallet_hash_digest = sha256(wallet.encode("ascii")).digest()[:8]
iscc_id_xor_digest = bytes(a ^ b for (a, b) in zip(iscc_id_digest, wallet_hash_digest))
if uc:
iscc_id_xor_digest += uvarint.encode(uc)
return iscc_id_xor_digest
def iscc_id_incr(iscc_id):
# type: (str) -> str
"""
Increment uniqueness counter of an ISCC-ID.
Note: Only ISCC-IDv0 supports uniqueness counters. ISCC-IDv1 uses timestamps
and will raise an error if passed to this function.
:param str iscc_id: Base32-encoded ISCC-ID.
:return: Base32-encoded ISCC-ID with counter incremented by one.
:rtype: str
:raises ValueError: If the ISCC-ID is v1 (which doesn't support counters).
"""
# Check version to provide clear error for v1
clean = ic.iscc_clean(iscc_id)
code_digest = ic.decode_base32(clean)
mt, st, vs, ln, data = ic.decode_header(code_digest)
if mt != ic.MT.ID:
raise ValueError(f"MainType {mt} is not ISCC-ID")
if vs == ic.VS.V1:
raise ValueError("ISCC-IDv1 does not support uniqueness counters (uses timestamps instead)")
elif vs == ic.VS.V0:
return iscc_id_incr_v0(iscc_id)
else:
raise ValueError(f"Unsupported ISCC-ID version {vs}")
def iscc_id_incr_v0(iscc_id):
# type: (str) -> str
"""
Increment uniqueness counter of an ISCC-ID with algorithm v0.
:param str iscc_id: Base32-encoded ISCC-ID.
:return: Base32-encoded ISCC-ID with counter incremented by one (without "ISCC:" prefix).
:rtype: str
"""
clean = ic.iscc_clean(iscc_id)
code_digest = ic.decode_base32(clean)
mt, st, vs, ln, data = ic.decode_header(code_digest)
if mt != ic.MT.ID:
raise ValueError(f"MainType {mt} is not ISCC-ID")
if vs != ic.VS.V0:
raise ValueError(f"Version {vs} is not v0")
if len(data) == 8:
data += uvarint.encode(1)
else:
counter = uvarint.decode(data[8:])
suffix = uvarint.encode(counter.integer + 1)
data = data[:8] + suffix
iscc_id_len = len(data) * 8
iscc_id = ic.encode_component(
mtype=mt,
stype=st,
version=vs,
bit_length=iscc_id_len,
digest=data,
)
return iscc_id
def alg_simhash_from_iscc_id(iscc_id, wallet):
# type: (str, str) -> str
"""
Extract similarity preserving hex-encoded hash digest from ISCC-ID
We need to un-xor the ISCC-ID hash digest with the wallet address hash to obtain the similarity
preserving bytestring.
"""
wallet_hash_digest = sha256(wallet.encode("ascii")).digest()[:8]
cleaned = ic.iscc_clean(iscc_id)
iscc_tuple = ic.iscc_decode(cleaned)
iscc_id_xor_digest = iscc_tuple[4][:8]
iscc_id_digest = bytes(a ^ b for (a, b) in zip(iscc_id_xor_digest, wallet_hash_digest))
return iscc_id_digest.hex()