Skip to content

Commit c4b256d

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

13 files changed

+1197
-10
lines changed

hathor/conf/unittests.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
MIN_SHARE_WEIGHT=2,
2626
MAX_TX_WEIGHT_DIFF=25.0,
2727
BLOCK_DIFFICULTY_N_BLOCKS=20,
28-
GENESIS_OUTPUT_SCRIPT=bytes.fromhex('76a914fd05059b6006249543b82f36876a17c73fd2267b88ac'),
28+
GENESIS_OUTPUT_SCRIPT=bytes.fromhex('76a914d07bc82d6e0d1bb116614076645e9b87c8c83b4188ac'),
2929
GENESIS_BLOCK_NONCE=0,
30-
GENESIS_BLOCK_HASH=bytes.fromhex('339f47da87435842b0b1b528ecd9eac2495ce983b3e9c923a37e1befbe12c792'),
30+
GENESIS_BLOCK_HASH=bytes.fromhex('e0b484fa4254cc628028f8cf8947396644beb22312f6e295f51b269f64d3b28b'),
3131
GENESIS_TX1_NONCE=6,
3232
GENESIS_TX1_HASH=bytes.fromhex('16ba3dbe424c443e571b00840ca54b9ff4cff467e10b6a15536e718e2008f952'),
3333
GENESIS_TX2_NONCE=2,

hathor/conf/unittests.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ MIN_TX_WEIGHT: 2
77
MIN_SHARE_WEIGHT: 2
88
MAX_TX_WEIGHT_DIFF: 25.0
99
BLOCK_DIFFICULTY_N_BLOCKS: 20
10-
GENESIS_OUTPUT_SCRIPT: 76a914fd05059b6006249543b82f36876a17c73fd2267b88ac
10+
GENESIS_OUTPUT_SCRIPT: 76a914d07bc82d6e0d1bb116614076645e9b87c8c83b4188ac
1111
GENESIS_BLOCK_NONCE: 0
12-
GENESIS_BLOCK_HASH: 339f47da87435842b0b1b528ecd9eac2495ce983b3e9c923a37e1befbe12c792
12+
GENESIS_BLOCK_HASH: e0b484fa4254cc628028f8cf8947396644beb22312f6e295f51b269f64d3b28b
1313
GENESIS_TX1_NONCE: 6
1414
GENESIS_TX1_HASH: 16ba3dbe424c443e571b00840ca54b9ff4cff467e10b6a15536e718e2008f952
1515
GENESIS_TX2_NONCE: 2

hathor/dag_builder/__init__.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from hathor.dag_builder.builder import DAGBuilder
16+
17+
__all__ = ['DAGBuilder']

hathor/dag_builder/artifacts.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import TYPE_CHECKING, Iterator, NamedTuple
18+
19+
from hathor.dag_builder.types import DAGNode
20+
21+
if TYPE_CHECKING:
22+
from hathor.transaction import BaseTransaction
23+
24+
25+
class _Pair(NamedTuple):
26+
node: DAGNode
27+
vertex: BaseTransaction
28+
29+
30+
class DAGArtifacts:
31+
def __init__(self, items: Iterator[tuple[DAGNode, BaseTransaction]]) -> None:
32+
self.by_name: dict[str, _Pair] = {}
33+
34+
v: list[_Pair] = []
35+
for node, vertex in items:
36+
p = _Pair(node, vertex)
37+
v.append(p)
38+
self.by_name[node.name] = p
39+
40+
self.list: tuple[_Pair, ...] = tuple(v)

hathor/dag_builder/builder.py

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from collections import defaultdict
18+
from typing import Iterator, Self
19+
20+
from structlog import get_logger
21+
22+
from hathor.conf.settings import HathorSettings
23+
from hathor.daa import DifficultyAdjustmentAlgorithm
24+
from hathor.dag_builder.artifacts import DAGArtifacts
25+
from hathor.dag_builder.tokenizer import Token, TokenType
26+
from hathor.dag_builder.types import (
27+
AttributeType,
28+
DAGInput,
29+
DAGNode,
30+
DAGNodeType,
31+
DAGOutput,
32+
VertexResolverType,
33+
WalletFactoryType,
34+
)
35+
from hathor.wallet import BaseWallet
36+
37+
logger = get_logger()
38+
39+
40+
class DAGBuilder:
41+
def __init__(
42+
self,
43+
settings: HathorSettings,
44+
daa: DifficultyAdjustmentAlgorithm,
45+
genesis_wallet: BaseWallet,
46+
wallet_factory: WalletFactoryType,
47+
vertex_resolver: VertexResolverType,
48+
) -> None:
49+
from hathor.dag_builder.default_filler import DefaultFiller
50+
from hathor.dag_builder.tokenizer import tokenize
51+
from hathor.dag_builder.vertex_exporter import VertexExporter
52+
53+
self.log = logger.new()
54+
55+
self._nodes: dict[str, DAGNode] = {}
56+
self._tokenize = tokenize
57+
self._filler = DefaultFiller(self, settings, daa)
58+
self._exporter = VertexExporter(
59+
builder=self,
60+
settings=settings,
61+
daa=daa,
62+
genesis_wallet=genesis_wallet,
63+
wallet_factory=wallet_factory,
64+
vertex_resolver=vertex_resolver,
65+
)
66+
67+
def parse_tokens(self, tokens: Iterator[Token]) -> None:
68+
for parts in tokens:
69+
match parts:
70+
case (TokenType.PARENT, (_from, _to)):
71+
self.add_parent_edge(_from, _to)
72+
73+
case (TokenType.SPEND, (_from, _to, _txout_index)):
74+
self.add_spending_edge(_from, _to, _txout_index)
75+
76+
case (TokenType.ATTRIBUTE, (name, key, value)):
77+
self.add_attribute(name, key, value)
78+
79+
case (TokenType.ORDER_BEFORE, (_from, _to)):
80+
self.add_deps(_from, _to)
81+
82+
case (TokenType.OUTPUT, (name, index, amount, token, attrs)):
83+
self.set_output(name, index, amount, token, attrs)
84+
85+
case (TokenType.BLOCKCHAIN, (name, first_parent, begin_index, end_index)):
86+
self.add_blockchain(name, first_parent, begin_index, end_index)
87+
88+
case _:
89+
raise NotImplementedError(parts)
90+
91+
def _get_node(self, name: str) -> DAGNode:
92+
return self._nodes[name]
93+
94+
def _get_or_create_node(self, name: str, *, default_type: DAGNodeType = DAGNodeType.Unknown) -> DAGNode:
95+
if name not in self._nodes:
96+
node = DAGNode(name=name, type=default_type)
97+
self._nodes[name] = node
98+
else:
99+
node = self._nodes[name]
100+
if node.type is DAGNodeType.Unknown:
101+
node.type = default_type
102+
else:
103+
if default_type != DAGNodeType.Unknown:
104+
assert node.type is default_type, f'{node.type} != {default_type}'
105+
return node
106+
107+
def add_deps(self, _from: str, _to: str) -> Self:
108+
from_node = self._get_or_create_node(_from)
109+
self._get_or_create_node(_to)
110+
from_node.deps.add(_to)
111+
return self
112+
113+
def add_blockchain(self, prefix: str, first_parent: str | None, first_index: int, last_index: int) -> Self:
114+
prev = first_parent
115+
for i in range(first_index, last_index + 1):
116+
name = f'{prefix}{i}'
117+
self._get_or_create_node(name, default_type=DAGNodeType.Block)
118+
if prev is not None:
119+
self.add_parent_edge(name, prev)
120+
prev = name
121+
return self
122+
123+
def add_parent_edge(self, _from: str, _to: str) -> Self:
124+
self._get_or_create_node(_to)
125+
from_node = self._get_or_create_node(_from)
126+
from_node.parents.add(_to)
127+
return self
128+
129+
def add_spending_edge(self, _from: str, _to: str, _txout_index: int) -> Self:
130+
self._get_or_create_node(_to)
131+
from_node = self._get_or_create_node(_from)
132+
from_node.inputs.add(DAGInput(_to, _txout_index))
133+
return self
134+
135+
def set_output(self, name: str, index: int, amount: int, token: str, attrs: AttributeType) -> Self:
136+
node = self._get_or_create_node(name)
137+
if len(node.outputs) <= index:
138+
node.outputs.extend([None] * (index - len(node.outputs) + 1))
139+
node.outputs[index] = DAGOutput(amount, token, attrs)
140+
if token != 'HTR':
141+
self._get_or_create_node(token, default_type=DAGNodeType.Token)
142+
node.deps.add(token)
143+
return self
144+
145+
def add_attribute(self, name: str, key: str, value: str) -> Self:
146+
node = self._get_or_create_node(name)
147+
if key == 'type':
148+
node.type = DAGNodeType(value)
149+
else:
150+
node.attrs[key] = value
151+
return self
152+
153+
def topological_sorting(self) -> Iterator[DAGNode]:
154+
direct_deps: dict[str, set[str]] = {}
155+
rev_deps: dict[str, set[str]] = defaultdict(set)
156+
seen: set[str] = set()
157+
candidates: list[str] = []
158+
for name, node in self._nodes.items():
159+
assert name == node.name
160+
deps = set(node.get_all_dependencies())
161+
assert name not in direct_deps
162+
direct_deps[name] = deps
163+
for x in deps:
164+
rev_deps[x].add(name)
165+
if len(deps) == 0:
166+
candidates.append(name)
167+
168+
for _ in range(len(self._nodes)):
169+
if len(candidates) == 0:
170+
self.log('fail because there is at least one cycle in the dependencies',
171+
direct_deps=direct_deps,
172+
rev_deps=rev_deps,
173+
seen=seen,
174+
not_seen=set(self._nodes.keys()) - seen,
175+
nodes=self._nodes)
176+
raise RuntimeError('there is at least one cycle')
177+
name = candidates.pop()
178+
assert name not in seen
179+
seen.add(name)
180+
for d in rev_deps[name]:
181+
direct_deps[d].remove(name)
182+
if len(direct_deps[d]) == 0:
183+
candidates.append(d)
184+
del direct_deps[d]
185+
node = self._get_node(name)
186+
yield node
187+
188+
def build(self) -> DAGArtifacts:
189+
self._filler.run()
190+
return DAGArtifacts(self._exporter.export())
191+
192+
def build_from_str(self, content: str) -> DAGArtifacts:
193+
self.parse_tokens(self._tokenize(content))
194+
return self.build()

hathor/dag_builder/cli.py

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from hathor.dag_builder.builder import DAGBuilder
16+
17+
18+
def main(filename: str, genesis_seed: str) -> None:
19+
from hathor.reactor import initialize_global_reactor
20+
21+
# reactor
22+
_ = initialize_global_reactor(use_asyncio_reactor=False)
23+
24+
from hathor.conf.get_settings import get_global_settings
25+
from hathor.daa import DifficultyAdjustmentAlgorithm
26+
from hathor.wallet import HDWallet
27+
settings = get_global_settings()
28+
29+
def wallet_factory(words=None):
30+
if words is None:
31+
words = ('bind daring above film health blush during tiny neck slight clown salmon '
32+
'wine brown good setup later omit jaguar tourist rescue flip pet salute')
33+
hd = HDWallet(words=words)
34+
hd._manually_initialize()
35+
return hd
36+
37+
genesis_wallet = wallet_factory(genesis_seed)
38+
daa = DifficultyAdjustmentAlgorithm(settings=settings)
39+
40+
builder = DAGBuilder(
41+
settings=settings,
42+
daa=daa,
43+
genesis_wallet=genesis_wallet,
44+
wallet_factory=wallet_factory,
45+
vertex_resolver=lambda x: None,
46+
)
47+
48+
fp = open(filename, 'r')
49+
content = fp.read()
50+
artifacts = builder.build_from_str(content)
51+
52+
for node, vertex in artifacts._list:
53+
print('//', node)
54+
print('//', repr(vertex))
55+
print('//', node.name)
56+
print(bytes(vertex).hex())
57+
print()
58+
59+
60+
if __name__ == '__main__':
61+
import os
62+
import sys
63+
if 'HATHOR_CONFIG_YAML' not in os.environ:
64+
os.environ['HATHOR_CONFIG_YAML'] = './hathor/conf/testnet.yml'
65+
genesis_seed = os.environ['GENESIS_SEED']
66+
main(sys.argv[1], genesis_seed)

0 commit comments

Comments
 (0)