Skip to content

Commit 7b52412

Browse files
committed
feat(dag-builder): Add a tool to generate vertices from a DAG description
1 parent 759c1d7 commit 7b52412

File tree

8 files changed

+864
-0
lines changed

8 files changed

+864
-0
lines changed

hathor/dag_builder/__init__.py

Whitespace-only changes.

hathor/dag_builder/builder.py

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

hathor/dag_builder/cli.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from hathor.dag_builder.builder import DAGBuilder
2+
from hathor.dag_builder.tokenizer import parse_file
3+
4+
5+
def main(filename: str, genesis_seed: str) -> None:
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(
30+
tokenizer=tokenizer,
31+
settings=settings,
32+
daa=daa,
33+
genesis_wallet=genesis_wallet,
34+
wallet_factory=wallet_factory
35+
)
36+
37+
for node, vertex in it:
38+
print('//', node)
39+
print('//', repr(vertex))
40+
print('//', node.name)
41+
print(bytes(vertex).hex())
42+
print()
43+
44+
45+
if __name__ == '__main__':
46+
import os
47+
import sys
48+
if 'HATHOR_CONFIG_YAML' not in os.environ:
49+
os.environ['HATHOR_CONFIG_YAML'] = './hathor/conf/testnet.yml'
50+
genesis_seed = os.environ['GENESIS_SEED']
51+
main(sys.argv[1], genesis_seed)

0 commit comments

Comments
 (0)