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.
- pynapse/__init__.py +6 -0
- pynapse/_version.py +1 -0
- pynapse/contracts/__init__.py +34 -0
- pynapse/contracts/abi_registry.py +11 -0
- pynapse/contracts/addresses.json +30 -0
- pynapse/contracts/erc20_abi.json +92 -0
- pynapse/contracts/errorsAbi.json +933 -0
- pynapse/contracts/filecoinPayV1Abi.json +2424 -0
- pynapse/contracts/filecoinWarmStorageServiceAbi.json +2363 -0
- pynapse/contracts/filecoinWarmStorageServiceStateViewAbi.json +651 -0
- pynapse/contracts/generated.py +35 -0
- pynapse/contracts/payments_abi.json +205 -0
- pynapse/contracts/pdpVerifierAbi.json +1266 -0
- pynapse/contracts/providerIdSetAbi.json +161 -0
- pynapse/contracts/serviceProviderRegistryAbi.json +1479 -0
- pynapse/contracts/sessionKeyRegistryAbi.json +147 -0
- pynapse/core/__init__.py +68 -0
- pynapse/core/abis.py +25 -0
- pynapse/core/chains.py +97 -0
- pynapse/core/constants.py +27 -0
- pynapse/core/errors.py +22 -0
- pynapse/core/piece.py +263 -0
- pynapse/core/rand.py +14 -0
- pynapse/core/typed_data.py +320 -0
- pynapse/core/utils.py +30 -0
- pynapse/evm/__init__.py +3 -0
- pynapse/evm/client.py +26 -0
- pynapse/filbeam/__init__.py +3 -0
- pynapse/filbeam/service.py +39 -0
- pynapse/payments/__init__.py +17 -0
- pynapse/payments/service.py +826 -0
- pynapse/pdp/__init__.py +21 -0
- pynapse/pdp/server.py +331 -0
- pynapse/pdp/types.py +38 -0
- pynapse/pdp/verifier.py +82 -0
- pynapse/retriever/__init__.py +12 -0
- pynapse/retriever/async_chain.py +227 -0
- pynapse/retriever/chain.py +209 -0
- pynapse/session/__init__.py +12 -0
- pynapse/session/key.py +30 -0
- pynapse/session/permissions.py +57 -0
- pynapse/session/registry.py +90 -0
- pynapse/sp_registry/__init__.py +11 -0
- pynapse/sp_registry/capabilities.py +25 -0
- pynapse/sp_registry/pdp_capabilities.py +102 -0
- pynapse/sp_registry/service.py +446 -0
- pynapse/sp_registry/types.py +52 -0
- pynapse/storage/__init__.py +57 -0
- pynapse/storage/async_context.py +682 -0
- pynapse/storage/async_manager.py +757 -0
- pynapse/storage/context.py +680 -0
- pynapse/storage/manager.py +758 -0
- pynapse/synapse.py +191 -0
- pynapse/utils/__init__.py +25 -0
- pynapse/utils/constants.py +25 -0
- pynapse/utils/errors.py +3 -0
- pynapse/utils/metadata.py +35 -0
- pynapse/utils/piece_url.py +16 -0
- pynapse/warm_storage/__init__.py +13 -0
- pynapse/warm_storage/service.py +513 -0
- synapse_filecoin_sdk-0.1.0.dist-info/METADATA +74 -0
- synapse_filecoin_sdk-0.1.0.dist-info/RECORD +64 -0
- synapse_filecoin_sdk-0.1.0.dist-info/WHEEL +4 -0
- 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
|
+
]
|
pynapse/core/__init__.py
ADDED
|
@@ -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)
|