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
18 changes: 11 additions & 7 deletions pyjamaz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,16 +728,20 @@ async def store_block(self, block: Block):
await self.store_block_header(block.header)

async def store_block_header(self, header: Header):

self.block_db.put(
b'block_header:' + header.hash, header.to_jam_bytes().to_bytes()
)

header_hash = header.hash
header_bytes = header.to_jam_bytes().to_bytes()
header_key = b'block_header:' + header_hash
if self.block_db.get(header_key) != header_bytes:
self.block_db.put(header_key, header_bytes)

# Ancestor replay can select an earlier same-slot sibling again. Keep
# restoring both indexes even when its canonical bytes are persisted.
slot_bytes = header.timeslot.to_bytes(length=4, byteorder='little')
self.block_db.put(
b'block_hash:' + header.timeslot.to_bytes(length=4, byteorder='little'), header.hash
b'block_hash:' + slot_bytes, header_hash
)
self.block_db.put(
b'block_number:' + header.hash, header.timeslot.to_bytes(length=4, byteorder='little')
b'block_number:' + header_hash, slot_bytes
)

def retrieve_block(self, timeslot: int) -> Optional[Block]:
Expand Down
62 changes: 62 additions & 0 deletions pyjamaz/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import traceback
from asyncio import CancelledError
from dataclasses import fields, is_dataclass
from datetime import datetime, timezone
import json
import os
Expand All @@ -24,6 +25,7 @@
from pyjamaz.exceptions import StateKeyNoResult, StateTransitionError, BlockValidationError

from pyjamaz.graypaper_constants import COMMON_ERA, EPOCH_TIMESLOTS
from pyjamaz.hashing import blake2b_256_hash
from pyjamaz.logger import setup_logging
from pyjamaz.models.app import Trace, TraceGenesis
from pyjamaz.models.state import STORAGE_KEY_MAPPING, ServiceAccount
Expand Down Expand Up @@ -532,6 +534,63 @@ async def init(
async def fuzzer():
pass


def _trace_header_field_key(value):
"""Snapshot codec values with exact types, including mutable containers."""
value_type = type(value)
if value is None or value_type in (bytes, int, str, bool, float):
return value_type, value
if value_type is bytearray:
return value_type, bytes(value)
if value_type in (list, tuple):
return value_type, tuple(_trace_header_field_key(item) for item in value)
if is_dataclass(value) and not isinstance(value, type):
return value_type, tuple(
_trace_header_field_key(getattr(value, field.name)) for field in fields(value)
)
# An unfamiliar codec input may be mutable. Do not reuse its encoding.
return object()


class _TraceHeader(Header):
"""Cache replay header bytes without changing mutable runtime headers.

Compare all codec fields with a type-sensitive snapshot before reusing bytes.
This also detects edits inside epoch markers, tickets and offender lists;
derived fields such as the author's key do not affect the encoding.
"""

@classmethod
def from_header(cls, header: Header) -> "_TraceHeader":
return cls(**{field.name: getattr(header, field.name) for field in fields(Header)})

@classmethod
def to_codec_def(cls):
# Serializable otherwise changes the shared codec's deserialize type
# to this replay-only subclass.
return Header.to_codec_def()

def _encoded_bytes(self) -> bytes:
values = tuple(
_trace_header_field_key(getattr(self, field.name)) for field in fields(Header)
)
if values != getattr(self, '_cached_fields', None):
encoded = super().to_jam_bytes().to_bytes()
self._cached_fields = values
self._cached_bytes = encoded
self._cached_hash = blake2b_256_hash(encoded)
return self._cached_bytes

def to_jam_bytes(self) -> JamBytes:
# JamBytes has a mutable decode offset; callers need their own cursor.
return JamBytes(self._encoded_bytes())

@property
def hash(self) -> bytes:
self._encoded_bytes()
return self._cached_hash


def _trace_ancestor_headers(block: Block, known_headers: dict[bytes, Header]) -> List[Header]:
"""Resolve input-header ancestry, oldest first (GP-0.8.0-5.3,11.38).

Expand Down Expand Up @@ -602,6 +661,8 @@ async def replay_traces(
# Skip genesis creation
continue

trace.block.header = _TraceHeader.from_header(trace.block.header)

# Flush DB
for key, _ in app.state_db.as_list():
app.state_db.delete(key)
Expand All @@ -617,6 +678,7 @@ async def replay_traces(
genesis_file = block_file.parent / "genesis.bin"
if genesis_file.exists():
genesis = TraceGenesis.from_jam_bytes(JamBytes(genesis_file.read_bytes()))
genesis.header = _TraceHeader.from_header(genesis.header)
known_headers[bytes(genesis.header.hash)] = genesis.header
trace_parent = block_file.parent
ancestor_headers = _trace_ancestor_headers(trace.block, known_headers)
Expand Down
2 changes: 1 addition & 1 deletion pyjamaz/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

TEST_SUITE = 'tiny' # tiny or full
GP_VERSION = '0.8.0'
APP_VERSION = '0.3.0'
APP_VERSION = '0.3.2'


def _apply_jam_fuzz_spec_override():
Expand Down
Loading