config: add option to prevent empty block proposals and improve genesis block handling
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 propose_only_if_mempool_not_empty config option (default: True) to skip block proposals when mempool is empty
- Add detailed logging for transaction processing in block proposals (sender/recipient validation, balance checks, duplicate detection)
- Check for existing transactions in database before adding to prevent duplicates
- Improve genesis block creation: check by height and hash, use "genesis" as proposer to avoid hash conflicts
- Add proper error handling and rollback for
This commit is contained in:
aitbc
2026-04-10 15:32:44 +02:00
parent 56100f0099
commit 632595b0ba
2 changed files with 51 additions and 5 deletions

View File

@@ -43,6 +43,9 @@ class ChainSettings(BaseSettings):
max_block_size_bytes: int = 1_000_000 # 1 MB
max_txs_per_block: int = 500
# Only propose blocks if mempool is not empty (prevents empty blocks)
propose_only_if_mempool_not_empty: bool = True
# Monitoring interval (in seconds)
blockchain_monitoring_interval_seconds: int = 60
min_fee: int = 0 # Minimum fee to accept into mempool

View File

@@ -123,8 +123,16 @@ class PoAProposer:
# Check internal mempool and include transactions
from ..mempool import get_mempool
from ..models import Transaction, Account
from ..config import settings
mempool = get_mempool()
# Check if we should only propose when mempool is not empty
if getattr(settings, "propose_only_if_mempool_not_empty", True):
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
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()
next_height = 0
@@ -154,31 +162,51 @@ class PoAProposer:
value = tx_data.get("amount", 0)
fee = tx_data.get("fee", 0)
self._logger.info(f"[PROPOSE] Processing tx {tx.tx_hash}: from={sender}, to={recipient}, amount={value}, fee={fee}")
if not sender or not recipient:
self._logger.warning(f"[PROPOSE] Skipping tx {tx.tx_hash}: missing sender or recipient")
continue
# Get sender account
sender_account = session.get(Account, (self._config.chain_id, sender))
if not sender_account:
self._logger.warning(f"[PROPOSE] Skipping tx {tx.tx_hash}: sender account not found for {sender}")
continue
# Check sufficient balance
total_cost = value + fee
if sender_account.balance < total_cost:
self._logger.warning(f"[PROPOSE] Skipping tx {tx.tx_hash}: insufficient balance (has {sender_account.balance}, needs {total_cost})")
continue
# Get or create recipient account
recipient_account = session.get(Account, (self._config.chain_id, recipient))
if not recipient_account:
self._logger.info(f"[PROPOSE] Creating recipient account for {recipient}")
recipient_account = Account(chain_id=self._config.chain_id, address=recipient, balance=0, nonce=0)
session.add(recipient_account)
session.flush()
else:
self._logger.info(f"[PROPOSE] Recipient account exists for {recipient}")
# Update balances
sender_account.balance -= total_cost
sender_account.nonce += 1
recipient_account.balance += value
# Check if transaction already exists in database
existing_tx = session.exec(
select(Transaction).where(
Transaction.chain_id == self._config.chain_id,
Transaction.tx_hash == tx.tx_hash
)
).first()
if existing_tx:
self._logger.warning(f"[PROPOSE] Skipping tx {tx.tx_hash}: already exists in database at block {existing_tx.block_height}")
continue
# Create transaction record
transaction = Transaction(
chain_id=self._config.chain_id,
@@ -195,6 +223,7 @@ class PoAProposer:
)
session.add(transaction)
processed_txs.append(tx)
self._logger.info(f"[PROPOSE] Successfully processed tx {tx.tx_hash}: updated balances")
except Exception as e:
self._logger.warning(f"Failed to process transaction {tx.tx_hash}: {e}")
@@ -256,25 +285,39 @@ class PoAProposer:
async def _ensure_genesis_block(self) -> None:
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()
if head is not None:
# Check if genesis block already exists
genesis = session.exec(select(Block).where(Block.chain_id == self._config.chain_id).where(Block.height == 0).limit(1)).first()
if genesis is not None:
self._logger.info(f"Genesis block already exists: height={genesis.height}, hash={genesis.hash}, proposer={genesis.proposer}")
return
# Use a deterministic genesis timestamp so all nodes agree on the genesis block hash
timestamp = datetime(2025, 1, 1, 0, 0, 0)
block_hash = self._compute_block_hash(0, "0x00", timestamp)
# Check if block with this hash already exists (duplicate check)
existing = session.exec(select(Block).where(Block.chain_id == self._config.chain_id).where(Block.hash == block_hash).limit(1)).first()
if existing is not None:
self._logger.info(f"Genesis block with hash {block_hash} already exists, skipping creation")
return
genesis = Block(
chain_id=self._config.chain_id,
height=0,
hash=block_hash,
parent_hash="0x00",
proposer=self._config.proposer_id, # Use configured proposer as genesis proposer
proposer="genesis", # Use "genesis" as the proposer for genesis block to avoid hash conflicts
timestamp=timestamp,
tx_count=0,
state_root=None,
)
session.add(genesis)
session.commit()
try:
session.commit()
except Exception as e:
self._logger.warning(f"Failed to create genesis block: {e}")
session.rollback()
return
# Initialize accounts from genesis allocations file (if present)
await self._initialize_genesis_allocations(session)