Skip to content

Commit 8899037

Browse files
committed
feat(nano): Add address balance in global state
1 parent 297959c commit 8899037

File tree

14 files changed

+452
-17
lines changed

14 files changed

+452
-17
lines changed

hathor/dag_builder/builder.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545

4646
NC_DEPOSIT_KEY = 'nc_deposit'
4747
NC_WITHDRAWAL_KEY = 'nc_withdrawal'
48+
NC_TRANSFER_INPUT_KEY = 'nc_transfer_input'
49+
NC_TRANSFER_OUTPUT_KEY = 'nc_transfer_output'
4850

4951

5052
class DAGBuilder:
@@ -240,6 +242,22 @@ def _add_nc_attribute(self, name: str, key: str, value: str) -> None:
240242
actions.append((token, amount))
241243
node.attrs[key] = actions
242244

245+
elif key == NC_TRANSFER_INPUT_KEY:
246+
transfer_inputs = node.get_attr_list(key, default=[])
247+
token, amount, (wallet,) = parse_amount_token(value)
248+
if amount < 0:
249+
raise SyntaxError(f'unexpected negative amount in `{key}`')
250+
transfer_inputs.append((wallet, token, amount))
251+
node.attrs[key] = transfer_inputs
252+
253+
elif key == NC_TRANSFER_OUTPUT_KEY:
254+
transfer_outputs = node.get_attr_list(key, default=[])
255+
token, amount, (wallet,) = parse_amount_token(value)
256+
if amount < 0:
257+
raise SyntaxError(f'unexpected negative amount in `{key}`')
258+
transfer_outputs.append((wallet, token, amount))
259+
node.attrs[key] = transfer_outputs
260+
243261
else:
244262
node.attrs[key] = value
245263

hathor/dag_builder/vertex_exporter.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import ast
16+
import hashlib
1617
import re
1718
from collections import defaultdict
1819
from types import ModuleType
@@ -23,7 +24,14 @@
2324
from hathor.conf.settings import HathorSettings
2425
from hathor.crypto.util import decode_address, get_address_from_public_key_bytes
2526
from hathor.daa import DifficultyAdjustmentAlgorithm
26-
from hathor.dag_builder.builder import NC_DEPOSIT_KEY, NC_WITHDRAWAL_KEY, DAGBuilder, DAGNode
27+
from hathor.dag_builder.builder import (
28+
NC_DEPOSIT_KEY,
29+
NC_TRANSFER_INPUT_KEY,
30+
NC_TRANSFER_OUTPUT_KEY,
31+
NC_WITHDRAWAL_KEY,
32+
DAGBuilder,
33+
DAGNode,
34+
)
2735
from hathor.dag_builder.types import DAGNodeType, VertexResolverType, WalletFactoryType
2836
from hathor.dag_builder.utils import get_literal, is_literal
2937
from hathor.nanocontracts import Blueprint, OnChainBlueprint
@@ -34,6 +42,7 @@
3442
from hathor.transaction import BaseTransaction, Block, Transaction
3543
from hathor.transaction.base_transaction import TxInput, TxOutput
3644
from hathor.transaction.headers.nano_header import ADDRESS_LEN_BYTES
45+
from hathor.transaction.headers.transfer_header import TxTransferInput, TxTransferOutput
3746
from hathor.transaction.scripts.p2pkh import P2PKH
3847
from hathor.transaction.token_creation_tx import TokenCreationTransaction
3948
from hathor.wallet import BaseWallet, HDWallet, KeyPair
@@ -229,6 +238,7 @@ def create_vertex_token(self, node: DAGNode) -> TokenCreationTransaction:
229238
vertex.token_symbol = node.name
230239
vertex.timestamp = self.get_min_timestamp(node)
231240
self.add_nano_header_if_needed(node, vertex)
241+
self.add_transfer_header_if_needed(node, vertex)
232242
self.sign_all_inputs(vertex, node=node)
233243
if 'weight' in node.attrs:
234244
vertex.weight = float(node.attrs['weight'])
@@ -253,6 +263,7 @@ def create_vertex_block(self, node: DAGNode) -> Block:
253263

254264
blk = Block(parents=parents, outputs=outputs)
255265
self.add_nano_header_if_needed(node, blk)
266+
self.add_transfer_header_if_needed(node, blk)
256267
blk.timestamp = self.get_min_timestamp(node) + self._settings.AVG_TIME_BETWEEN_BLOCKS
257268
blk.get_height = lambda: height # type: ignore[method-assign]
258269
blk.update_hash() # the next call fails is blk.hash is None
@@ -302,6 +313,65 @@ def _get_next_nc_seqnum(self, nc_pubkey: bytes) -> int:
302313
self._next_nc_seqnum[address] = cur + 1
303314
return cur
304315

316+
def _get_token_index(self, token_name: str, node: DAGNode, vertex: BaseTransaction) -> None:
317+
token_index = 0
318+
if token_name != 'HTR':
319+
assert isinstance(vertex, Transaction)
320+
token_creation_tx = self._vertices[token_name]
321+
if token_creation_tx.hash not in vertex.tokens:
322+
vertex.tokens.append(token_creation_tx.hash)
323+
token_index = 1 + vertex.tokens.index(token_creation_tx.hash)
324+
return token_index
325+
326+
def add_transfer_header_if_needed(self, node: DAGNode, vertex: BaseTransaction) -> None:
327+
inputs = node.get_attr_list(NC_TRANSFER_INPUT_KEY)
328+
outputs = node.get_attr_list(NC_TRANSFER_OUTPUT_KEY)
329+
330+
if not inputs and not outputs:
331+
return
332+
333+
transfer_inputs: list[TxTransferInput] = []
334+
for wallet_name, token_name, amount in inputs:
335+
wallet = self.get_wallet(wallet_name)
336+
assert isinstance(wallet, HDWallet)
337+
privkey = wallet.get_key_at_index(0)
338+
pubkey_bytes = privkey.sec()
339+
address = get_address_from_public_key_bytes(pubkey_bytes)
340+
token_index = self._get_token_index(token_name, node, vertex)
341+
342+
sighash_data = vertex.get_sighash_all_data()
343+
sighash_data_hash = hashlib.sha256(sighash_data).digest()
344+
signature = privkey.sign(sighash_data_hash)
345+
script = P2PKH.create_input_data(public_key_bytes=pubkey_bytes, signature=signature)
346+
347+
transfer_inputs.append(TxTransferInput(
348+
address=address,
349+
amount=amount,
350+
token_index=token_index,
351+
script=script,
352+
))
353+
354+
transfer_outputs: list[TxTransferOutput] = []
355+
for wallet_name, token_name, amount in outputs:
356+
wallet = self.get_wallet(wallet_name)
357+
assert isinstance(wallet, HDWallet)
358+
privkey = wallet.get_key_at_index(0)
359+
pubkey_bytes = privkey.sec()
360+
address = get_address_from_public_key_bytes(pubkey_bytes)
361+
token_index = self._get_token_index(token_name, node, vertex)
362+
transfer_outputs.append(TxTransferOutput(
363+
address=address,
364+
amount=amount,
365+
token_index=token_index,
366+
))
367+
368+
from hathor.transaction.headers import TransferHeader
369+
transfer_header = TransferHeader(
370+
inputs=transfer_inputs,
371+
outputs=transfer_outputs,
372+
)
373+
vertex.headers.append(transfer_header)
374+
305375
def add_nano_header_if_needed(self, node: DAGNode, vertex: BaseTransaction) -> None:
306376
if 'nc_id' not in node.attrs:
307377
return
@@ -405,6 +475,7 @@ def create_vertex_on_chain_blueprint(self, node: DAGNode) -> OnChainBlueprint:
405475
assert len(block_parents) == 0
406476
ocb = OnChainBlueprint(parents=txs_parents, inputs=inputs, outputs=outputs, tokens=tokens)
407477
self.add_nano_header_if_needed(node, ocb)
478+
self.add_transfer_header_if_needed(node, ocb)
408479
code_attr = node.get_attr_str('ocb_code')
409480

410481
if is_literal(code_attr):
@@ -453,6 +524,7 @@ def create_vertex_transaction(self, node: DAGNode, *, cls: type[Transaction] = T
453524
tx = cls(parents=txs_parents, inputs=inputs, outputs=outputs, tokens=tokens)
454525
tx.timestamp = self.get_min_timestamp(node)
455526
self.add_nano_header_if_needed(node, tx)
527+
self.add_transfer_header_if_needed(node, tx)
456528
self.sign_all_inputs(tx, node=node)
457529
if 'weight' in node.attrs:
458530
tx.weight = float(node.attrs['weight'])

hathor/nanocontracts/blueprint_env.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from hathor.nanocontracts.nc_exec_logs import NCLogger
2525
from hathor.nanocontracts.rng import NanoRNG
2626
from hathor.nanocontracts.runner import Runner
27-
from hathor.nanocontracts.types import NCArgs
27+
from hathor.nanocontracts.types import Address, NCArgs
2828

2929

3030
NCAttrCache: TypeAlias = dict[bytes, Any] | None
@@ -309,3 +309,7 @@ def get_contract(
309309
"""
310310
from hathor.nanocontracts.contract_accessor import ContractAccessor
311311
return ContractAccessor(runner=self.__runner, contract_id=contract_id, blueprint_id=blueprint_id)
312+
313+
def transfer_to_address(self, address: Address, amount: Amount) -> None:
314+
"""Transfer a given amount to an address balance."""
315+
self.__runner.syscall_transfer_to_address(address, amount)

hathor/nanocontracts/runner/runner.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
NC_FALLBACK_METHOD,
6262
NC_INITIALIZE_METHOD,
6363
Address,
64+
Amount,
6465
BaseTokenAction,
6566
BlueprintId,
6667
ContractId,
@@ -512,6 +513,15 @@ def _validate_balances(self, ctx: Context) -> None:
512513
def _commit_all_changes_to_storage(self) -> None:
513514
"""Commit all change trackers."""
514515
assert self._call_info is not None
516+
517+
transfer_header = tx.get_transfer_header()
518+
for input_ in transfer_header.inputs:
519+
token_uid = tx.get_token_uid(input_.token_index)
520+
self.block_storage.add_address_balance(-input_.amount, token_uid)
521+
for output_ in transfer_header.outputs:
522+
token_uid = tx.get_token_uid(output_.token_index)
523+
self.block_storage.add_address_balance(output_.amount, token_uid)
524+
515525
for nc_id, change_trackers in self._call_info.change_trackers.items():
516526
assert len(change_trackers) == 1
517527
change_tracker = change_trackers[0]
@@ -1105,6 +1115,23 @@ def syscall_change_blueprint(self, blueprint_id: BlueprintId) -> None:
11051115
nc_storage = self.get_current_changes_tracker(last_call_record.contract_id)
11061116
nc_storage.set_blueprint_id(blueprint_id)
11071117

1118+
@_forbid_syscall_from_view('transfer_to_address')
1119+
def syscall_transfer_to_address(self, address: Address, amount: Amount) -> None:
1120+
if amount < 0:
1121+
raise NCInvalidSyscall('amount cannot be negative')
1122+
1123+
if amount == 0:
1124+
# XXX Should we fail?
1125+
return
1126+
1127+
# XXX Should we check for the size to prevent miscalling with a contract id?
1128+
if not isinstance(address, Address):
1129+
raise NCInvalidSyscall('only addresses are allowed')
1130+
1131+
call_record = self.get_current_call_record()
1132+
changes_tracker = self.get_current_changes_tracker(call_record.contract_id)
1133+
changes_tracker.add_address_balance(address, amount)
1134+
11081135

11091136
class RunnerFactory:
11101137
__slots__ = ('reactor', 'settings', 'tx_storage', 'nc_storage_factory')

hathor/nanocontracts/storage/block_storage.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@
2222
from hathor.nanocontracts.storage.contract_storage import NCContractStorage
2323
from hathor.nanocontracts.storage.patricia_trie import NodeId, PatriciaTrie
2424
from hathor.nanocontracts.storage.token_proxy import TokenProxy
25-
from hathor.nanocontracts.types import Address, ContractId, TokenUid
25+
from hathor.nanocontracts.types import Address, Amount, ContractId, TokenUid
2626
from hathor.transaction.headers.nano_header import ADDRESS_SEQNUM_SIZE
2727
from hathor.utils import leb128
2828

2929

3030
class _Tag(Enum):
3131
CONTRACT = b'\0'
3232
TOKEN = b'\1'
33-
ADDRESS = b'\2'
33+
ADDRESS_SEQNUM = b'\2'
34+
ADDRESS_BALANCE = b'\3'
3435

3536

3637
class ContractKey(NamedTuple):
@@ -47,11 +48,18 @@ def __bytes__(self):
4748
return _Tag.TOKEN.value + self.token_id
4849

4950

50-
class AddressKey(NamedTuple):
51+
class AddressSeqnumKey(NamedTuple):
5152
address: Address
5253

5354
def __bytes__(self):
54-
return _Tag.ADDRESS.value + self.address
55+
return _Tag.ADDRESS_SEQNUM.value + self.address
56+
57+
58+
class AddressBalanceKey(NamedTuple):
59+
address: Address
60+
61+
def __bytes__(self):
62+
return _Tag.ADDRESS_BALANCE.value + self.address
5563

5664

5765
class NCBlockStorage:
@@ -142,11 +150,30 @@ def create_token(self, token_id: TokenUid, token_name: str, token_symbol: str) -
142150
token_description_bytes = self._TOKEN_DESCRIPTION_NC_TYPE.to_bytes(token_description)
143151
self._block_trie.update(bytes(key), token_description_bytes)
144152

153+
def get_address_balance(self, address: Address, token_id) -> Amount:
154+
key = AddressBalanceKey(address, token_id)
155+
try:
156+
balance_bytes = self._block_trie.get(bytes(key))
157+
except KeyError:
158+
return Amount(0)
159+
else:
160+
balance, buf = leb128.decode_unsigned(balance_bytes)
161+
assert len(buf) == 0
162+
return balance
163+
164+
def add_address_balance(self, address: Address, amount: Amount, token_id: TokenUid) -> None:
165+
key = AddressBalanceKey(address, token_id)
166+
balance = self.get_address_balance(address, token_id)
167+
balance += amount
168+
assert balance >= 0
169+
balance_bytes = leb128.encode_unsigned(amount)
170+
self._block_trie.update(bytes(key), balance_bytes)
171+
145172
def get_address_seqnum(self, address: Address) -> int:
146173
"""Get the latest seqnum for an address.
147174
148175
For clarity, new transactions must have a GREATER seqnum to be able to be executed."""
149-
key = AddressKey(address)
176+
key = AddressSeqnumKey(address)
150177
try:
151178
seqnum_bytes = self._block_trie.get(bytes(key))
152179
except KeyError:
@@ -161,6 +188,6 @@ def set_address_seqnum(self, address: Address, seqnum: int) -> None:
161188
assert seqnum >= 0
162189
old_seqnum = self.get_address_seqnum(address)
163190
assert seqnum > old_seqnum
164-
key = AddressKey(address)
191+
key = AddressSeqnumKey(address)
165192
seqnum_bytes = leb128.encode_unsigned(seqnum, max_bytes=ADDRESS_SEQNUM_SIZE)
166193
self._block_trie.update(bytes(key), seqnum_bytes)

hathor/nanocontracts/storage/changes_tracker.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import itertools
16+
from collections import defaultdict
1617
from dataclasses import dataclass
1718
from enum import Enum
1819
from types import MappingProxyType
@@ -31,7 +32,7 @@
3132
NCContractStorage,
3233
)
3334
from hathor.nanocontracts.storage.types import _NOT_PROVIDED, DeletedKey, DeletedKeyType
34-
from hathor.nanocontracts.types import BlueprintId, ContractId, TokenUid
35+
from hathor.nanocontracts.types import Address, Amount, BlueprintId, ContractId, TokenUid
3536
from hathor.transaction.token_info import TokenDescription
3637

3738
T = TypeVar('T')
@@ -81,6 +82,7 @@ def __init__(self, nc_id: ContractId, storage: NCContractStorage):
8182
self._balance_diff: dict[BalanceKey, int] = {}
8283
self._authorities_diff: dict[BalanceKey, _NCAuthorityDiff] = {}
8384
self._created_tokens: dict[TokenUid, TokenDescription] = {}
85+
self._transfers: defaultdict[Address, int] = defaultdict(int)
8486
self._blueprint_id: BlueprintId | None = None
8587

8688
self.has_been_commited = False
@@ -102,6 +104,10 @@ def has_token(self, token_id: TokenUid) -> bool:
102104
return True
103105
return self.storage.has_token(token_id)
104106

107+
def add_address_balance(self, address: Address, amount: Amount) -> None:
108+
assert amount >= 0
109+
self._transfers[address] += amount
110+
105111
def get_balance_diff(self) -> MappingProxyType[BalanceKey, int]:
106112
"""Return the balance diff of this change tracker."""
107113
return MappingProxyType(self._balance_diff)
@@ -188,6 +194,9 @@ def commit(self) -> None:
188194
for td in self._created_tokens.values():
189195
self.storage.create_token(TokenUid(td.token_id), td.token_name, td.token_symbol)
190196

197+
for address, amount in self._transfers.items():
198+
self.stoarge.add_address_balance(address, amount)
199+
191200
if self._blueprint_id is not None:
192201
self.storage.set_blueprint_id(self._blueprint_id)
193202

hathor/nanocontracts/storage/contract_storage.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from hathor.nanocontracts.storage.patricia_trie import PatriciaTrie
2828
from hathor.nanocontracts.storage.token_proxy import TokenProxy
2929
from hathor.nanocontracts.storage.types import _NOT_PROVIDED, DeletedKey, DeletedKeyType
30-
from hathor.nanocontracts.types import BlueprintId, TokenUid, VertexId
30+
from hathor.nanocontracts.types import Address, Amount, BlueprintId, TokenUid, VertexId
3131
from hathor.serialization import Deserializer, Serializer
3232

3333
T = TypeVar('T')
@@ -138,7 +138,7 @@ class NCContractStorage:
138138
139139
This implementation works for both memory and rocksdb backends."""
140140

141-
def __init__(self, *, trie: PatriciaTrie, nc_id: VertexId, token_proxy: TokenProxy) -> None:
141+
def __init__(self, *, trie: PatriciaTrie, nc_id: VertexId, block_proxy: TokenProxy) -> None:
142142
# State (balances, metadata and attributes)
143143
self._trie: PatriciaTrie = trie
144144

@@ -148,15 +148,18 @@ def __init__(self, *, trie: PatriciaTrie, nc_id: VertexId, token_proxy: TokenPro
148148
# Flag to check whether any change or commit can be executed.
149149
self.is_locked = False
150150

151-
self._token_proxy = token_proxy
151+
self._block_proxy = block_proxy
152152

153153
def has_token(self, token_id: TokenUid) -> bool:
154154
"""Return True if token_id exists in the current block."""
155-
return self._token_proxy.has_token(token_id)
155+
return self._block_proxy.has_token(token_id)
156156

157157
def create_token(self, token_id: TokenUid, token_name: str, token_symbol: str) -> None:
158158
"""Create a new token in the current block."""
159-
self._token_proxy.create_token(token_id, token_name, token_symbol)
159+
self._block_proxy.create_token(token_id, token_name, token_symbol)
160+
161+
def add_address_balance(self, address: Address, amount: Amount) -> None:
162+
self._block_proxy.add_address_balance(address, amount)
160163

161164
def lock(self) -> None:
162165
"""Lock the storage for changes or commits."""

0 commit comments

Comments
 (0)