From 68fa8072561a172a1e0b590b2456ca5af6ef91d5 Mon Sep 17 00:00:00 2001 From: aitbc Date: Fri, 10 Apr 2026 16:10:45 +0200 Subject: [PATCH] consensus: improve block proposal timing and return status, fix transaction field mapping in sync - Add initial sleep before first proposal to prevent immediate block creation - Return bool from _propose_block to indicate if block was actually proposed - Wait for next slot only after successful proposal, use regular interval after skipped proposals - Add 1s sleep after proposal errors to prevent tight error loops - Add parent_hash, proposer, and state_root fields to /rpc/blocks-range response - Fix transaction field mapping --- .../src/aitbc_chain/consensus/poa.py | 20 +++++++++++++++---- .../src/aitbc_chain/rpc/router.py | 5 ++++- apps/blockchain-node/src/aitbc_chain/sync.py | 7 +++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/blockchain-node/src/aitbc_chain/consensus/poa.py b/apps/blockchain-node/src/aitbc_chain/consensus/poa.py index 11ee4fa8..fecefde0 100755 --- a/apps/blockchain-node/src/aitbc_chain/consensus/poa.py +++ b/apps/blockchain-node/src/aitbc_chain/consensus/poa.py @@ -96,14 +96,24 @@ class PoAProposer: self._task = None async def _run_loop(self) -> None: + # Initial sleep so we don't start proposing immediately + await asyncio.sleep(self._config.interval_seconds) while not self._stop_event.is_set(): - await self._wait_until_next_slot() if self._stop_event.is_set(): break try: - await self._propose_block() + proposed = await self._propose_block() + if proposed: + await self._wait_until_next_slot() + else: + # If we skipped proposing, wait a regular interval + try: + await asyncio.wait_for(self._stop_event.wait(), timeout=self._config.interval_seconds) + except asyncio.TimeoutError: + pass except Exception as exc: # pragma: no cover - defensive logging self._logger.exception("Failed to propose block", extra={"error": str(exc)}) + await asyncio.sleep(1.0) async def _wait_until_next_slot(self) -> None: head = self._fetch_chain_head() @@ -119,7 +129,7 @@ class PoAProposer: except asyncio.TimeoutError: return - async def _propose_block(self) -> None: + async def _propose_block(self) -> bool: # Check internal mempool and include transactions from ..mempool import get_mempool from ..models import Transaction, Account @@ -131,7 +141,7 @@ class PoAProposer: mempool_size = mempool.size(self._config.chain_id) if mempool_size == 0: self._logger.info(f"[PROPOSE] Skipping block proposal: mempool is empty (chain={self._config.chain_id})") - return + return False with self._session_factory() as session: head = session.exec(select(Block).where(Block.chain_id == self._config.chain_id).order_by(Block.height.desc()).limit(1)).first() @@ -283,6 +293,8 @@ class PoAProposer: }, ) + return True + async def _ensure_genesis_block(self) -> None: with self._session_factory() as session: # Check if genesis block already exists diff --git a/apps/blockchain-node/src/aitbc_chain/rpc/router.py b/apps/blockchain-node/src/aitbc_chain/rpc/router.py index 653c337c..dd7cd554 100755 --- a/apps/blockchain-node/src/aitbc_chain/rpc/router.py +++ b/apps/blockchain-node/src/aitbc_chain/rpc/router.py @@ -254,10 +254,13 @@ async def get_blocks_range(start: int = 0, end: int = 10, include_tx: bool = Tru block_data = { "height": b.height, "hash": b.hash, + "parent_hash": b.parent_hash, + "proposer": b.proposer, "timestamp": b.timestamp.isoformat(), "tx_count": b.tx_count, + "state_root": b.state_root, } - + if include_tx: # Fetch transactions for this block txs = session.exec( diff --git a/apps/blockchain-node/src/aitbc_chain/sync.py b/apps/blockchain-node/src/aitbc_chain/sync.py index 54126e6e..4e840b90 100755 --- a/apps/blockchain-node/src/aitbc_chain/sync.py +++ b/apps/blockchain-node/src/aitbc_chain/sync.py @@ -288,10 +288,9 @@ class ChainSync: # Import transactions if provided and apply state changes if transactions: for tx_data in transactions: - sender_addr = tx_data.get("sender", "") - payload = tx_data.get("payload", {}) or {} - recipient_addr = payload.get("to") or tx_data.get("recipient", "") - value = int(payload.get("value", 0) or 0) + sender_addr = tx_data.get("from", "") + recipient_addr = tx_data.get("to", "") + value = int(tx_data.get("amount", 0) or 0) fee = int(tx_data.get("fee", 0) or 0) tx_hash = tx_data.get("tx_hash", "")