config: add option to prevent empty block proposals and improve genesis block handling
- 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:
@@ -43,6 +43,9 @@ class ChainSettings(BaseSettings):
|
|||||||
max_block_size_bytes: int = 1_000_000 # 1 MB
|
max_block_size_bytes: int = 1_000_000 # 1 MB
|
||||||
max_txs_per_block: int = 500
|
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)
|
# Monitoring interval (in seconds)
|
||||||
blockchain_monitoring_interval_seconds: int = 60
|
blockchain_monitoring_interval_seconds: int = 60
|
||||||
min_fee: int = 0 # Minimum fee to accept into mempool
|
min_fee: int = 0 # Minimum fee to accept into mempool
|
||||||
|
|||||||
@@ -123,8 +123,16 @@ class PoAProposer:
|
|||||||
# Check internal mempool and include transactions
|
# Check internal mempool and include transactions
|
||||||
from ..mempool import get_mempool
|
from ..mempool import get_mempool
|
||||||
from ..models import Transaction, Account
|
from ..models import Transaction, Account
|
||||||
|
from ..config import settings
|
||||||
mempool = get_mempool()
|
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:
|
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()
|
head = session.exec(select(Block).where(Block.chain_id == self._config.chain_id).order_by(Block.height.desc()).limit(1)).first()
|
||||||
next_height = 0
|
next_height = 0
|
||||||
@@ -154,31 +162,51 @@ class PoAProposer:
|
|||||||
value = tx_data.get("amount", 0)
|
value = tx_data.get("amount", 0)
|
||||||
fee = tx_data.get("fee", 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:
|
if not sender or not recipient:
|
||||||
|
self._logger.warning(f"[PROPOSE] Skipping tx {tx.tx_hash}: missing sender or recipient")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get sender account
|
# Get sender account
|
||||||
sender_account = session.get(Account, (self._config.chain_id, sender))
|
sender_account = session.get(Account, (self._config.chain_id, sender))
|
||||||
if not sender_account:
|
if not sender_account:
|
||||||
|
self._logger.warning(f"[PROPOSE] Skipping tx {tx.tx_hash}: sender account not found for {sender}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check sufficient balance
|
# Check sufficient balance
|
||||||
total_cost = value + fee
|
total_cost = value + fee
|
||||||
if sender_account.balance < total_cost:
|
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
|
continue
|
||||||
|
|
||||||
# Get or create recipient account
|
# Get or create recipient account
|
||||||
recipient_account = session.get(Account, (self._config.chain_id, recipient))
|
recipient_account = session.get(Account, (self._config.chain_id, recipient))
|
||||||
if not recipient_account:
|
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)
|
recipient_account = Account(chain_id=self._config.chain_id, address=recipient, balance=0, nonce=0)
|
||||||
session.add(recipient_account)
|
session.add(recipient_account)
|
||||||
session.flush()
|
session.flush()
|
||||||
|
else:
|
||||||
|
self._logger.info(f"[PROPOSE] Recipient account exists for {recipient}")
|
||||||
|
|
||||||
# Update balances
|
# Update balances
|
||||||
sender_account.balance -= total_cost
|
sender_account.balance -= total_cost
|
||||||
sender_account.nonce += 1
|
sender_account.nonce += 1
|
||||||
recipient_account.balance += value
|
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
|
# Create transaction record
|
||||||
transaction = Transaction(
|
transaction = Transaction(
|
||||||
chain_id=self._config.chain_id,
|
chain_id=self._config.chain_id,
|
||||||
@@ -195,6 +223,7 @@ class PoAProposer:
|
|||||||
)
|
)
|
||||||
session.add(transaction)
|
session.add(transaction)
|
||||||
processed_txs.append(tx)
|
processed_txs.append(tx)
|
||||||
|
self._logger.info(f"[PROPOSE] Successfully processed tx {tx.tx_hash}: updated balances")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._logger.warning(f"Failed to process transaction {tx.tx_hash}: {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:
|
async def _ensure_genesis_block(self) -> None:
|
||||||
with self._session_factory() as session:
|
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()
|
# Check if genesis block already exists
|
||||||
if head is not None:
|
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
|
return
|
||||||
|
|
||||||
# Use a deterministic genesis timestamp so all nodes agree on the genesis block hash
|
# Use a deterministic genesis timestamp so all nodes agree on the genesis block hash
|
||||||
timestamp = datetime(2025, 1, 1, 0, 0, 0)
|
timestamp = datetime(2025, 1, 1, 0, 0, 0)
|
||||||
block_hash = self._compute_block_hash(0, "0x00", timestamp)
|
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(
|
genesis = Block(
|
||||||
chain_id=self._config.chain_id,
|
chain_id=self._config.chain_id,
|
||||||
height=0,
|
height=0,
|
||||||
hash=block_hash,
|
hash=block_hash,
|
||||||
parent_hash="0x00",
|
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,
|
timestamp=timestamp,
|
||||||
tx_count=0,
|
tx_count=0,
|
||||||
state_root=None,
|
state_root=None,
|
||||||
)
|
)
|
||||||
session.add(genesis)
|
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)
|
# Initialize accounts from genesis allocations file (if present)
|
||||||
await self._initialize_genesis_allocations(session)
|
await self._initialize_genesis_allocations(session)
|
||||||
|
|||||||
Reference in New Issue
Block a user