Skip to content

Commit 3851875

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

14 files changed

+1342
-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

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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
19+
from typing_extensions import Self
20+
21+
from structlog import get_logger
22+
23+
from hathor.conf.settings import HathorSettings
24+
from hathor.daa import DifficultyAdjustmentAlgorithm
25+
from hathor.dag_builder.artifacts import DAGArtifacts
26+
from hathor.dag_builder.tokenizer import Token, TokenType
27+
from hathor.dag_builder.types import (
28+
AttributeType,
29+
DAGInput,
30+
DAGNode,
31+
DAGNodeType,
32+
DAGOutput,
33+
VertexResolverType,
34+
WalletFactoryType,
35+
)
36+
from hathor.wallet import BaseWallet
37+
38+
logger = get_logger()
39+
40+
41+
class DAGBuilder:
42+
def __init__(
43+
self,
44+
settings: HathorSettings,
45+
daa: DifficultyAdjustmentAlgorithm,
46+
genesis_wallet: BaseWallet,
47+
wallet_factory: WalletFactoryType,
48+
vertex_resolver: VertexResolverType,
49+
) -> None:
50+
from hathor.dag_builder.default_filler import DefaultFiller
51+
from hathor.dag_builder.tokenizer import tokenize
52+
from hathor.dag_builder.vertex_exporter import VertexExporter
53+
54+
self.log = logger.new()
55+
56+
self._nodes: dict[str, DAGNode] = {}
57+
self._tokenize = tokenize
58+
self._filler = DefaultFiller(self, settings, daa)
59+
self._exporter = VertexExporter(
60+
builder=self,
61+
settings=settings,
62+
daa=daa,
63+
genesis_wallet=genesis_wallet,
64+
wallet_factory=wallet_factory,
65+
vertex_resolver=vertex_resolver,
66+
)
67+
68+
def parse_tokens(self, tokens: Iterator[Token]) -> None:
69+
"""Parse tokens and update the DAG accordingly."""
70+
for parts in tokens:
71+
match parts:
72+
case (TokenType.PARENT, (_from, _to)):
73+
self.add_parent_edge(_from, _to)
74+
75+
case (TokenType.SPEND, (_from, _to, _txout_index)):
76+
self.add_spending_edge(_from, _to, _txout_index)
77+
78+
case (TokenType.ATTRIBUTE, (name, key, value)):
79+
self.add_attribute(name, key, value)
80+
81+
case (TokenType.ORDER_BEFORE, (_from, _to)):
82+
self.add_deps(_from, _to)
83+
84+
case (TokenType.OUTPUT, (name, index, amount, token, attrs)):
85+
self.set_output(name, index, amount, token, attrs)
86+
87+
case (TokenType.BLOCKCHAIN, (name, first_parent, begin_index, end_index)):
88+
self.add_blockchain(name, first_parent, begin_index, end_index)
89+
90+
case _:
91+
raise NotImplementedError(parts)
92+
93+
def _get_node(self, name: str) -> DAGNode:
94+
"""Return a node."""
95+
return self._nodes[name]
96+
97+
def _get_or_create_node(self, name: str, *, default_type: DAGNodeType = DAGNodeType.Unknown) -> DAGNode:
98+
"""Return a node, creating one if needed."""
99+
if name not in self._nodes:
100+
node = DAGNode(name=name, type=default_type)
101+
self._nodes[name] = node
102+
else:
103+
node = self._nodes[name]
104+
if node.type is DAGNodeType.Unknown:
105+
node.type = default_type
106+
else:
107+
if default_type != DAGNodeType.Unknown:
108+
assert node.type is default_type, f'{node.type} != {default_type}'
109+
return node
110+
111+
def add_deps(self, _from: str, _to: str) -> Self:
112+
"""Add a dependency between two nodes. For clarity, `_to` has to be created before `_from`."""
113+
from_node = self._get_or_create_node(_from)
114+
self._get_or_create_node(_to)
115+
from_node.deps.add(_to)
116+
return self
117+
118+
def add_blockchain(self, prefix: str, first_parent: str | None, first_index: int, last_index: int) -> Self:
119+
"""Add a sequence of nodes representing a chain of blocks."""
120+
prev = first_parent
121+
for i in range(first_index, last_index + 1):
122+
name = f'{prefix}{i}'
123+
self._get_or_create_node(name, default_type=DAGNodeType.Block)
124+
if prev is not None:
125+
self.add_parent_edge(name, prev)
126+
prev = name
127+
return self
128+
129+
def add_parent_edge(self, _from: str, _to: str) -> Self:
130+
"""Add a parent edge between two nodes. For clarity, `_to` has to be created befre `_from`."""
131+
self._get_or_create_node(_to)
132+
from_node = self._get_or_create_node(_from)
133+
from_node.parents.add(_to)
134+
return self
135+
136+
def add_spending_edge(self, _from: str, _to: str, _txout_index: int) -> Self:
137+
"""Add a spending edge between two nodes. For clarity, `_to` has to be created before `_from`."""
138+
to_node = self._get_or_create_node(_to)
139+
if len(to_node.outputs) <= _txout_index:
140+
to_node.outputs.extend([None] * (_txout_index - len(to_node.outputs) + 1))
141+
to_node.outputs[_txout_index] = DAGOutput(0, '', {})
142+
from_node = self._get_or_create_node(_from)
143+
from_node.inputs.add(DAGInput(_to, _txout_index))
144+
return self
145+
146+
def set_output(self, name: str, index: int, amount: int, token: str, attrs: AttributeType) -> Self:
147+
"""Set information about an output."""
148+
node = self._get_or_create_node(name)
149+
if len(node.outputs) <= index:
150+
node.outputs.extend([None] * (index - len(node.outputs) + 1))
151+
node.outputs[index] = DAGOutput(amount, token, attrs)
152+
if token != 'HTR':
153+
self._get_or_create_node(token, default_type=DAGNodeType.Token)
154+
node.deps.add(token)
155+
return self
156+
157+
def add_attribute(self, name: str, key: str, value: str) -> Self:
158+
"""Add an attribute to a node."""
159+
node = self._get_or_create_node(name)
160+
if key == 'type':
161+
node.type = DAGNodeType(value)
162+
else:
163+
node.attrs[key] = value
164+
return self
165+
166+
def topological_sorting(self) -> Iterator[DAGNode]:
167+
"""Run a topological sort on the DAG, yielding nodes in an order that respects all dependency constraints."""
168+
direct_deps: dict[str, set[str]] = {}
169+
rev_deps: dict[str, set[str]] = defaultdict(set)
170+
seen: set[str] = set()
171+
candidates: list[str] = []
172+
for name, node in self._nodes.items():
173+
assert name == node.name
174+
deps = set(node.get_all_dependencies())
175+
assert name not in direct_deps
176+
direct_deps[name] = deps
177+
for x in deps:
178+
rev_deps[x].add(name)
179+
if len(deps) == 0:
180+
candidates.append(name)
181+
182+
for _ in range(len(self._nodes)):
183+
if len(candidates) == 0:
184+
self.log('fail because there is at least one cycle in the dependencies',
185+
direct_deps=direct_deps,
186+
rev_deps=rev_deps,
187+
seen=seen,
188+
not_seen=set(self._nodes.keys()) - seen,
189+
nodes=self._nodes)
190+
raise RuntimeError('there is at least one cycle')
191+
name = candidates.pop()
192+
assert name not in seen
193+
seen.add(name)
194+
for d in rev_deps[name]:
195+
direct_deps[d].remove(name)
196+
if len(direct_deps[d]) == 0:
197+
candidates.append(d)
198+
del direct_deps[d]
199+
node = self._get_node(name)
200+
yield node
201+
202+
def build(self) -> DAGArtifacts:
203+
"""Build all the transactions based on the DAG."""
204+
self._filler.run()
205+
return DAGArtifacts(self._exporter.export())
206+
207+
def build_from_str(self, content: str) -> DAGArtifacts:
208+
"""Run build() after creating an initial DAG from a string."""
209+
self.parse_tokens(self._tokenize(content))
210+
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)