Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions hathor/conf/nano_testnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@
NC_ON_CHAIN_BLUEPRINT_ALLOWED_ADDRESSES=[
'WWFiNeWAFSmgtjm4ht2MydwS5GY3kMJsEK',
],
BLUEPRINTS={
bytes.fromhex('3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595'): 'Bet',
},
SOFT_VOIDED_TX_IDS=list(map(bytes.fromhex, [
'0000003dd5802b05f430a1f54304879173550c0944b49d74321bb9125ee727cb',
])),
Expand Down
2 changes: 0 additions & 2 deletions hathor/conf/nano_testnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ ENABLE_NANO_CONTRACTS: true
ENABLE_ON_CHAIN_BLUEPRINTS: true
NC_ON_CHAIN_BLUEPRINT_ALLOWED_ADDRESSES:
- WWFiNeWAFSmgtjm4ht2MydwS5GY3kMJsEK
BLUEPRINTS:
3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595: Bet

SOFT_VOIDED_TX_IDS:
- 0000003dd5802b05f430a1f54304879173550c0944b49d74321bb9125ee727cb
3 changes: 1 addition & 2 deletions hathor/nanocontracts/blueprints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@
from hathor.nanocontracts.blueprint import Blueprint


_blueprints_mapper: dict[str, Type['Blueprint']] = {
}
_blueprints_mapper: dict[str, Type['Blueprint']] = {}
8 changes: 6 additions & 2 deletions hathor/nanocontracts/storage/block_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from enum import Enum
from typing import NamedTuple, Optional

from hathor.nanocontracts.exception import NanoContractDoesNotExist
from hathor.nanocontracts.nc_types.dataclass_nc_type import make_dataclass_nc_type
from hathor.nanocontracts.storage.contract_storage import NCContractStorage
from hathor.nanocontracts.storage.patricia_trie import NodeId, PatriciaTrie
Expand Down Expand Up @@ -102,8 +103,11 @@ def _get_trie(self, root_id: Optional[bytes]) -> 'PatriciaTrie':
return trie

def get_contract_storage(self, contract_id: ContractId) -> NCContractStorage:
nc_root_id = self.get_contract_root_id(contract_id)
trie = self._get_trie(nc_root_id)
try:
nc_root_id = self.get_contract_root_id(contract_id)
trie = self._get_trie(nc_root_id)
except KeyError:
raise NanoContractDoesNotExist(contract_id.hex())
token_proxy = TokenProxy(self)
return NCContractStorage(trie=trie, nc_id=contract_id, token_proxy=token_proxy)

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ addopts = "-n auto"
markers = [
"slow",
]
norecursedirs = ["tests/nanocontracts/test_blueprints"]

[build-system]
requires = ["poetry-core >= 1.3.2", "cython < 0.30"]
Expand Down
2 changes: 2 additions & 0 deletions tests/dag_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from hathor.manager import HathorManager
from hathor.util import Random
from hathor.wallet import HDWallet
from tests.nanocontracts import test_blueprints
from tests.utils import GENESIS_SEED


Expand All @@ -45,4 +46,5 @@ def from_manager(
manager=manager,
genesis_words=genesis_words or GENESIS_SEED,
wallet_factory=wallet_factory or (lambda: TestDAGBuilder.create_random_hd_wallet(manager.rng)),
blueprints_module=blueprints_module or test_blueprints,
)
201 changes: 201 additions & 0 deletions tests/dag_builder/test_dag_builder.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
import pytest

from hathor.nanocontracts import Blueprint, Context, OnChainBlueprint, public
from hathor.nanocontracts.types import NCDepositAction, NCWithdrawalAction, TokenUid
from hathor.nanocontracts.utils import load_builtin_blueprint_for_ocb
from hathor.transaction import Block, Transaction
from hathor.transaction.token_creation_tx import TokenCreationTransaction
from tests import unittest
from tests.dag_builder.builder import TestDAGBuilder
from tests.nanocontracts import test_blueprints


class MyBlueprint(Blueprint):
counter: int

@public
def initialize(self, ctx: Context, initial: int) -> None:
self.counter = initial

@public
def add(self, ctx: Context, value: int) -> int:
self.counter += value
return self.counter

@public
def sub(self, ctx: Context, value: int) -> int:
self.counter -= value
return self.counter


class DAGBuilderTestCase(unittest.TestCase):
Expand Down Expand Up @@ -217,3 +241,180 @@ def test_propagate_with(self) -> None:

artifacts.propagate_with(self.manager)
assert len(list(tx_storage.get_all_transactions())) == 16 # 3 genesis + 10 blocks + dummy + tx1 + tx2

def test_nc_transactions(self) -> None:
blueprint_id = b'x' * 32
self.nc_catalog.blueprints[blueprint_id] = MyBlueprint

artifacts = self.dag_builder.build_from_str(f"""
blockchain genesis a[0..40]
a30 < dummy

tx1.nc_id = "{blueprint_id.hex()}"
tx1.nc_method = initialize(0)

tx2.nc_id = tx1
tx2.nc_method = add(5)
tx2.nc_deposit = 10 HTR
tx2.nc_deposit = 5 TKA

tx3.nc_id = tx1
tx3.nc_method = sub(3)
tx3.nc_deposit = 3 HTR
tx3.nc_withdrawal = 2 TKA

a31 --> tx1
a32 --> tx2
a33 --> tx3
""")

artifacts.propagate_with(self.manager)

tx1 = artifacts.by_name['tx1'].vertex
self.assertIsInstance(tx1, Transaction)
self.assertTrue(tx1.is_nano_contract())

htr_id = TokenUid(b'\0')
tka_id = TokenUid(artifacts.by_name['TKA'].vertex.hash)

tx2 = artifacts.by_name['tx2'].vertex
tx3 = artifacts.by_name['tx3'].vertex

ctx2 = tx2.get_nano_header().get_context()
self.assertEqual(dict(ctx2.actions), {
tka_id: (NCDepositAction(token_uid=tka_id, amount=5),),
htr_id: (NCDepositAction(token_uid=htr_id, amount=10),),
})

ctx3 = tx3.get_nano_header().get_context()
self.assertEqual(dict(ctx3.actions), {
htr_id: (NCDepositAction(token_uid=htr_id, amount=3),),
tka_id: (NCWithdrawalAction(token_uid=tka_id, amount=2),),
})

def test_multiline_literals(self) -> None:
artifacts = self.dag_builder.build_from_str("""
tx.attr1 = ```
test
```
tx.attr2 = ```
if foo:
bar
```
""")
node = artifacts.by_name['tx'].node

# asserting with raw shifted strings to make sure we get the expected output.
assert node.get_required_literal('attr1') == """\
test"""
assert node.get_required_literal('attr2') == """\
if foo:
bar"""

invalid_start_texts = [
"""
tx.attr1 = a```
```
""",
"""
tx.attr1 = ```a
```
""",
"""
tx.attr1 = ```a```
""",
]

for text in invalid_start_texts:
with pytest.raises(SyntaxError) as e:
self.dag_builder.build_from_str(text)
assert str(e.value) == 'invalid multiline string start'

invalid_end_texts = [
"""
tx.attr1 = ```
a```
""",
"""
tx.attr1 = ```
```a
""",
]

for text in invalid_end_texts:
with pytest.raises(SyntaxError) as e:
self.dag_builder.build_from_str(text)
assert str(e.value) == 'invalid multiline string end'

with pytest.raises(SyntaxError) as e:
self.dag_builder.build_from_str("""
tx.attr1 = ```
test
""")
assert str(e.value) == 'unclosed multiline string'

def test_on_chain_blueprints(self) -> None:
bet_code = load_builtin_blueprint_for_ocb('bet.py', 'Bet', test_blueprints)
private_key = unittest.OCB_TEST_PRIVKEY.hex()
password = unittest.OCB_TEST_PASSWORD.hex()
artifacts = self.dag_builder.build_from_str(f"""
blockchain genesis b[1..11]
b10 < dummy

ocb1.ocb_private_key = "{private_key}"
ocb1.ocb_password = "{password}"

ocb2.ocb_private_key = "{private_key}"
ocb2.ocb_password = "{password}"

ocb3.ocb_private_key = "{private_key}"
ocb3.ocb_password = "{password}"

nc1.nc_id = ocb1
nc1.nc_method = initialize("00", "00", 0)

nc2.nc_id = ocb2
nc2.nc_method = initialize(0)

nc3.nc_id = ocb3
nc3.nc_method = initialize()

ocb1 <-- ocb2 <-- ocb3 <-- b11
b11 < nc1 < nc2 < nc3

ocb1.ocb_code = "{bet_code.encode().hex()}"
ocb2.ocb_code = test_blueprint1.py, TestBlueprint1
ocb3.ocb_code = ```
from hathor.nanocontracts import Blueprint
from hathor.nanocontracts.context import Context
from hathor.nanocontracts.types import public
class MyBlueprint(Blueprint):
@public
def initialize(self, ctx: Context) -> None:
pass
__blueprint__ = MyBlueprint
```
""")

artifacts.propagate_with(self.manager)
ocb1, ocb2, ocb3 = artifacts.get_typed_vertices(['ocb1', 'ocb2', 'ocb3'], OnChainBlueprint)
nc1, nc2, nc3 = artifacts.get_typed_vertices(['nc1', 'nc2', 'nc3'], Transaction)

assert nc1.is_nano_contract()
assert nc2.is_nano_contract()
assert nc3.is_nano_contract()

assert ocb1.get_blueprint_class().__name__ == 'Bet'
assert nc1.get_nano_header().nc_id == ocb1.hash
blueprint_class = self.manager.tx_storage.get_blueprint_class(ocb1.hash)
assert blueprint_class.__name__ == 'Bet'

assert ocb2.get_blueprint_class().__name__ == 'TestBlueprint1'
assert nc2.get_nano_header().nc_id == ocb2.hash
blueprint_class = self.manager.tx_storage.get_blueprint_class(ocb2.hash)
assert blueprint_class.__name__ == 'TestBlueprint1'

assert ocb3.get_blueprint_class().__name__ == 'MyBlueprint'
assert nc3.get_nano_header().nc_id == ocb3.hash
blueprint_class = self.manager.tx_storage.get_blueprint_class(ocb3.hash)
assert blueprint_class.__name__ == 'MyBlueprint'
Empty file.
Empty file.
103 changes: 103 additions & 0 deletions tests/nanocontracts/blueprints/unittest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from hathor.conf import HathorSettings
from hathor.crypto.util import decode_address
from hathor.manager import HathorManager
from hathor.nanocontracts import Context
from hathor.nanocontracts.blueprint import Blueprint
from hathor.nanocontracts.nc_exec_logs import NCLogConfig
from hathor.nanocontracts.storage import NCBlockStorage, NCMemoryStorageFactory
from hathor.nanocontracts.storage.backends import MemoryNodeTrieStore
from hathor.nanocontracts.storage.patricia_trie import PatriciaTrie
from hathor.nanocontracts.types import Address, BlueprintId, ContractId, NCAction, TokenUid, VertexId
from hathor.nanocontracts.vertex_data import VertexData
from hathor.transaction import BaseTransaction, Transaction
from hathor.util import not_none
from hathor.wallet import KeyPair
from tests import unittest
from tests.nanocontracts.utils import TestRunner

settings = HathorSettings()


class BlueprintTestCase(unittest.TestCase):
use_memory_storage = True

def setUp(self):
super().setUp()
self.manager = self.build_manager()
self.rng = self.manager.rng
self.wallet = self.manager.wallet
self.reactor = self.manager.reactor
self.nc_catalog = self.manager.tx_storage.nc_catalog

self.htr_token_uid = settings.HATHOR_TOKEN_UID
self.runner = self.build_runner()
self.now = int(self.reactor.seconds())

self._token_index = 1

def build_manager(self) -> HathorManager:
"""Create a HathorManager instance."""
return self.create_peer('testnet', nc_indices=True, nc_log_config=NCLogConfig.FAILED, wallet_index=True)

def register_blueprint_class(self, blueprint_id: BlueprintId, blueprint_class: type[Blueprint]) -> None:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of receiving a mandatory blueprint_id, we could generate one, use it, and return it. Most tests won't need to pass this argument.

"""Register a blueprint class with a given id, allowing contracts to be created from it."""
assert blueprint_id not in self.nc_catalog.blueprints
self.nc_catalog.blueprints[blueprint_id] = blueprint_class

def build_runner(self) -> TestRunner:
"""Create a Runner instance."""
nc_storage_factory = NCMemoryStorageFactory()
store = MemoryNodeTrieStore()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use RocksDB?

block_trie = PatriciaTrie(store)
block_storage = NCBlockStorage(block_trie)
return TestRunner(
self.manager.tx_storage, nc_storage_factory, block_storage, settings=self._settings, reactor=self.reactor
)

def gen_random_token_uid(self) -> TokenUid:
"""Generate a random token UID (32 bytes)."""
token = self._token_index.to_bytes(32, byteorder='big', signed=False)
self._token_index += 1
return TokenUid(token)

def gen_random_address(self) -> Address:
"""Generate a random wallet address."""
address, _ = self.gen_random_address_with_key()
return address

def gen_random_address_with_key(self) -> tuple[Address, KeyPair]:
"""Generate a random wallet address with its key."""
password = self.rng.randbytes(12)
key = KeyPair.create(password)
address_b58 = key.address
address_bytes = decode_address(not_none(address_b58))
return Address(address_bytes), key

def gen_random_contract_id(self) -> ContractId:
"""Generate a random contract id."""
return ContractId(VertexId(self.rng.randbytes(32)))

def gen_random_blueprint_id(self) -> BlueprintId:
"""Generate a random contract id."""
return BlueprintId(self.rng.randbytes(32))

def get_genesis_tx(self) -> Transaction:
"""Return a genesis transaction."""
genesis = self.manager.tx_storage.get_all_genesis()
tx = list(tx for tx in genesis if isinstance(tx, Transaction))[0]
return tx

def create_context(
self,
actions: list[NCAction] | None = None,
vertex: BaseTransaction | VertexData | None = None,
address: Address | None = None,
timestamp: int | None = None,
) -> Context:
"""Create a Context instance with optional values or defaults."""
return Context(
actions=actions if actions is not None else [],
vertex=vertex or self.get_genesis_tx(),
address=address or self.gen_random_address(),
timestamp=timestamp or self.now,
)
Empty file.
Loading
Loading