consensus: improve block proposal timing and return status, fix transaction field mapping in sync
Some checks failed
Integration Tests / test-service-integration (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled

- 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
This commit is contained in:
aitbc
2026-04-10 16:10:45 +02:00
parent 632595b0ba
commit 68fa807256
3 changed files with 23 additions and 9 deletions

View File

@@ -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

View File

@@ -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(

View File

@@ -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", "")