Skip to content

Commit 232bc3e

Browse files
committed
tests: DAG builder
1 parent 759c1d7 commit 232bc3e

12 files changed

+838
-6
lines changed

hathor/cli/load_from_logs.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import re
1516
import sys
1617
from argparse import ArgumentParser, FileType
1718

@@ -39,17 +40,38 @@ async def _load_from_logs(self) -> None:
3940
settings = get_global_settings()
4041
parser = VertexParser(settings=settings)
4142

43+
last_comment = ''
44+
labels = {}
45+
4246
while True:
4347
line_with_break = self._args.log_dump.readline()
4448
if not line_with_break:
4549
break
46-
if line_with_break.startswith('//'):
47-
continue
4850
line = line_with_break.strip()
51+
if not line:
52+
continue
53+
if line.startswith('//'):
54+
last_comment = line[2:].strip()
55+
continue
4956
vertex_bytes = bytes.fromhex(line)
5057
vertex = parser.deserialize(vertex_bytes)
58+
labels[vertex.hash] = last_comment
5159
await deferLater(self.reactor, 0, self.manager.on_new_tx, vertex)
5260

61+
print('---> graphviz')
62+
from hathor.graphviz import GraphvizVisualizer
63+
tx_storage = self.manager.tx_storage
64+
graphviz = GraphvizVisualizer(tx_storage, include_verifications=True, include_funds=True, only_with_labels=True)
65+
graphviz.labels[self.manager._settings.GENESIS_BLOCK_HASH] = 'g_block'
66+
graphviz.labels[self.manager._settings.GENESIS_TX1_HASH] = 'g_tx1'
67+
graphviz.labels[self.manager._settings.GENESIS_TX2_HASH] = 'g_tx2'
68+
for k, v in labels.items():
69+
if re.match(r'^a[0-9]+$', v):
70+
continue
71+
graphviz.labels[k] = v
72+
dot = graphviz.dot()
73+
dot.render('dot0')
74+
5375
self.manager.connections.disconnect_all_peers(force=True)
5476
self.reactor.fireSystemEvent('shutdown')
5577

hathor/daa.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def __init__(self, *, settings: HathorSettings, test_mode: TestMode = TestMode.D
5757
self.TEST_MODE = test_mode
5858
DifficultyAdjustmentAlgorithm.singleton = self
5959

60-
@cpu.profiler(key=lambda _, block: 'calculate_block_difficulty!{}'.format(block.hash.hex()))
60+
@cpu.profiler(key=lambda _, block: 'calculate_block_difficulty!{}'.format(block.hash.hex() if block._hash else None))
6161
def calculate_block_difficulty(self, block: 'Block', parent_block_getter: Callable[['Block'], 'Block']) -> float:
6262
""" Calculate block weight according to the ascendants of `block`, using calculate_next_weight."""
6363
if self.TEST_MODE & TestMode.TEST_BLOCK_WEIGHT:

hathor/dag_builder/__init__.py

Whitespace-only changes.

hathor/dag_builder/builder.py

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
from __future__ import annotations
2+
3+
from collections import defaultdict
4+
from dataclasses import dataclass, field
5+
from typing import Any, Iterator, NamedTuple
6+
7+
from hathor.dag_builder.tokenizer import Token
8+
9+
class DAGBuilder:
10+
def __init__(self):
11+
self._nodes = {}
12+
13+
def parse_tokens(self, tokens: Iterator) -> None:
14+
for parts in tokens:
15+
match parts:
16+
case (Token.PARENT, _from, _to):
17+
self.add_parent_edge(_from, _to)
18+
19+
case (Token.SPEND, _from, _to, _txout_index):
20+
self.add_spending_edge(_from, _to, _txout_index)
21+
22+
case (Token.ATTRIBUTE, name, key, value):
23+
self.add_attribute(name, key, value)
24+
25+
case (Token.ORDER_BEFORE, _from, _to):
26+
self.add_deps(_from, _to)
27+
28+
case (Token.OUTPUT, name, index, amount, token, attrs):
29+
self.set_output(name, index, amount, token, attrs)
30+
31+
case (Token.BLOCKCHAIN, name, first_parent, begin_index, end_index):
32+
self.add_blockchain(name, first_parent, begin_index, end_index)
33+
34+
case _:
35+
raise NotImplementedError(parts)
36+
37+
def _get_node(self, name, *, default_type='unknown'):
38+
if name not in self._nodes:
39+
self._nodes[name] = DAGNode(name=name, type=default_type)
40+
node = self._nodes[name]
41+
# TODO Set type if unknown.
42+
return node
43+
44+
def add_deps(self, _from, _to):
45+
from_node = self._get_node(_from)
46+
self._get_node(_to)
47+
from_node.deps.add(_to)
48+
49+
def add_blockchain(self, prefix: str, first_parent: str | None, first_index: int, last_index: int):
50+
prev = first_parent
51+
for i in range(first_index, last_index + 1):
52+
name = f'{prefix}{i}'
53+
self._get_node(name, default_type='block')
54+
if prev is not None:
55+
self.add_parent_edge(name, prev)
56+
prev = name
57+
58+
def add_parent_edge(self, _from, _to):
59+
self._get_node(_to, default_type='transaction')
60+
from_node = self._get_node(_from, default_type='transaction')
61+
from_node.parents.add(_to)
62+
63+
def add_spending_edge(self, _from, _to, _txout_index):
64+
self._get_node(_to, default_type='transaction')
65+
from_node = self._get_node(_from, default_type='transaction')
66+
from_node.inputs.add(DAGInput(_to, _txout_index))
67+
68+
def set_output(self, name, index, amount, token, attrs):
69+
node = self._get_node(name)
70+
if len(node.outputs) <= index:
71+
node.outputs.extend([None] * (index - len(node.outputs) + 1))
72+
node.outputs[index] = DAGOutput(amount, token, attrs)
73+
if token != 'HTR':
74+
self._get_node(token, default_type='token')
75+
node.deps.add(token)
76+
77+
def add_attribute(self, name, key, value):
78+
node = self._get_node(name)
79+
node.attrs[key] = value
80+
81+
def topological_sorting(self) -> Iterator[DAGNode]:
82+
direct_deps: dict[str, str[str]] = {}
83+
rev_deps: dict[str, set[str]] = defaultdict(set)
84+
seen: set[str] = set()
85+
candidates: list[str] = []
86+
for name, node in self._nodes.items():
87+
assert name == node.name
88+
deps = set(node.get_all_dependencies())
89+
assert name not in direct_deps
90+
direct_deps[name] = deps
91+
for x in deps:
92+
rev_deps[x].add(name)
93+
if len(deps) == 0:
94+
candidates.append(name)
95+
96+
for _ in range(len(self._nodes)):
97+
if len(candidates) == 0:
98+
# TODO improve error message showing at least one cycle
99+
print()
100+
print('direct_deps', direct_deps)
101+
print()
102+
print('rev_deps', rev_deps)
103+
print()
104+
print('seen', seen)
105+
print()
106+
print('not_seen', set(self._nodes.keys()) - seen)
107+
print()
108+
print('nodes')
109+
for node in self._nodes.values():
110+
print(node)
111+
print()
112+
raise RuntimeError('there is at least one cycle')
113+
name = candidates.pop()
114+
assert name not in seen
115+
seen.add(name)
116+
for d in rev_deps[name]:
117+
direct_deps[d].remove(name)
118+
if len(direct_deps[d]) == 0:
119+
candidates.append(d)
120+
del direct_deps[d]
121+
node = self._get_node(name)
122+
yield node
123+
124+
def build(self, tokenizer, settings, daa, genesis_wallet, wallet_factory) -> Iterator[tuple[str, 'BaseTransaction']]:
125+
from hathor.dag_builder.default_filler import DefaultFiller
126+
from hathor.dag_builder.vertex_exporter import VertexExporter
127+
128+
filler = DefaultFiller(self, settings, daa)
129+
130+
exporter = VertexExporter(self)
131+
exporter.set_genesis_wallet(genesis_wallet)
132+
exporter.set_wallet_factory(wallet_factory)
133+
exporter.set_daa(daa)
134+
exporter.set_settings(settings)
135+
136+
self._get_node('dummy', default_type='transaction')
137+
138+
self.parse_tokens(tokenizer)
139+
140+
for node in self._nodes.values():
141+
if node.type == 'block':
142+
continue
143+
if node.type == 'genesis':
144+
continue
145+
if node.name == 'dummy':
146+
continue
147+
self.add_deps(node.name, 'dummy')
148+
149+
filler.run()
150+
return exporter.export()
151+
152+
153+
@dataclass(frozen=True)
154+
class DAGNode:
155+
name: str
156+
type: str
157+
158+
attrs: dict[str, str] = field(default_factory=dict)
159+
inputs: set[DAGInput] = field(default_factory=set)
160+
outputs: list[DAGOutput] = field(default_factory=list)
161+
parents: set[str] = field(default_factory=set)
162+
deps: set[str] = field(default_factory=set)
163+
164+
def get_all_dependencies(self):
165+
yield from self.parents
166+
yield from (name for name, _ in self.inputs)
167+
yield from self.deps
168+
169+
170+
class DAGInput(NamedTuple):
171+
node_name: str
172+
index: int
173+
174+
175+
class DAGOutput(NamedTuple):
176+
amount: int
177+
token: str
178+
attrs: list[tuple[str, Any]]

hathor/dag_builder/cli.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from hathor.dag_builder.builder import DAGBuilder
2+
from hathor.dag_builder.tokenizer import parse_file
3+
4+
5+
def main(filename, genesis_seed):
6+
from hathor.reactor import initialize_global_reactor
7+
8+
# reactor
9+
_ = initialize_global_reactor(use_asyncio_reactor=False)
10+
11+
from hathor.conf.get_settings import get_global_settings
12+
from hathor.daa import DifficultyAdjustmentAlgorithm
13+
from hathor.wallet import HDWallet
14+
settings = get_global_settings()
15+
16+
def wallet_factory(words=None):
17+
if words is None:
18+
words = ('bind daring above film health blush during tiny neck slight clown salmon '
19+
'wine brown good setup later omit jaguar tourist rescue flip pet salute')
20+
hd = HDWallet(words=words)
21+
hd._manually_initialize()
22+
return hd
23+
24+
genesis_wallet = wallet_factory(genesis_seed)
25+
daa = DifficultyAdjustmentAlgorithm(settings=settings)
26+
27+
builder = DAGBuilder()
28+
tokenizer = parse_file(filename)
29+
it = builder.build(tokenizer, settings, daa, genesis_wallet, wallet_factory)
30+
31+
for node, vertex in it:
32+
print('//', node)
33+
print('//', repr(vertex))
34+
print('//', node.name)
35+
print(bytes(vertex).hex())
36+
print()
37+
38+
39+
if __name__ == '__main__':
40+
import os
41+
import sys
42+
if 'HATHOR_CONFIG_YAML' not in os.environ:
43+
os.environ['HATHOR_CONFIG_YAML'] = './hathor/conf/testnet.yml'
44+
genesis_seed = os.environ['GENESIS_SEED']
45+
main(sys.argv[1], genesis_seed)

0 commit comments

Comments
 (0)