Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
105 changes: 69 additions & 36 deletions hathor/nanocontracts/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
from hathor.crypto.util import get_address_b58_from_bytes
from hathor.nanocontracts.exception import NCFail, NCInvalidContext
from hathor.nanocontracts.types import Address, CallerId, ContractId, NCAction, TokenUid
from hathor.nanocontracts.vertex_data import VertexData
from hathor.nanocontracts.vertex_data import BlockData, VertexData
from hathor.transaction.exceptions import TxValidationError

if TYPE_CHECKING:
from hathor.transaction import BaseTransaction
from hathor.transaction import Vertex

_EMPTY_MAP: MappingProxyType[TokenUid, tuple[NCAction, ...]] = MappingProxyType({})

Expand All @@ -39,53 +39,91 @@ class Context:
Deposits and withdrawals are grouped by token. Note that it is impossible
to have both a deposit and a withdrawal for the same token.
"""
__slots__ = ('__actions', '__caller_id', '__vertex', '__timestamp', '__all_actions__')
__actions: MappingProxyType[TokenUid, tuple[NCAction, ...]]
__slots__ = ('__actions', '__caller_id', '__vertex', '__block', '__all_actions__')
__caller_id: CallerId
__vertex: VertexData
__timestamp: int
__block: BlockData | None
__actions: MappingProxyType[TokenUid, tuple[NCAction, ...]]

def __init__(
self,
actions: Sequence[NCAction],
vertex: BaseTransaction | VertexData,
@staticmethod
def __group_actions__(actions: Sequence[NCAction]) -> MappingProxyType[TokenUid, tuple[NCAction, ...]]:
actions_map: defaultdict[TokenUid, tuple[NCAction, ...]] = defaultdict(tuple)
for action in actions:
actions_map[action.token_uid] = (*actions_map[action.token_uid], action)
return MappingProxyType(actions_map)

@classmethod
def create_from_vertex(
cls,
*,
caller_id: CallerId,
timestamp: int,
) -> None:
vertex: Vertex,
actions: Sequence[NCAction],
) -> Context:
# Dict of action where the key is the token_uid.
# If empty, it is a method call without any actions.
actions_map: MappingProxyType[TokenUid, tuple[NCAction, ...]]
if not actions:
self.__actions = _EMPTY_MAP
actions_map = _EMPTY_MAP
else:
from hathor.verification.nano_header_verifier import NanoHeaderVerifier
try:
NanoHeaderVerifier.verify_action_list(actions)
except TxValidationError as e:
raise NCInvalidContext('invalid nano context') from e

actions_map: defaultdict[TokenUid, tuple[NCAction, ...]] = defaultdict(tuple)
for action in actions:
actions_map[action.token_uid] = (*actions_map[action.token_uid], action)
self.__actions = MappingProxyType(actions_map)
actions_map = cls.__group_actions__(actions)

vertex_data = VertexData.create_from_vertex(vertex)
vertex_meta = vertex.get_metadata()

block_data: BlockData | None = None
if vertex_meta.first_block is not None:
# The context is also created when getting the tx's `to_json` for example,
# and in this case it might not be confirmed yet.
assert vertex.storage is not None
block = vertex.storage.get_block(vertex_meta.first_block)
block_data = BlockData.create_from_block(block)

return Context(
caller_id=caller_id,
vertex_data=vertex_data,
block_data=block_data,
actions=actions_map,
)

def __init__(
self,
*,
caller_id: CallerId,
vertex_data: VertexData,
block_data: BlockData | None,
actions: MappingProxyType[TokenUid, tuple[NCAction, ...]],
) -> None:
# Dict of action where the key is the token_uid.
# If empty, it is a method call without any actions.
self.__actions = actions

self.__all_actions__: tuple[NCAction, ...] = tuple(chain(*self.__actions.values()))

# Vertex calling the method.
if isinstance(vertex, VertexData):
self.__vertex = vertex
else:
self.__vertex = VertexData.create_from_vertex(vertex)
self.__vertex = vertex_data

# Block executing the vertex.
self.__block = block_data

# Address calling the method.
self.__caller_id = caller_id

# Timestamp of the first block confirming tx.
self.__timestamp = timestamp

@property
def vertex(self) -> VertexData:
return self.__vertex

@property
def block(self) -> BlockData:
assert self.__block is not None
return self.__block

@property
def caller_id(self) -> CallerId:
"""Get the caller ID which can be either an Address or a ContractId."""
Expand All @@ -98,7 +136,7 @@ def get_caller_address(self) -> Address | None:
return self.caller_id
case ContractId():
return None
case _:
case _: # pragma: no cover
assert_never(self.caller_id)

def get_caller_contract_id(self) -> ContractId | None:
Expand All @@ -108,22 +146,18 @@ def get_caller_contract_id(self) -> ContractId | None:
return None
case ContractId():
return self.caller_id
case _:
case _: # pragma: no cover
assert_never(self.caller_id)

@property
def timestamp(self) -> int:
return self.__timestamp

Comment thread
jansegre marked this conversation as resolved.
@property
def actions(self) -> MappingProxyType[TokenUid, tuple[NCAction, ...]]:
"""Get a mapping of actions per token."""
return self.__actions

@property
def actions_list(self) -> list[NCAction]:
def actions_list(self) -> Sequence[NCAction]:
"""Get a list of all actions."""
return list(self.__all_actions__)
return tuple(self.__all_actions__)

def get_single_action(self, token_uid: TokenUid) -> NCAction:
"""Get exactly one action for the provided token, and fail otherwise."""
Expand All @@ -135,10 +169,10 @@ def get_single_action(self, token_uid: TokenUid) -> NCAction:
def copy(self) -> Context:
"""Return a copy of the context."""
return Context(
actions=list(self.__all_actions__),
vertex=self.vertex,
caller_id=self.caller_id,
timestamp=self.timestamp,
vertex_data=self.vertex,
block_data=self.block, # We only copy during execution, so we know the block must not be `None`.
actions=self.actions,
)

def to_json(self) -> dict[str, Any]:
Expand All @@ -149,13 +183,12 @@ def to_json(self) -> dict[str, Any]:
caller_id = get_address_b58_from_bytes(self.caller_id)
case ContractId():
caller_id = self.caller_id.hex()
case _:
case _: # pragma: no cover
assert_never(self.caller_id)

return {
'actions': [action.to_json() for action in self.__all_actions__],
'caller_id': caller_id,
'timestamp': self.timestamp,
Comment thread
jansegre marked this conversation as resolved.
# XXX: Deprecated attribute
'address': caller_id,
}
8 changes: 4 additions & 4 deletions hathor/nanocontracts/runner/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def execute_from_tx(self, tx: Transaction) -> None:
assert vertex_metadata.first_block is not None, 'execute must only be called after first_block is updated'

context = nano_header.get_context()
assert context.vertex.block.hash == vertex_metadata.first_block
assert context.block.hash == vertex_metadata.first_block

nc_args = NCRawArgs(nano_header.nc_args_bytes)
if nano_header.is_creating_a_new_contract():
Expand Down Expand Up @@ -413,10 +413,10 @@ def _unsafe_call_another_contract_public_method(

# Call the other contract method.
ctx = Context(
actions=actions,
vertex=first_ctx.vertex,
caller_id=last_call_record.contract_id,
timestamp=first_ctx.timestamp,
vertex_data=first_ctx.vertex,
Comment thread
msbrogli marked this conversation as resolved.
block_data=first_ctx.block,
actions=Context.__group_actions__(actions),
)
return self._execute_public_method_call(
contract_id=contract_id,
Expand Down
11 changes: 0 additions & 11 deletions hathor/nanocontracts/vertex_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ class VertexData:
outputs: tuple[TxOutputData, ...]
tokens: tuple[TokenUid, ...]
parents: tuple[VertexId, ...]
block: BlockData
headers: tuple[HeaderData, ...]

@classmethod
Expand All @@ -72,15 +71,6 @@ def create_from_vertex(cls, vertex: BaseTransaction) -> Self:
outputs = tuple(TxOutputData.create_from_txout(txout) for txout in vertex.outputs)
parents = tuple(vertex.parents)
tokens: tuple[TokenUid, ...] = tuple()
vertex_meta = vertex.get_metadata()
if vertex_meta.first_block is not None:
assert vertex.storage is not None
assert vertex_meta.first_block is not None
block = vertex.storage.get_block(vertex_meta.first_block)
block_data = BlockData.create_from_block(block)
else:
# XXX: use dummy data instead
block_data = BlockData(hash=VertexId(b''), timestamp=0, height=0)

assert isinstance(vertex, Transaction)
headers_data: list[HeaderData] = []
Expand All @@ -106,7 +96,6 @@ def create_from_vertex(cls, vertex: BaseTransaction) -> Self:
outputs=outputs,
tokens=tokens,
parents=parents,
block=block_data,
headers=tuple(headers_data),
)

Expand Down
20 changes: 3 additions & 17 deletions hathor/transaction/headers/nano_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,24 +307,10 @@ def get_actions(self) -> list[NCAction]:

def get_context(self) -> Context:
"""Return a context to be used in a method call."""
action_list = self.get_actions()

meta = self.tx.get_metadata()
timestamp: int
if meta.first_block is None:
# XXX Which timestamp to use when it is on mempool?
timestamp = self.tx.timestamp
else:
assert self.tx.storage is not None
first_block = self.tx.storage.get_transaction(meta.first_block)
timestamp = first_block.timestamp

from hathor.nanocontracts.context import Context
from hathor.nanocontracts.types import Address
context = Context(
actions=action_list,
vertex=self.tx,
return Context.create_from_vertex(
caller_id=Address(self.nc_address),
timestamp=timestamp,
vertex=self.tx,
actions=self.get_actions(),
)
return context
Loading
Loading