Skip to content

Commit dafb129

Browse files
msbroglijansegre
andcommitted
feat(nano): Add address balance in global state
Co-authored-by: Jan Segre <jan@hathor.network>
1 parent 9d036e3 commit dafb129

31 files changed

+1201
-34
lines changed

hathor/consensus/consensus.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,9 @@ def _feature_activation_rules(self, tx: Transaction, new_best_block: Block) -> b
450450
case Feature.FEE_TOKENS:
451451
if not self._fee_tokens_activation_rule(tx, is_active):
452452
return False
453+
case Feature.TRANSFER_HEADER:
454+
if not self._transfer_headers_activation_rule(tx, is_active):
455+
return False
453456
case Feature.COUNT_CHECKDATASIG_OP:
454457
if not self._checkdatasig_count_rule(tx):
455458
return False
@@ -522,6 +525,18 @@ def _checkdatasig_count_rule(self, tx: Transaction) -> bool:
522525
return False
523526
return True
524527

528+
def _transfer_headers_activation_rule(self, tx: Transaction, is_active: bool) -> bool:
529+
"""
530+
Check whether a tx became invalid because the reorg changed the transfer-headers feature activation state.
531+
"""
532+
if is_active:
533+
return True
534+
535+
if tx.has_transfer_header():
536+
return False
537+
538+
return True
539+
525540
def _opcodes_v2_activation_rule(self, tx: Transaction, new_best_block: Block) -> bool:
526541
"""Check whether a tx became invalid because of the opcodes V2 feature."""
527542
from hathor.verification.nano_header_verifier import NanoHeaderVerifier

hathor/dag_builder/builder.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
NC_WITHDRAWAL_KEY = 'nc_withdrawal'
4848
TOKEN_VERSION_KEY = 'token_version'
4949
FEE_KEY = 'fee'
50+
NC_TRANSFER_INPUT_KEY = 'nc_transfer_input'
51+
NC_TRANSFER_OUTPUT_KEY = 'nc_transfer_output'
5052

5153

5254
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: 74 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,15 @@
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 FEE_KEY, NC_DEPOSIT_KEY, NC_WITHDRAWAL_KEY, DAGBuilder, DAGNode
27+
from hathor.dag_builder.builder import (
28+
FEE_KEY,
29+
NC_DEPOSIT_KEY,
30+
NC_TRANSFER_INPUT_KEY,
31+
NC_TRANSFER_OUTPUT_KEY,
32+
NC_WITHDRAWAL_KEY,
33+
DAGBuilder,
34+
DAGNode,
35+
)
2736
from hathor.dag_builder.types import DAGNodeType, VertexResolverType, WalletFactoryType
2837
from hathor.dag_builder.utils import get_literal, is_literal
2938
from hathor.nanocontracts import Blueprint, OnChainBlueprint
@@ -42,6 +51,7 @@
4251
from hathor.transaction.base_transaction import TxInput, TxOutput
4352
from hathor.transaction.headers.fee_header import FeeHeader, FeeHeaderEntry
4453
from hathor.transaction.headers.nano_header import ADDRESS_LEN_BYTES
54+
from hathor.transaction.headers.transfer_header import TxTransferInput, TxTransferOutput
4555
from hathor.transaction.scripts.p2pkh import P2PKH
4656
from hathor.transaction.token_creation_tx import TokenCreationTransaction
4757
from hathor.wallet import BaseWallet, HDWallet, KeyPair
@@ -330,6 +340,69 @@ def add_headers_if_needed(self, node: DAGNode, vertex: BaseTransaction) -> None:
330340
"""Add the configured headers."""
331341
self.add_nano_header_if_needed(node, vertex)
332342
self.add_fee_header_if_needed(node, vertex)
343+
self.add_transfer_header_if_needed(node, vertex)
344+
345+
def _get_token_index(self, token_name: str, vertex: Transaction) -> int:
346+
token_index = 0
347+
if token_name != 'HTR':
348+
token_creation_tx = self._vertices[token_name]
349+
if token_creation_tx.hash not in vertex.tokens:
350+
vertex.tokens.append(token_creation_tx.hash)
351+
token_index = 1 + vertex.tokens.index(token_creation_tx.hash)
352+
return token_index
353+
354+
def add_transfer_header_if_needed(self, node: DAGNode, vertex: BaseTransaction) -> None:
355+
inputs = node.get_attr_list(NC_TRANSFER_INPUT_KEY, default=[])
356+
outputs = node.get_attr_list(NC_TRANSFER_OUTPUT_KEY, default=[])
357+
358+
if not inputs and not outputs:
359+
return
360+
361+
if not isinstance(vertex, Transaction):
362+
raise TypeError('TransferHeader is only supported for transactions')
363+
364+
transfer_inputs: list[TxTransferInput] = []
365+
for wallet_name, token_name, amount in inputs:
366+
wallet = self.get_wallet(wallet_name)
367+
assert isinstance(wallet, HDWallet)
368+
privkey = wallet.get_key_at_index(0)
369+
pubkey_bytes = privkey.sec()
370+
address = get_address_from_public_key_bytes(pubkey_bytes)
371+
token_index = self._get_token_index(token_name, vertex)
372+
373+
sighash_data = vertex.get_sighash_all_data()
374+
sighash_data_hash = hashlib.sha256(sighash_data).digest()
375+
signature = privkey.sign(sighash_data_hash)
376+
script = P2PKH.create_input_data(public_key_bytes=pubkey_bytes, signature=signature)
377+
378+
transfer_inputs.append(TxTransferInput(
379+
address=address,
380+
amount=amount,
381+
token_index=token_index,
382+
script=script,
383+
))
384+
385+
transfer_outputs: list[TxTransferOutput] = []
386+
for wallet_name, token_name, amount in outputs:
387+
wallet = self.get_wallet(wallet_name)
388+
assert isinstance(wallet, HDWallet)
389+
privkey = wallet.get_key_at_index(0)
390+
pubkey_bytes = privkey.sec()
391+
address = get_address_from_public_key_bytes(pubkey_bytes)
392+
token_index = self._get_token_index(token_name, vertex)
393+
transfer_outputs.append(TxTransferOutput(
394+
address=address,
395+
amount=amount,
396+
token_index=token_index,
397+
))
398+
399+
from hathor.transaction.headers import TransferHeader
400+
transfer_header = TransferHeader(
401+
tx=vertex,
402+
inputs=transfer_inputs,
403+
outputs=transfer_outputs,
404+
)
405+
vertex.headers.append(transfer_header)
333406

334407
def add_nano_header_if_needed(self, node: DAGNode, vertex: BaseTransaction) -> None:
335408
if 'nc_id' not in node.attrs:

hathor/feature_activation/feature.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ class Feature(StrEnum):
3232
COUNT_CHECKDATASIG_OP = 'COUNT_CHECKDATASIG_OP'
3333
NANO_CONTRACTS = 'NANO_CONTRACTS'
3434
FEE_TOKENS = 'FEE_TOKENS'
35+
TRANSFER_HEADER = 'TRANSFER_HEADER'
3536
OPCODES_V2 = 'OPCODES_V2'

hathor/feature_activation/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,18 @@ class Features:
3636
nanocontracts: bool
3737
fee_tokens: bool
3838
opcodes_version: OpcodesVersion
39+
transfer_headers: bool = False
3940

4041
@staticmethod
4142
def from_vertex(*, settings: HathorSettings, feature_service: FeatureService, vertex: Vertex) -> Features:
42-
"""Return whether the Nano Contracts feature is active according to the provided settings and vertex."""
43+
"""Return active/inactive state for every runtime feature according to the provided settings and vertex."""
4344
from hathorlib.conf.settings import FeatureSetting
4445
feature_states = feature_service.get_feature_states(vertex=vertex)
4546
feature_settings = {
4647
Feature.COUNT_CHECKDATASIG_OP: FeatureSetting.FEATURE_ACTIVATION,
4748
Feature.NANO_CONTRACTS: settings.ENABLE_NANO_CONTRACTS,
4849
Feature.FEE_TOKENS: settings.ENABLE_FEE_BASED_TOKENS,
50+
Feature.TRANSFER_HEADER: settings.ENABLE_TRANSFER_HEADER,
4951
Feature.OPCODES_V2: settings.ENABLE_OPCODES_V2,
5052
}
5153

@@ -61,6 +63,7 @@ def from_vertex(*, settings: HathorSettings, feature_service: FeatureService, ve
6163
nanocontracts=feature_is_active[Feature.NANO_CONTRACTS],
6264
fee_tokens=feature_is_active[Feature.FEE_TOKENS],
6365
opcodes_version=opcodes_version,
66+
transfer_headers=feature_is_active[Feature.TRANSFER_HEADER],
6467
)
6568

6669

hathor/nanocontracts/blueprint_env.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from hathor.nanocontracts.rng import NanoRNG
2828
from hathor.nanocontracts.runner import Runner
2929
from hathor.nanocontracts.storage import NCContractStorage
30+
from hathor.nanocontracts.types import Address
3031

3132

3233
NCAttrCache: TypeAlias = dict[bytes, Any] | None
@@ -266,3 +267,7 @@ def setup_new_contract(
266267
actions=actions,
267268
fees=fees or (),
268269
)
270+
271+
def transfer_to_address(self, address: Address, amount: Amount, token: TokenUid) -> None:
272+
"""Transfer a given amount of token to an address balance."""
273+
self.__runner.syscall_transfer_to_address(address, amount, token)

hathor/nanocontracts/execution/block_executor.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818

1919
import hashlib
2020
import traceback
21+
from collections import defaultdict
2122
from dataclasses import dataclass
2223
from typing import TYPE_CHECKING, Iterator
2324

24-
from hathor.nanocontracts.exception import NCFail
25+
from hathor.nanocontracts.exception import NCFail, NCInsufficientFunds
2526
from hathor.transaction import Block, Transaction
2627
from hathor.transaction.exceptions import TokenNotFound
2728
from hathor.transaction.nc_execution_state import NCExecutionState
@@ -32,6 +33,7 @@
3233
from hathor.nanocontracts.runner.runner import RunnerFactory
3334
from hathor.nanocontracts.sorter.types import NCSorterCallable
3435
from hathor.nanocontracts.storage import NCBlockStorage, NCStorageFactory
36+
from hathor.nanocontracts.types import Address, TokenUid
3537

3638

3739
# Transaction execution result types (also used as block execution effects)
@@ -233,19 +235,23 @@ def execute_transaction(
233235
block_storage.set_address_seqnum(nc_address, nc_header.nc_seqnum)
234236
return NCTxExecutionSkipped(tx=tx)
235237

238+
transfer_header_diffs = self._get_transfer_header_diffs(tx)
236239
runner = self._runner_factory.create(
237240
block_storage=block_storage,
238241
seed=rng_seed,
239242
)
240243

241244
try:
245+
self._verify_transfer_header_balances(block_storage, transfer_header_diffs)
242246
runner.execute_from_tx(tx)
243247

244248
# after the execution we have the latest state in the storage
245249
# and at this point no tokens pending creation
246250
self._verify_sum_after_execution(tx, block_storage)
251+
self._apply_transfer_header_diffs(block_storage, transfer_header_diffs)
247252

248253
except NCFail as e:
254+
self._ensure_runner_has_last_call_info(tx, runner)
249255
return NCTxExecutionFailure(
250256
tx=tx,
251257
runner=runner,
@@ -255,6 +261,65 @@ def execute_transaction(
255261

256262
return NCTxExecutionSuccess(tx=tx, runner=runner)
257263

264+
def _get_transfer_header_diffs(self, tx: Transaction) -> dict[tuple['Address', 'TokenUid'], int]:
265+
from hathor.nanocontracts.types import Address, TokenUid
266+
267+
diffs: defaultdict[tuple[Address, TokenUid], int] = defaultdict(int)
268+
if not tx.has_transfer_header():
269+
return dict(diffs)
270+
271+
transfer_header = tx.get_transfer_header()
272+
for txin in transfer_header.inputs:
273+
token_uid = TokenUid(tx.get_token_uid(txin.token_index))
274+
diffs[(Address(txin.address), token_uid)] -= txin.amount
275+
276+
for txout in transfer_header.outputs:
277+
token_uid = TokenUid(tx.get_token_uid(txout.token_index))
278+
diffs[(Address(txout.address), token_uid)] += txout.amount
279+
280+
return dict(diffs)
281+
282+
def _verify_transfer_header_balances(
283+
self,
284+
block_storage: 'NCBlockStorage',
285+
transfer_header_diffs: dict[tuple['Address', 'TokenUid'], int],
286+
) -> None:
287+
for (address, token_uid), diff in transfer_header_diffs.items():
288+
if diff >= 0:
289+
continue
290+
291+
balance = block_storage.get_address_balance(address, token_uid)
292+
if balance + diff < 0:
293+
raise NCInsufficientFunds(
294+
f'insufficient transfer-header balance for address={address.hex()} '
295+
f'token={token_uid.hex()}: available={balance} required={-diff}'
296+
)
297+
298+
def _apply_transfer_header_diffs(
299+
self,
300+
block_storage: 'NCBlockStorage',
301+
transfer_header_diffs: dict[tuple['Address', 'TokenUid'], int],
302+
) -> None:
303+
from hathor.nanocontracts.types import Amount
304+
305+
for (address, token_uid), diff in transfer_header_diffs.items():
306+
if diff == 0:
307+
continue
308+
block_storage.add_address_balance(address, Amount(diff), token_uid)
309+
310+
def _ensure_runner_has_last_call_info(self, tx: Transaction, runner: 'Runner') -> None:
311+
from hathor.nanocontracts.types import ContractId, VertexId
312+
313+
if runner._last_call_info is not None:
314+
return
315+
316+
nano_header = tx.get_nano_header()
317+
if nano_header.is_creating_a_new_contract():
318+
contract_id = ContractId(VertexId(tx.hash))
319+
else:
320+
contract_id = ContractId(VertexId(nano_header.nc_id))
321+
runner._last_call_info = runner._build_call_info(contract_id)
322+
258323
def _verify_sum_after_execution(self, tx: Transaction, block_storage: 'NCBlockStorage') -> None:
259324
"""Verify token sums after execution for dynamically created tokens."""
260325
from hathor.verification.transaction_verifier import TransactionVerifier

hathor/nanocontracts/runner/runner.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
NC_FALLBACK_METHOD,
6666
NC_INITIALIZE_METHOD,
6767
Address,
68+
Amount,
6869
BaseTokenAction,
6970
BlueprintId,
7071
ContractId,
@@ -91,6 +92,7 @@
9192
from hathor.reactor import ReactorProtocol
9293
from hathor.transaction import Transaction
9394
from hathor.transaction.exceptions import InvalidFeeAmount
95+
from hathor.transaction.headers.nano_header import ADDRESS_LEN_BYTES
9496
from hathor.transaction.storage import TransactionStorage
9597
from hathor.transaction.storage.exceptions import TransactionDoesNotExist
9698
from hathor.transaction.token_info import TokenDescription, TokenVersion
@@ -585,6 +587,7 @@ def _validate_balances(self, ctx: Context) -> None:
585587
def _commit_all_changes_to_storage(self) -> None:
586588
"""Commit all change trackers."""
587589
assert self._call_info is not None
590+
588591
for nc_id, change_trackers in self._call_info.change_trackers.items():
589592
assert len(change_trackers) == 1
590593
change_tracker = change_trackers[0]
@@ -1434,6 +1437,24 @@ def forbid_call_on_view(self, name: str) -> None:
14341437
if current_call_record.type == CallType.VIEW:
14351438
raise NCViewMethodError(f'@view method cannot call `syscall.{name}`')
14361439

1440+
@_forbid_syscall_from_view('transfer_to_address')
1441+
def syscall_transfer_to_address(self, address: Address, amount: Amount, token: TokenUid) -> None:
1442+
if amount < 0:
1443+
raise NCInvalidSyscall('amount cannot be negative')
1444+
1445+
if amount == 0:
1446+
# XXX Should we fail?
1447+
return
1448+
1449+
if not isinstance(address, Address) or len(address) != ADDRESS_LEN_BYTES:
1450+
raise NCInvalidSyscall(f'only addresses with {ADDRESS_LEN_BYTES} bytes are allowed')
1451+
1452+
# XXX: this makes sure the token exists
1453+
self._get_token(token)
1454+
1455+
changes_tracker = self.get_current_changes_tracker()
1456+
changes_tracker.add_address_balance(address, amount, token)
1457+
14371458

14381459
class RunnerFactory:
14391460
__slots__ = ('reactor', 'settings', 'tx_storage', 'nc_storage_factory')

0 commit comments

Comments
 (0)