synapse-filecoin-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. pynapse/__init__.py +6 -0
  2. pynapse/_version.py +1 -0
  3. pynapse/contracts/__init__.py +34 -0
  4. pynapse/contracts/abi_registry.py +11 -0
  5. pynapse/contracts/addresses.json +30 -0
  6. pynapse/contracts/erc20_abi.json +92 -0
  7. pynapse/contracts/errorsAbi.json +933 -0
  8. pynapse/contracts/filecoinPayV1Abi.json +2424 -0
  9. pynapse/contracts/filecoinWarmStorageServiceAbi.json +2363 -0
  10. pynapse/contracts/filecoinWarmStorageServiceStateViewAbi.json +651 -0
  11. pynapse/contracts/generated.py +35 -0
  12. pynapse/contracts/payments_abi.json +205 -0
  13. pynapse/contracts/pdpVerifierAbi.json +1266 -0
  14. pynapse/contracts/providerIdSetAbi.json +161 -0
  15. pynapse/contracts/serviceProviderRegistryAbi.json +1479 -0
  16. pynapse/contracts/sessionKeyRegistryAbi.json +147 -0
  17. pynapse/core/__init__.py +68 -0
  18. pynapse/core/abis.py +25 -0
  19. pynapse/core/chains.py +97 -0
  20. pynapse/core/constants.py +27 -0
  21. pynapse/core/errors.py +22 -0
  22. pynapse/core/piece.py +263 -0
  23. pynapse/core/rand.py +14 -0
  24. pynapse/core/typed_data.py +320 -0
  25. pynapse/core/utils.py +30 -0
  26. pynapse/evm/__init__.py +3 -0
  27. pynapse/evm/client.py +26 -0
  28. pynapse/filbeam/__init__.py +3 -0
  29. pynapse/filbeam/service.py +39 -0
  30. pynapse/payments/__init__.py +17 -0
  31. pynapse/payments/service.py +826 -0
  32. pynapse/pdp/__init__.py +21 -0
  33. pynapse/pdp/server.py +331 -0
  34. pynapse/pdp/types.py +38 -0
  35. pynapse/pdp/verifier.py +82 -0
  36. pynapse/retriever/__init__.py +12 -0
  37. pynapse/retriever/async_chain.py +227 -0
  38. pynapse/retriever/chain.py +209 -0
  39. pynapse/session/__init__.py +12 -0
  40. pynapse/session/key.py +30 -0
  41. pynapse/session/permissions.py +57 -0
  42. pynapse/session/registry.py +90 -0
  43. pynapse/sp_registry/__init__.py +11 -0
  44. pynapse/sp_registry/capabilities.py +25 -0
  45. pynapse/sp_registry/pdp_capabilities.py +102 -0
  46. pynapse/sp_registry/service.py +446 -0
  47. pynapse/sp_registry/types.py +52 -0
  48. pynapse/storage/__init__.py +57 -0
  49. pynapse/storage/async_context.py +682 -0
  50. pynapse/storage/async_manager.py +757 -0
  51. pynapse/storage/context.py +680 -0
  52. pynapse/storage/manager.py +758 -0
  53. pynapse/synapse.py +191 -0
  54. pynapse/utils/__init__.py +25 -0
  55. pynapse/utils/constants.py +25 -0
  56. pynapse/utils/errors.py +3 -0
  57. pynapse/utils/metadata.py +35 -0
  58. pynapse/utils/piece_url.py +16 -0
  59. pynapse/warm_storage/__init__.py +13 -0
  60. pynapse/warm_storage/service.py +513 -0
  61. synapse_filecoin_sdk-0.1.0.dist-info/METADATA +74 -0
  62. synapse_filecoin_sdk-0.1.0.dist-info/RECORD +64 -0
  63. synapse_filecoin_sdk-0.1.0.dist-info/WHEEL +4 -0
  64. synapse_filecoin_sdk-0.1.0.dist-info/licenses/LICENSE.md +228 -0
@@ -0,0 +1,147 @@
1
+ [
2
+ {
3
+ "type": "function",
4
+ "inputs": [
5
+ {
6
+ "name": "user",
7
+ "internalType": "address",
8
+ "type": "address"
9
+ },
10
+ {
11
+ "name": "signer",
12
+ "internalType": "address",
13
+ "type": "address"
14
+ },
15
+ {
16
+ "name": "permission",
17
+ "internalType": "bytes32",
18
+ "type": "bytes32"
19
+ }
20
+ ],
21
+ "name": "authorizationExpiry",
22
+ "outputs": [
23
+ {
24
+ "name": "",
25
+ "internalType": "uint256",
26
+ "type": "uint256"
27
+ }
28
+ ],
29
+ "stateMutability": "view"
30
+ },
31
+ {
32
+ "type": "function",
33
+ "inputs": [
34
+ {
35
+ "name": "signer",
36
+ "internalType": "address",
37
+ "type": "address"
38
+ },
39
+ {
40
+ "name": "expiry",
41
+ "internalType": "uint256",
42
+ "type": "uint256"
43
+ },
44
+ {
45
+ "name": "permissions",
46
+ "internalType": "bytes32[]",
47
+ "type": "bytes32[]"
48
+ },
49
+ {
50
+ "name": "origin",
51
+ "internalType": "string",
52
+ "type": "string"
53
+ }
54
+ ],
55
+ "name": "login",
56
+ "outputs": [],
57
+ "stateMutability": "nonpayable"
58
+ },
59
+ {
60
+ "type": "function",
61
+ "inputs": [
62
+ {
63
+ "name": "signer",
64
+ "internalType": "address payable",
65
+ "type": "address"
66
+ },
67
+ {
68
+ "name": "expiry",
69
+ "internalType": "uint256",
70
+ "type": "uint256"
71
+ },
72
+ {
73
+ "name": "permissions",
74
+ "internalType": "bytes32[]",
75
+ "type": "bytes32[]"
76
+ },
77
+ {
78
+ "name": "origin",
79
+ "internalType": "string",
80
+ "type": "string"
81
+ }
82
+ ],
83
+ "name": "loginAndFund",
84
+ "outputs": [],
85
+ "stateMutability": "payable"
86
+ },
87
+ {
88
+ "type": "function",
89
+ "inputs": [
90
+ {
91
+ "name": "signer",
92
+ "internalType": "address",
93
+ "type": "address"
94
+ },
95
+ {
96
+ "name": "permissions",
97
+ "internalType": "bytes32[]",
98
+ "type": "bytes32[]"
99
+ },
100
+ {
101
+ "name": "origin",
102
+ "internalType": "string",
103
+ "type": "string"
104
+ }
105
+ ],
106
+ "name": "revoke",
107
+ "outputs": [],
108
+ "stateMutability": "nonpayable"
109
+ },
110
+ {
111
+ "type": "event",
112
+ "anonymous": false,
113
+ "inputs": [
114
+ {
115
+ "name": "identity",
116
+ "internalType": "address",
117
+ "type": "address",
118
+ "indexed": true
119
+ },
120
+ {
121
+ "name": "signer",
122
+ "internalType": "address",
123
+ "type": "address",
124
+ "indexed": false
125
+ },
126
+ {
127
+ "name": "expiry",
128
+ "internalType": "uint256",
129
+ "type": "uint256",
130
+ "indexed": false
131
+ },
132
+ {
133
+ "name": "permissions",
134
+ "internalType": "bytes32[]",
135
+ "type": "bytes32[]",
136
+ "indexed": false
137
+ },
138
+ {
139
+ "name": "origin",
140
+ "internalType": "string",
141
+ "type": "string",
142
+ "indexed": false
143
+ }
144
+ ],
145
+ "name": "AuthorizationsUpdated"
146
+ }
147
+ ]
@@ -0,0 +1,68 @@
1
+ """Core primitives and contract utilities."""
2
+
3
+ from .chains import (
4
+ CALIBRATION,
5
+ MAINNET,
6
+ Chain,
7
+ ChainContracts,
8
+ as_chain,
9
+ )
10
+ from .abis import (
11
+ ADDRESSES,
12
+ ERRORS_ABI,
13
+ FILECOIN_PAY_V1_ABI,
14
+ FWSS_ABI,
15
+ FWSS_VIEW_ABI,
16
+ PDP_VERIFIER_ABI,
17
+ PROVIDER_ID_SET_ABI,
18
+ SERVICE_PROVIDER_REGISTRY_ABI,
19
+ SESSION_KEY_REGISTRY_ABI,
20
+ )
21
+ from .constants import LOCKUP_PERIOD, RETRY_CONSTANTS, SIZE_CONSTANTS, TIME_CONSTANTS
22
+ from .errors import SynapseError, create_error
23
+ from .piece import PieceCidInfo, calculate_piece_cid
24
+ from .rand import rand_index, rand_u256
25
+ from .typed_data import (
26
+ EIP712_TYPES,
27
+ get_storage_domain,
28
+ sign_add_pieces_extra_data,
29
+ sign_create_dataset,
30
+ sign_erc20_permit,
31
+ sign_schedule_piece_removals,
32
+ )
33
+ from .utils import format_units, parse_units
34
+
35
+ __all__ = [
36
+ "Chain",
37
+ "ChainContracts",
38
+ "MAINNET",
39
+ "CALIBRATION",
40
+ "as_chain",
41
+ "ADDRESSES",
42
+ "ERRORS_ABI",
43
+ "FILECOIN_PAY_V1_ABI",
44
+ "FWSS_ABI",
45
+ "FWSS_VIEW_ABI",
46
+ "PDP_VERIFIER_ABI",
47
+ "PROVIDER_ID_SET_ABI",
48
+ "SERVICE_PROVIDER_REGISTRY_ABI",
49
+ "SESSION_KEY_REGISTRY_ABI",
50
+ "SIZE_CONSTANTS",
51
+ "TIME_CONSTANTS",
52
+ "LOCKUP_PERIOD",
53
+ "RETRY_CONSTANTS",
54
+ "SynapseError",
55
+ "create_error",
56
+ "PieceCidInfo",
57
+ "calculate_piece_cid",
58
+ "rand_u256",
59
+ "rand_index",
60
+ "EIP712_TYPES",
61
+ "get_storage_domain",
62
+ "sign_create_dataset",
63
+ "sign_add_pieces_extra_data",
64
+ "sign_schedule_piece_removals",
65
+ "sign_erc20_permit",
66
+ "format_units",
67
+ "parse_units",
68
+ ]
pynapse/core/abis.py ADDED
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from pynapse.contracts import (
4
+ ADDRESSES,
5
+ ERRORS_ABI,
6
+ FILECOIN_PAY_V1_ABI,
7
+ FWSS_ABI,
8
+ FWSS_VIEW_ABI,
9
+ PDP_VERIFIER_ABI,
10
+ PROVIDER_ID_SET_ABI,
11
+ SERVICE_PROVIDER_REGISTRY_ABI,
12
+ SESSION_KEY_REGISTRY_ABI,
13
+ )
14
+
15
+ __all__ = [
16
+ "ADDRESSES",
17
+ "ERRORS_ABI",
18
+ "FILECOIN_PAY_V1_ABI",
19
+ "FWSS_ABI",
20
+ "FWSS_VIEW_ABI",
21
+ "PDP_VERIFIER_ABI",
22
+ "PROVIDER_ID_SET_ABI",
23
+ "SERVICE_PROVIDER_REGISTRY_ABI",
24
+ "SESSION_KEY_REGISTRY_ABI",
25
+ ]
pynapse/core/chains.py ADDED
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Dict, Optional
5
+
6
+ from pynapse.contracts.generated import ADDRESSES
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ChainContracts:
11
+ multicall3: str
12
+ usdfc: str
13
+ payments: str
14
+ warm_storage: str
15
+ warm_storage_state_view: str
16
+ sp_registry: str
17
+ session_key_registry: str
18
+ pdp_verifier: str
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Chain:
23
+ id: int
24
+ name: str
25
+ rpc_url: str
26
+ genesis_timestamp: int
27
+ contracts: ChainContracts
28
+ filbeam_domain: Optional[str] = None
29
+
30
+
31
+ NETWORK_MAINNET = "mainnet"
32
+ NETWORK_CALIBRATION = "calibration"
33
+
34
+ CHAIN_ID_MAINNET = 314
35
+ CHAIN_ID_CALIBRATION = 314159
36
+
37
+ RPC_URLS: Dict[str, str] = {
38
+ NETWORK_MAINNET: "https://api.node.glif.io/rpc/v1",
39
+ NETWORK_CALIBRATION: "https://api.calibration.node.glif.io/rpc/v1",
40
+ }
41
+
42
+ GENESIS_TIMESTAMPS: Dict[str, int] = {
43
+ NETWORK_MAINNET: 1598306400,
44
+ NETWORK_CALIBRATION: 1667326380,
45
+ }
46
+
47
+ CONTRACTS_BY_NETWORK: Dict[str, ChainContracts] = {
48
+ NETWORK_MAINNET: ChainContracts(
49
+ multicall3="0xcA11bde05977b3631167028862bE2a173976CA11",
50
+ usdfc="0x80B98d3aa09ffff255c3ba4A241111Ff1262F045",
51
+ payments=ADDRESSES["filecoinPayV1Address"]["314"],
52
+ warm_storage=ADDRESSES["filecoinWarmStorageServiceAddress"]["314"],
53
+ warm_storage_state_view=ADDRESSES["filecoinWarmStorageServiceStateViewAddress"]["314"],
54
+ sp_registry=ADDRESSES["serviceProviderRegistryAddress"]["314"],
55
+ session_key_registry=ADDRESSES["sessionKeyRegistryAddress"]["314"],
56
+ pdp_verifier=ADDRESSES["pdpVerifierAddress"]["314"],
57
+ ),
58
+ NETWORK_CALIBRATION: ChainContracts(
59
+ multicall3="0xcA11bde05977b3631167028862bE2a173976CA11",
60
+ usdfc="0xb3042734b608a1B16e9e86B374A3f3e389B4cDf0",
61
+ payments=ADDRESSES["filecoinPayV1Address"]["314159"],
62
+ warm_storage=ADDRESSES["filecoinWarmStorageServiceAddress"]["314159"],
63
+ warm_storage_state_view=ADDRESSES["filecoinWarmStorageServiceStateViewAddress"]["314159"],
64
+ sp_registry=ADDRESSES["serviceProviderRegistryAddress"]["314159"],
65
+ session_key_registry=ADDRESSES["sessionKeyRegistryAddress"]["314159"],
66
+ pdp_verifier=ADDRESSES["pdpVerifierAddress"]["314159"],
67
+ ),
68
+ }
69
+
70
+
71
+ MAINNET = Chain(
72
+ id=CHAIN_ID_MAINNET,
73
+ name="Filecoin Mainnet",
74
+ rpc_url=RPC_URLS[NETWORK_MAINNET],
75
+ genesis_timestamp=GENESIS_TIMESTAMPS[NETWORK_MAINNET],
76
+ contracts=CONTRACTS_BY_NETWORK[NETWORK_MAINNET],
77
+ filbeam_domain="filbeam.io",
78
+ )
79
+
80
+ CALIBRATION = Chain(
81
+ id=CHAIN_ID_CALIBRATION,
82
+ name="Filecoin Calibration",
83
+ rpc_url=RPC_URLS[NETWORK_CALIBRATION],
84
+ genesis_timestamp=GENESIS_TIMESTAMPS[NETWORK_CALIBRATION],
85
+ contracts=CONTRACTS_BY_NETWORK[NETWORK_CALIBRATION],
86
+ filbeam_domain=None,
87
+ )
88
+
89
+
90
+ def as_chain(chain: Chain | str | int) -> Chain:
91
+ if isinstance(chain, Chain):
92
+ return chain
93
+ if chain in (NETWORK_MAINNET, CHAIN_ID_MAINNET):
94
+ return MAINNET
95
+ if chain in (NETWORK_CALIBRATION, CHAIN_ID_CALIBRATION):
96
+ return CALIBRATION
97
+ raise ValueError(f"Unsupported chain: {chain}")
@@ -0,0 +1,27 @@
1
+ TIME_CONSTANTS = {
2
+ "EPOCH_DURATION": 30,
3
+ "EPOCHS_PER_DAY": 2880,
4
+ "EPOCHS_PER_MONTH": 86400,
5
+ "DAYS_PER_MONTH": 30,
6
+ "DEFAULT_LOCKUP_DAYS": 30,
7
+ }
8
+
9
+ SIZE_CONSTANTS = {
10
+ "KiB": 1024,
11
+ "MiB": 1 << 20,
12
+ "GiB": 1 << 30,
13
+ "TiB": 1 << 40,
14
+ "PiB": 1 << 50,
15
+ "MAX_UPLOAD_SIZE": 1_065_353_216,
16
+ "MIN_UPLOAD_SIZE": 127,
17
+ "DEFAULT_UPLOAD_BATCH_SIZE": 32,
18
+ }
19
+
20
+ LOCKUP_PERIOD = TIME_CONSTANTS["DEFAULT_LOCKUP_DAYS"] * TIME_CONSTANTS["EPOCHS_PER_DAY"]
21
+
22
+ RETRY_CONSTANTS = {
23
+ "FACTOR": 1,
24
+ "DELAY_TIME": 4000,
25
+ "RETRIES": None,
26
+ "MAX_RETRY_TIME": 1000 * 60 * 5,
27
+ }
pynapse/core/errors.py ADDED
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+
7
+ @dataclass
8
+ class SynapseError(Exception):
9
+ component: str
10
+ operation: str
11
+ message: str
12
+ cause: Optional[BaseException] = None
13
+
14
+ def __str__(self) -> str:
15
+ base = f"{self.component}.{self.operation}: {self.message}"
16
+ if self.cause is not None:
17
+ return f"{base} (cause: {self.cause})"
18
+ return base
19
+
20
+
21
+ def create_error(component: str, operation: str, message: str, cause: Optional[BaseException] = None) -> SynapseError:
22
+ return SynapseError(component=component, operation=operation, message=message, cause=cause)
pynapse/core/piece.py ADDED
@@ -0,0 +1,263 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import os
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import BinaryIO, Optional, Union
11
+
12
+ from .errors import create_error
13
+
14
+
15
+ DEFAULT_STREAM_COMMP_PATH = Path(
16
+ "/Users/anjor/repos/filecoin-project/go-fil-commp-hashhash/cmd/stream-commp/stream-commp"
17
+ )
18
+
19
+ # Multicodec constants
20
+ RAW_CODEC = 0x55 # raw codec for PieceCIDv2
21
+ FIL_COMMITMENT_UNSEALED = 0xf101 # fil-commitment-unsealed for PieceCIDv1
22
+ SHA2_256_TRUNC254_PADDED = 0x1012 # sha2-256-trunc254-padded for PieceCIDv1
23
+ FR32_SHA2_256_TRUNC254_PADBINTREE = 0x1011 # fr32-sha2-256-trunc254-padded-binary-tree for PieceCIDv2
24
+
25
+ # Node size in bytes (32 bytes for SHA256)
26
+ NODE_SIZE = 32
27
+
28
+
29
+ def _encode_varint(value: int) -> bytes:
30
+ """Encode an unsigned integer as a varint (unsigned LEB128)."""
31
+ result = bytearray()
32
+ while value >= 0x80:
33
+ result.append((value & 0x7f) | 0x80)
34
+ value >>= 7
35
+ result.append(value)
36
+ return bytes(result)
37
+
38
+
39
+ def _decode_multibase_base32(cid_str: str) -> bytes:
40
+ """Decode a base32lower multibase CID string to bytes."""
41
+ import base64
42
+ # Remove 'b' prefix (base32lower multibase prefix)
43
+ if cid_str.startswith('baga') or cid_str.startswith('bafk'):
44
+ # This is base32lower - need to decode
45
+ # Add padding if necessary
46
+ raw = cid_str[1:] # Remove 'b' prefix
47
+ # base32lower uses lowercase RFC 4648 alphabet
48
+ # Python's base64.b32decode expects uppercase
49
+ raw_upper = raw.upper()
50
+ # Add padding
51
+ padding = (8 - len(raw_upper) % 8) % 8
52
+ raw_padded = raw_upper + '=' * padding
53
+ return base64.b32decode(raw_padded)
54
+ raise ValueError(f"Unsupported CID format: {cid_str}")
55
+
56
+
57
+ def _encode_multibase_base32(data: bytes) -> str:
58
+ """Encode bytes to base32lower multibase CID string."""
59
+ import base64
60
+ encoded = base64.b32encode(data).decode('ascii').lower().rstrip('=')
61
+ return 'b' + encoded
62
+
63
+
64
+ def _extract_root_hash_from_pieceCIDv1(piece_cid_v1: str) -> bytes:
65
+ """Extract the 32-byte root hash from a PieceCIDv1 CID."""
66
+ cid_bytes = _decode_multibase_base32(piece_cid_v1)
67
+
68
+ # CIDv1 structure: version (varint) + codec (varint) + multihash
69
+ # version = 1
70
+ # codec = 0xf101 (fil-commitment-unsealed) = 2 bytes varint
71
+ # multihash = code (varint) + length (varint) + digest
72
+
73
+ idx = 0
74
+ # Skip version (1 = single byte varint)
75
+ idx += 1
76
+
77
+ # Skip codec (0xf101 = 2 bytes as varint: 0x81 0xe2 0x03)
78
+ while cid_bytes[idx] & 0x80:
79
+ idx += 1
80
+ idx += 1
81
+
82
+ # Skip multihash code (0x1012 = 2 bytes as varint)
83
+ while cid_bytes[idx] & 0x80:
84
+ idx += 1
85
+ idx += 1
86
+
87
+ # Skip multihash length (32 = single byte)
88
+ idx += 1
89
+
90
+ # The rest is the 32-byte digest
91
+ return cid_bytes[idx:idx + 32]
92
+
93
+
94
+ def _create_pieceCIDv2(root_hash: bytes, payload_size: int, padded_piece_size: int) -> str:
95
+ """Create a PieceCIDv2 CID from root hash and size information.
96
+
97
+ PieceCIDv2 uses:
98
+ - Raw codec (0x55)
99
+ - fr32-sha2-256-trunc254-padded-binary-tree multihash (0x1011)
100
+
101
+ The multihash digest format (per FRC-0069):
102
+ - varint: padding (padded_piece_size - payload_size after fr32 expansion)
103
+ - 1 byte: tree height (log2 of number of leaves)
104
+ - 32 bytes: root hash
105
+ """
106
+ # Calculate tree height
107
+ # For fr32, the padded size determines the number of leaves
108
+ # Each leaf is 32 bytes (NODE_SIZE)
109
+ num_leaves = padded_piece_size // NODE_SIZE
110
+ tree_height = int(math.log2(num_leaves)) if num_leaves > 0 else 0
111
+
112
+ # Calculate padding
113
+ # In fr32 encoding, padding = padded_piece_size - unpadded_piece_size
114
+ # But for PieceCIDv2, we need the payload padding which accounts for fr32 expansion
115
+ # unpadded_piece_size is payload_size * 128 / 127 (fr32 expansion), rounded up to power of 2
116
+ # padding = unpadded_piece_size - payload_size
117
+
118
+ # Actually, looking at the TypeScript SDK more carefully:
119
+ # The padding in PieceCIDv2 multihash is the number of zero bytes added to pad to a power of 2
120
+ # before fr32 encoding. This is: unpadded_piece_size - payload_size
121
+
122
+ # From the sizes:
123
+ # padded_piece_size = unpadded_piece_size * 128 / 127 (fr32 expansion)
124
+ # So: unpadded_piece_size = padded_piece_size * 127 / 128
125
+ unpadded_piece_size = (padded_piece_size * 127) // 128
126
+ padding = unpadded_piece_size - payload_size
127
+
128
+ # Build the multihash digest
129
+ digest = bytearray()
130
+ digest.extend(_encode_varint(padding))
131
+ digest.append(tree_height)
132
+ digest.extend(root_hash)
133
+
134
+ # Build the full multihash (code + length + digest)
135
+ multihash = bytearray()
136
+ multihash.extend(_encode_varint(FR32_SHA2_256_TRUNC254_PADBINTREE))
137
+ multihash.extend(_encode_varint(len(digest)))
138
+ multihash.extend(digest)
139
+
140
+ # Build CIDv1 (version + codec + multihash)
141
+ cid = bytearray()
142
+ cid.append(1) # CID version 1
143
+ cid.extend(_encode_varint(RAW_CODEC))
144
+ cid.extend(multihash)
145
+
146
+ return _encode_multibase_base32(bytes(cid))
147
+
148
+
149
+ def convert_to_pieceCIDv2(piece_cid_v1: str, payload_size: int, padded_piece_size: int) -> str:
150
+ """Convert a PieceCIDv1 (CommP) to PieceCIDv2 format.
151
+
152
+ PieceCIDv2 encodes the size information within the CID itself,
153
+ as per FRC-0069 specification.
154
+
155
+ Args:
156
+ piece_cid_v1: The PieceCIDv1 string (e.g., "baga6ea4seaq...")
157
+ payload_size: Original data size in bytes
158
+ padded_piece_size: Padded piece size in bytes (power of 2)
159
+
160
+ Returns:
161
+ PieceCIDv2 string (e.g., "bafkzcib...")
162
+ """
163
+ root_hash = _extract_root_hash_from_pieceCIDv1(piece_cid_v1)
164
+ return _create_pieceCIDv2(root_hash, payload_size, padded_piece_size)
165
+
166
+
167
+ @dataclass(frozen=True)
168
+ class PieceCidInfo:
169
+ piece_cid: str # PieceCIDv2 format
170
+ piece_cid_v1: str # Original PieceCIDv1 (CommP) from stream-commp
171
+ payload_size: int
172
+ unpadded_piece_size: int
173
+ padded_piece_size: int
174
+
175
+
176
+ def _resolve_commp_helper() -> Path:
177
+ override = os.environ.get("PYNAPSE_COMMP_HELPER")
178
+ if override:
179
+ return Path(override)
180
+
181
+ if DEFAULT_STREAM_COMMP_PATH.exists():
182
+ return DEFAULT_STREAM_COMMP_PATH
183
+
184
+ found = shutil.which("stream-commp")
185
+ if found:
186
+ return Path(found)
187
+
188
+ raise create_error(
189
+ "piece",
190
+ "calculate",
191
+ "stream-commp helper not found. Set PYNAPSE_COMMP_HELPER or install stream-commp.",
192
+ )
193
+
194
+
195
+ def _parse_stream_commp_output(output: str) -> PieceCidInfo:
196
+ commp_match = re.search(r"CommPCid:\s+(\S+)", output)
197
+ payload_match = re.search(r"Payload:\s+(\d+)\s+bytes", output)
198
+ unpadded_match = re.search(r"Unpadded piece:\s+(\d+)\s+bytes", output)
199
+ padded_match = re.search(r"Padded piece:\s+(\d+)\s+bytes", output)
200
+
201
+ if not (commp_match and payload_match and unpadded_match and padded_match):
202
+ raise create_error("piece", "parse", "Failed to parse stream-commp output")
203
+
204
+ piece_cid_v1 = commp_match.group(1)
205
+ payload_size = int(payload_match.group(1))
206
+ unpadded_piece_size = int(unpadded_match.group(1))
207
+ padded_piece_size = int(padded_match.group(1))
208
+
209
+ # Convert to PieceCIDv2 format (required by PDP servers per FRC-0069)
210
+ piece_cid_v2 = convert_to_pieceCIDv2(piece_cid_v1, payload_size, padded_piece_size)
211
+
212
+ return PieceCidInfo(
213
+ piece_cid=piece_cid_v2,
214
+ piece_cid_v1=piece_cid_v1,
215
+ payload_size=payload_size,
216
+ unpadded_piece_size=unpadded_piece_size,
217
+ padded_piece_size=padded_piece_size,
218
+ )
219
+
220
+
221
+ def calculate_piece_cid(data: Union[bytes, BinaryIO, Path]) -> PieceCidInfo:
222
+ helper = _resolve_commp_helper()
223
+
224
+ if isinstance(data, Path):
225
+ stream = data.open("rb")
226
+ close_stream = True
227
+ elif hasattr(data, "read"):
228
+ stream = data # type: ignore[assignment]
229
+ close_stream = False
230
+ else:
231
+ stream = None
232
+ close_stream = False
233
+
234
+ try:
235
+ if stream is not None:
236
+ proc = subprocess.run(
237
+ [str(helper)],
238
+ stdin=stream,
239
+ stdout=subprocess.PIPE,
240
+ stderr=subprocess.PIPE,
241
+ check=False,
242
+ )
243
+ else:
244
+ proc = subprocess.run(
245
+ [str(helper)],
246
+ input=data, # type: ignore[arg-type]
247
+ stdout=subprocess.PIPE,
248
+ stderr=subprocess.PIPE,
249
+ check=False,
250
+ )
251
+ finally:
252
+ if close_stream:
253
+ stream.close()
254
+
255
+ if proc.returncode != 0:
256
+ raise create_error(
257
+ "piece",
258
+ "calculate",
259
+ f"stream-commp failed with code {proc.returncode}",
260
+ )
261
+
262
+ stderr_text = proc.stderr.decode("utf-8", errors="replace")
263
+ return _parse_stream_commp_output(stderr_text)
pynapse/core/rand.py ADDED
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import secrets
4
+ from typing import Optional
5
+
6
+
7
+ def rand_u256() -> int:
8
+ return secrets.randbits(256)
9
+
10
+
11
+ def rand_index(length: int) -> int:
12
+ if length <= 0:
13
+ raise ValueError("length must be > 0")
14
+ return secrets.randbelow(length)