Skip to content

Commit 5b0be7f

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

File tree

9 files changed

+882
-0
lines changed

9 files changed

+882
-0
lines changed

hathor/dag_builder/__init__.py

Whitespace-only changes.

hathor/dag_builder/builder.py

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

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)