ed2k-client 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.
- ed2k_client/__init__.py +64 -0
- ed2k_client/__main__.py +7 -0
- ed2k_client/aich.py +351 -0
- ed2k_client/cli.py +409 -0
- ed2k_client/client.py +765 -0
- ed2k_client/constants.py +260 -0
- ed2k_client/download.py +551 -0
- ed2k_client/errors.py +59 -0
- ed2k_client/fetch.py +48 -0
- ed2k_client/files.py +29 -0
- ed2k_client/hashing.py +205 -0
- ed2k_client/identity.py +75 -0
- ed2k_client/kad/__init__.py +1 -0
- ed2k_client/kad/contacts.py +226 -0
- ed2k_client/kad/node.py +489 -0
- ed2k_client/links.py +179 -0
- ed2k_client/obfuscation.py +198 -0
- ed2k_client/partfile.py +386 -0
- ed2k_client/peer.py +715 -0
- ed2k_client/search.py +407 -0
- ed2k_client/security.py +832 -0
- ed2k_client/server.py +520 -0
- ed2k_client/servermet.py +247 -0
- ed2k_client/settings.py +307 -0
- ed2k_client/share.py +192 -0
- ed2k_client/sources.py +68 -0
- ed2k_client/tags.py +216 -0
- ed2k_client/transport.py +282 -0
- ed2k_client/wire.py +436 -0
- ed2k_client-0.1.0.dist-info/METADATA +296 -0
- ed2k_client-0.1.0.dist-info/RECORD +34 -0
- ed2k_client-0.1.0.dist-info/WHEEL +4 -0
- ed2k_client-0.1.0.dist-info/entry_points.txt +3 -0
- ed2k_client-0.1.0.dist-info/licenses/LICENSE +674 -0
ed2k_client/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Python client for the eDonkey2000 (eD2k) and Kad networks: connect, search, download and share."""
|
|
2
|
+
|
|
3
|
+
from ed2k_client.client import Client, ClientConfig, ClientStatus, ServerStatus
|
|
4
|
+
from ed2k_client.download import AichPolicy, ContentPolicy, DownloadProgress, DownloadResult, DownloadSettings
|
|
5
|
+
from ed2k_client.errors import (
|
|
6
|
+
ConnectionFailedError,
|
|
7
|
+
DownloadError,
|
|
8
|
+
Ed2kError,
|
|
9
|
+
HashMismatchError,
|
|
10
|
+
LinkMismatchError,
|
|
11
|
+
NotConnectedError,
|
|
12
|
+
ProtocolError,
|
|
13
|
+
SecurityError,
|
|
14
|
+
StateInUseError,
|
|
15
|
+
)
|
|
16
|
+
from ed2k_client.hashing import Ed2kHash, FileHashes, hash_file
|
|
17
|
+
from ed2k_client.links import FileLink, ServerLink
|
|
18
|
+
from ed2k_client.search import FileType, SearchQuery, SearchResult
|
|
19
|
+
from ed2k_client.security import DocumentKind, Inspection, inspect_document
|
|
20
|
+
from ed2k_client.servermet import ServerEntry, ServerList, ServerMemory
|
|
21
|
+
from ed2k_client.settings import IoSettings, KadSettings, Limits, PeerSettings, ShareSettings, Timeouts
|
|
22
|
+
from ed2k_client.sources import Source
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
'AichPolicy',
|
|
26
|
+
'Client',
|
|
27
|
+
'ClientConfig',
|
|
28
|
+
'ClientStatus',
|
|
29
|
+
'ConnectionFailedError',
|
|
30
|
+
'ContentPolicy',
|
|
31
|
+
'DocumentKind',
|
|
32
|
+
'DownloadError',
|
|
33
|
+
'DownloadProgress',
|
|
34
|
+
'DownloadResult',
|
|
35
|
+
'DownloadSettings',
|
|
36
|
+
'Ed2kError',
|
|
37
|
+
'Ed2kHash',
|
|
38
|
+
'FileHashes',
|
|
39
|
+
'FileLink',
|
|
40
|
+
'FileType',
|
|
41
|
+
'HashMismatchError',
|
|
42
|
+
'Inspection',
|
|
43
|
+
'IoSettings',
|
|
44
|
+
'KadSettings',
|
|
45
|
+
'Limits',
|
|
46
|
+
'LinkMismatchError',
|
|
47
|
+
'NotConnectedError',
|
|
48
|
+
'PeerSettings',
|
|
49
|
+
'ProtocolError',
|
|
50
|
+
'SearchQuery',
|
|
51
|
+
'SearchResult',
|
|
52
|
+
'SecurityError',
|
|
53
|
+
'ServerEntry',
|
|
54
|
+
'ServerLink',
|
|
55
|
+
'ServerList',
|
|
56
|
+
'ServerMemory',
|
|
57
|
+
'ServerStatus',
|
|
58
|
+
'ShareSettings',
|
|
59
|
+
'Source',
|
|
60
|
+
'StateInUseError',
|
|
61
|
+
'Timeouts',
|
|
62
|
+
'hash_file',
|
|
63
|
+
'inspect_document',
|
|
64
|
+
]
|
ed2k_client/__main__.py
ADDED
ed2k_client/aich.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""AICH, the SHA-1 hash tree eMule keeps over a file, and the proof of block hashes against its root.
|
|
2
|
+
|
|
3
|
+
The tree follows ``CAICHHashTree`` of aMule:
|
|
4
|
+
https://github.com/amule-project/amule/blob/6a1105634d81242a581a0f36a7e00379b12a83f3/src/SHAHashSet.cpp#L102
|
|
5
|
+
Its leaves are blocks of 180 KiB that start at a part boundary, the request blocks of :mod:`ed2k_client.partfile`,
|
|
6
|
+
and its upper levels split the file into whole parts first. A node holding an odd number of units gives the larger
|
|
7
|
+
half to the side it hangs on, and the root counts as a left node. A node is named by its path from the root: the root
|
|
8
|
+
is 1, and every step down appends one bit, 1 for the left child and 0 for the right one.
|
|
9
|
+
|
|
10
|
+
The root comes from the ``h=`` field of a link, which makes it trusted. A peer with the complete file answers a
|
|
11
|
+
request for one part with the hashes of the part's blocks and of the sibling of every node on the way up. The client
|
|
12
|
+
folds them to the root and keeps only hashes that took part in reproducing it, so a forged answer needs a second
|
|
13
|
+
preimage of SHA-1. The blocks of a downloaded part are then compared with their proven hashes one by one.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import TYPE_CHECKING, Self
|
|
18
|
+
|
|
19
|
+
from Crypto.Hash import SHA1
|
|
20
|
+
|
|
21
|
+
from ed2k_client.constants import AICH_HASH_LENGTH, BLOCK_SIZE, PART_SIZE
|
|
22
|
+
from ed2k_client.errors import ProtocolError
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Iterator, Mapping
|
|
26
|
+
|
|
27
|
+
from ed2k_client.wire import Reader
|
|
28
|
+
|
|
29
|
+
ROOT_IDENT: int = 1
|
|
30
|
+
|
|
31
|
+
# aMule refuses a 32-bit identifier above 2^22, which no file under the size limit needs:
|
|
32
|
+
# https://github.com/amule-project/amule/blob/6a1105634d81242a581a0f36a7e00379b12a83f3/src/SHAHashSet.cpp#L592
|
|
33
|
+
MAX_IDENT: int = 0x400000
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def combine(left: bytes, right: bytes) -> bytes:
|
|
37
|
+
"""Hash two child hashes into the hash of their parent.
|
|
38
|
+
|
|
39
|
+
:param left: the left child's hash.
|
|
40
|
+
:type left: bytes
|
|
41
|
+
:param right: the right child's hash.
|
|
42
|
+
:type right: bytes
|
|
43
|
+
:returns: the parent's hash.
|
|
44
|
+
:rtype: bytes
|
|
45
|
+
"""
|
|
46
|
+
return SHA1.new(left + right).digest()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class AichNode:
|
|
51
|
+
"""One node of the tree: the bytes it covers and its name.
|
|
52
|
+
|
|
53
|
+
:ivar ident: the path from the root as bits, 1 for the root.
|
|
54
|
+
:ivar start: the first byte offset it covers.
|
|
55
|
+
:ivar size: the number of bytes it covers.
|
|
56
|
+
:ivar left: whether it is a left child, which the root counts as.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
ident: int
|
|
60
|
+
start: int
|
|
61
|
+
size: int
|
|
62
|
+
left: bool
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def root(cls, size: int) -> Self:
|
|
66
|
+
"""Build the root of a file's tree.
|
|
67
|
+
|
|
68
|
+
:param size: the file size.
|
|
69
|
+
:type size: int
|
|
70
|
+
:returns: the root.
|
|
71
|
+
:rtype: Self
|
|
72
|
+
"""
|
|
73
|
+
return cls(ident=ROOT_IDENT, start=0, size=size, left=True)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def leaf(self) -> bool:
|
|
77
|
+
"""Whether the node is one block and has no children."""
|
|
78
|
+
return self.size <= BLOCK_SIZE
|
|
79
|
+
|
|
80
|
+
def children(self) -> tuple[Self, Self]:
|
|
81
|
+
"""Split the node the way aMule splits it.
|
|
82
|
+
|
|
83
|
+
:returns: the left and the right child.
|
|
84
|
+
:rtype: tuple[Self, Self]
|
|
85
|
+
"""
|
|
86
|
+
# A node larger than a part is split by whole parts, a part or less by blocks:
|
|
87
|
+
# https://github.com/amule-project/amule/blob/6a1105634d81242a581a0f36a7e00379b12a83f3/src/SHAHashSet.cpp#L128
|
|
88
|
+
unit = BLOCK_SIZE if self.size <= PART_SIZE else PART_SIZE
|
|
89
|
+
units = -(-self.size // unit)
|
|
90
|
+
left_size = (units + self.left) // 2 * unit
|
|
91
|
+
cls = type(self)
|
|
92
|
+
return (
|
|
93
|
+
cls(ident=self.ident << 1 | 1, start=self.start, size=left_size, left=True),
|
|
94
|
+
cls(ident=self.ident << 1, start=self.start + left_size, size=self.size - left_size, left=False),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def path_to(self, *, start: int, size: int) -> list[Self]:
|
|
98
|
+
"""Walk down to the node that covers exactly a range.
|
|
99
|
+
|
|
100
|
+
:param start: the first byte offset of the range.
|
|
101
|
+
:type start: int
|
|
102
|
+
:param size: the length of the range.
|
|
103
|
+
:type size: int
|
|
104
|
+
:returns: the nodes from this one down to the one covering the range.
|
|
105
|
+
:rtype: list[Self]
|
|
106
|
+
:raises ValueError: when no node covers exactly that range.
|
|
107
|
+
"""
|
|
108
|
+
node, path = self, [self]
|
|
109
|
+
while (node.start, node.size) != (start, size):
|
|
110
|
+
if node.leaf:
|
|
111
|
+
err_msg = f'no node covers {size} bytes at {start}'
|
|
112
|
+
raise ValueError(err_msg)
|
|
113
|
+
left, right = node.children()
|
|
114
|
+
node = left if start < right.start else right
|
|
115
|
+
path.append(node)
|
|
116
|
+
return path
|
|
117
|
+
|
|
118
|
+
def leaves(self) -> Iterator[Self]:
|
|
119
|
+
"""Yield the blocks under this node from left to right.
|
|
120
|
+
|
|
121
|
+
:returns: the leaves.
|
|
122
|
+
:rtype: Iterator[Self]
|
|
123
|
+
"""
|
|
124
|
+
if self.leaf:
|
|
125
|
+
yield self
|
|
126
|
+
return
|
|
127
|
+
for child in self.children():
|
|
128
|
+
yield from child.leaves()
|
|
129
|
+
|
|
130
|
+
def descendant(self, ident: int) -> Self:
|
|
131
|
+
"""Find the node an identifier names.
|
|
132
|
+
|
|
133
|
+
:param ident: the identifier.
|
|
134
|
+
:type ident: int
|
|
135
|
+
:returns: the node.
|
|
136
|
+
:rtype: Self
|
|
137
|
+
:raises ProtocolError: when the identifier names no node of this tree.
|
|
138
|
+
"""
|
|
139
|
+
node = self
|
|
140
|
+
for shift in range(ident.bit_length() - 2, -1, -1):
|
|
141
|
+
if node.leaf:
|
|
142
|
+
err_msg = f'AICH identifier {ident} reaches below a block'
|
|
143
|
+
raise ProtocolError(err_msg)
|
|
144
|
+
left, right = node.children()
|
|
145
|
+
node = left if ident >> shift & 1 else right
|
|
146
|
+
return node
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def read_recovery(reader: Reader, *, count: int) -> dict[int, bytes]:
|
|
150
|
+
"""Read the hashes of a recovery answer by node identifier.
|
|
151
|
+
|
|
152
|
+
aMule writes 16-bit identifiers and a closing zero count for a file under 4 GB, and a zero count followed by
|
|
153
|
+
32-bit identifiers for a larger one: https://github.com/amule-project/amule/blob/6a1105634d81242a581a0f36a7e00379b12a83f3/src/SHAHashSet.cpp#L534
|
|
154
|
+
|
|
155
|
+
:param reader: the reader, placed after the part number and the root.
|
|
156
|
+
:type reader: Reader
|
|
157
|
+
:param count: how many hashes the requested part needs.
|
|
158
|
+
:type count: int
|
|
159
|
+
:returns: the hashes, not yet proven.
|
|
160
|
+
:rtype: dict[int, bytes]
|
|
161
|
+
:raises ProtocolError: on another count, a repeated identifier or one that would replace the root.
|
|
162
|
+
"""
|
|
163
|
+
wide = False
|
|
164
|
+
found = reader.u16()
|
|
165
|
+
if found == 0:
|
|
166
|
+
wide, found = True, reader.u16()
|
|
167
|
+
if found != count:
|
|
168
|
+
err_msg = f'AICH answer with {found} hashes where {count} were due'
|
|
169
|
+
raise ProtocolError(err_msg)
|
|
170
|
+
|
|
171
|
+
hashes: dict[int, bytes] = {}
|
|
172
|
+
for _ in range(found):
|
|
173
|
+
ident = reader.u32() if wide else reader.u16()
|
|
174
|
+
if not ROOT_IDENT < ident <= MAX_IDENT or ident in hashes:
|
|
175
|
+
err_msg = f'AICH answer with the invalid or repeated identifier {ident}'
|
|
176
|
+
raise ProtocolError(err_msg)
|
|
177
|
+
hashes[ident] = reader.take(AICH_HASH_LENGTH)
|
|
178
|
+
return hashes
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class AichTree:
|
|
182
|
+
"""The hashes of one file's tree that are proven against its trusted root.
|
|
183
|
+
|
|
184
|
+
:ivar size: the file size.
|
|
185
|
+
:ivar root: the trusted root hash.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, *, size: int, root: bytes) -> None:
|
|
189
|
+
"""Start from the trusted root alone.
|
|
190
|
+
|
|
191
|
+
:param size: the file size.
|
|
192
|
+
:type size: int
|
|
193
|
+
:param root: the root hash from the link.
|
|
194
|
+
:type root: bytes
|
|
195
|
+
"""
|
|
196
|
+
self.size = size
|
|
197
|
+
self.root = root
|
|
198
|
+
self._top = AichNode.root(size)
|
|
199
|
+
self._proven: dict[int, bytes] = {ROOT_IDENT: root}
|
|
200
|
+
|
|
201
|
+
def _part_path(self, part: int) -> list[AichNode]:
|
|
202
|
+
"""Walk from the root to the node of one part.
|
|
203
|
+
|
|
204
|
+
:param part: the part index.
|
|
205
|
+
:type part: int
|
|
206
|
+
:returns: the nodes from the root down to the part.
|
|
207
|
+
:rtype: list[AichNode]
|
|
208
|
+
"""
|
|
209
|
+
start = part * PART_SIZE
|
|
210
|
+
return self._top.path_to(start=start, size=min(PART_SIZE, self.size - start))
|
|
211
|
+
|
|
212
|
+
def block_hash(self, *, start: int, size: int) -> bytes | None:
|
|
213
|
+
"""Look up the proven hash of one block.
|
|
214
|
+
|
|
215
|
+
:param start: the first byte offset of the block.
|
|
216
|
+
:type start: int
|
|
217
|
+
:param size: the block length.
|
|
218
|
+
:type size: int
|
|
219
|
+
:returns: the hash, or ``None`` while it is not proven.
|
|
220
|
+
:rtype: bytes | None
|
|
221
|
+
"""
|
|
222
|
+
return self._proven.get(self._top.path_to(start=start, size=size)[-1].ident)
|
|
223
|
+
|
|
224
|
+
def ready(self, part: int) -> bool:
|
|
225
|
+
"""Check whether every block of a part has a proven hash.
|
|
226
|
+
|
|
227
|
+
:param part: the part index.
|
|
228
|
+
:type part: int
|
|
229
|
+
:returns: whether the part can be checked block by block.
|
|
230
|
+
:rtype: bool
|
|
231
|
+
"""
|
|
232
|
+
return all(leaf.ident in self._proven for leaf in self._part_path(part)[-1].leaves())
|
|
233
|
+
|
|
234
|
+
def source_part(self, part: int) -> int:
|
|
235
|
+
"""Name the part whose recovery answer proves the blocks of a part.
|
|
236
|
+
|
|
237
|
+
A part of one block is a leaf, and aMule serves no answer for it. Being the last part, it is the right
|
|
238
|
+
sibling of a node on the way to the part before it, so that part's answer carries its hash:
|
|
239
|
+
https://github.com/amule-project/amule/blob/6a1105634d81242a581a0f36a7e00379b12a83f3/src/DownloadClient.cpp#L1681
|
|
240
|
+
|
|
241
|
+
:param part: the part index.
|
|
242
|
+
:type part: int
|
|
243
|
+
:returns: the part to ask for.
|
|
244
|
+
:rtype: int
|
|
245
|
+
"""
|
|
246
|
+
return part - 1 if part > 0 and self._part_path(part)[-1].leaf else part
|
|
247
|
+
|
|
248
|
+
def hash_count(self, part: int) -> int:
|
|
249
|
+
"""Count the hashes a recovery answer for a part carries.
|
|
250
|
+
|
|
251
|
+
:param part: the part index.
|
|
252
|
+
:type part: int
|
|
253
|
+
:returns: one sibling per level above the part plus one hash per block.
|
|
254
|
+
:rtype: int
|
|
255
|
+
"""
|
|
256
|
+
path = self._part_path(part)
|
|
257
|
+
return len(path) - 1 + sum(1 for _ in path[-1].leaves())
|
|
258
|
+
|
|
259
|
+
def accept(self, *, part: int, hashes: Mapping[int, bytes]) -> None:
|
|
260
|
+
"""Prove the hashes of a recovery answer against the root and keep the proven ones.
|
|
261
|
+
|
|
262
|
+
:param part: the part the answer was asked for.
|
|
263
|
+
:type part: int
|
|
264
|
+
:param hashes: the hashes by node identifier.
|
|
265
|
+
:type hashes: Mapping[int, bytes]
|
|
266
|
+
:raises ProtocolError: when the hashes do not reproduce the root or leave a block of the part out.
|
|
267
|
+
"""
|
|
268
|
+
folded: dict[int, bytes] = {}
|
|
269
|
+
if self._fold(self._top, hashes=hashes, ancestors=self._ancestors(hashes), folded=folded) != self.root:
|
|
270
|
+
err_msg = f'AICH answer for part {part} does not lead to the root of the link'
|
|
271
|
+
raise ProtocolError(err_msg)
|
|
272
|
+
|
|
273
|
+
proven = self._proven_by(folded=folded, hashes=hashes)
|
|
274
|
+
if any(leaf.ident not in proven for leaf in self._part_path(part)[-1].leaves()):
|
|
275
|
+
err_msg = f'the recovery hashes leave blocks of part {part} out'
|
|
276
|
+
raise ProtocolError(err_msg)
|
|
277
|
+
self._proven.update(proven)
|
|
278
|
+
|
|
279
|
+
def _ancestors(self, hashes: Mapping[int, bytes]) -> set[int]:
|
|
280
|
+
"""Collect the nodes above the given ones, the only nodes a fold has to visit.
|
|
281
|
+
|
|
282
|
+
Folding along them alone makes a 4 GB file cost as much as a small one.
|
|
283
|
+
|
|
284
|
+
:param hashes: the hashes by node identifier.
|
|
285
|
+
:type hashes: Mapping[int, bytes]
|
|
286
|
+
:returns: the identifiers of every strict ancestor of a given node.
|
|
287
|
+
:rtype: set[int]
|
|
288
|
+
:raises ProtocolError: when an identifier names no node of this file's tree.
|
|
289
|
+
"""
|
|
290
|
+
ancestors: set[int] = set()
|
|
291
|
+
for ident in hashes:
|
|
292
|
+
self._top.descendant(ident)
|
|
293
|
+
parent = ident
|
|
294
|
+
while parent > ROOT_IDENT:
|
|
295
|
+
parent >>= 1
|
|
296
|
+
ancestors.add(parent)
|
|
297
|
+
return ancestors
|
|
298
|
+
|
|
299
|
+
def _fold(
|
|
300
|
+
self, node: AichNode, *, hashes: Mapping[int, bytes], ancestors: set[int], folded: dict[int, bytes]
|
|
301
|
+
) -> bytes | None:
|
|
302
|
+
"""Compute a node's hash from below where possible and check it against a given one.
|
|
303
|
+
|
|
304
|
+
:param node: the node.
|
|
305
|
+
:type node: AichNode
|
|
306
|
+
:param hashes: the hashes of the answer.
|
|
307
|
+
:type hashes: Mapping[int, bytes]
|
|
308
|
+
:param ancestors: the nodes above the given ones.
|
|
309
|
+
:type ancestors: set[int]
|
|
310
|
+
:param folded: receives every hash computed from children.
|
|
311
|
+
:type folded: dict[int, bytes]
|
|
312
|
+
:returns: the hash, or ``None`` when neither the answer nor its children give one.
|
|
313
|
+
:rtype: bytes | None
|
|
314
|
+
:raises ProtocolError: when a given hash contradicts the hash of its children.
|
|
315
|
+
"""
|
|
316
|
+
given = hashes.get(node.ident)
|
|
317
|
+
if node.ident not in ancestors:
|
|
318
|
+
return given
|
|
319
|
+
left, right = (
|
|
320
|
+
self._fold(child, hashes=hashes, ancestors=ancestors, folded=folded) for child in node.children()
|
|
321
|
+
)
|
|
322
|
+
if left is None or right is None:
|
|
323
|
+
return given
|
|
324
|
+
digest = combine(left, right)
|
|
325
|
+
if given is not None and given != digest:
|
|
326
|
+
err_msg = f'AICH hash of node {node.ident} contradicts its children'
|
|
327
|
+
raise ProtocolError(err_msg)
|
|
328
|
+
folded[node.ident] = digest
|
|
329
|
+
return digest
|
|
330
|
+
|
|
331
|
+
def _proven_by(self, *, folded: Mapping[int, bytes], hashes: Mapping[int, bytes]) -> dict[int, bytes]:
|
|
332
|
+
"""Collect the hashes the root was computed from, so a stray hash beside them proves nothing.
|
|
333
|
+
|
|
334
|
+
:param folded: the hashes computed from children.
|
|
335
|
+
:type folded: Mapping[int, bytes]
|
|
336
|
+
:param hashes: the hashes of the answer.
|
|
337
|
+
:type hashes: Mapping[int, bytes]
|
|
338
|
+
:returns: the proven hashes by node identifier.
|
|
339
|
+
:rtype: dict[int, bytes]
|
|
340
|
+
"""
|
|
341
|
+
# Every child of a folded node gave its parent a hash, either folded from below or taken from the answer
|
|
342
|
+
proven: dict[int, bytes] = {}
|
|
343
|
+
pending = [self._top]
|
|
344
|
+
while pending:
|
|
345
|
+
node = pending.pop()
|
|
346
|
+
if node.ident in folded:
|
|
347
|
+
proven[node.ident] = folded[node.ident]
|
|
348
|
+
pending.extend(node.children())
|
|
349
|
+
else:
|
|
350
|
+
proven[node.ident] = hashes[node.ident]
|
|
351
|
+
return proven
|