Files
aitbc/apps/blockchain-node/src/aitbc_chain/state/state_transition.py
aitbc f36fd45d28
Some checks failed
Blockchain Synchronization Verification / sync-verification (push) Successful in 4s
Documentation Validation / validate-docs (push) Successful in 12s
Documentation Validation / validate-policies-strict (push) Successful in 3s
Integration Tests / test-service-integration (push) Failing after 12s
Multi-Node Blockchain Health Monitoring / health-check (push) Successful in 3s
P2P Network Verification / p2p-verification (push) Successful in 2s
Python Tests / test-python (push) Successful in 10s
Security Scanning / security-scan (push) Successful in 31s
Implement RECEIPT_CLAIM transaction type
- Add status fields to Receipt model (status, claimed_at, claimed_by)
- Add RECEIPT_CLAIM handling to state_transition.py with validation and reward minting
- Add type field to Transaction model for reliable transaction type storage
- Update router to use TransactionRequest model to preserve type field
- Update poa.py to extract type from mempool transaction content and store only original payload
- Add RECEIPT_CLAIM to GasType enum with gas schedule
2026-04-22 13:35:31 +02:00

308 lines
11 KiB
Python

"""
State Transition Layer for AITBC
This module provides the StateTransition class that validates all state changes
to ensure they only occur through validated transactions.
"""
from __future__ import annotations
from typing import Dict, List, Optional, Tuple
from sqlmodel import Session, select
from datetime import datetime
from ..models import Account, Transaction, Receipt
from ..logger import get_logger
logger = get_logger(__name__)
class StateTransition:
"""
Validates and applies state transitions only through validated transactions.
This class ensures that balance changes can only occur through properly
validated transactions, preventing direct database manipulation of account
balances.
"""
def __init__(self):
self._processed_nonces: Dict[str, int] = {}
self._processed_tx_hashes: set = set()
def validate_transaction(
self,
session: Session,
chain_id: str,
tx_data: Dict,
tx_hash: str
) -> Tuple[bool, str]:
"""
Validate a transaction before applying state changes.
Args:
session: Database session
chain_id: Chain identifier
tx_data: Transaction data
tx_hash: Transaction hash
Returns:
Tuple of (is_valid, error_message)
"""
# Check for replay attacks
if tx_hash in self._processed_tx_hashes:
return False, f"Transaction {tx_hash} already processed (replay attack)"
# Get sender account
sender_addr = tx_data.get("from")
sender_account = session.get(Account, (chain_id, sender_addr))
if not sender_account:
return False, f"Sender account not found: {sender_addr}"
# Validate nonce
expected_nonce = sender_account.nonce
tx_nonce = tx_data.get("nonce", 0)
if tx_nonce != expected_nonce:
return False, f"Invalid nonce for {sender_addr}: expected {expected_nonce}, got {tx_nonce}"
# Get transaction type - check Transaction model first, then tx_data
tx_record = session.exec(
select(Transaction).where(
Transaction.chain_id == chain_id,
Transaction.tx_hash == tx_hash
)
).first()
if tx_record and tx_record.type:
tx_type = tx_record.type.upper()
else:
tx_type = tx_data.get("type", "TRANSFER")
if not tx_type or tx_type == "TRANSFER":
# Check if type is in payload
payload = tx_data.get("payload", {})
if isinstance(payload, dict):
tx_type = payload.get("type", "TRANSFER")
if tx_type:
tx_type = tx_type.upper()
else:
tx_type = "TRANSFER"
# Validate balance
value = tx_data.get("value", 0)
fee = tx_data.get("fee", 0)
# For MESSAGE transactions, value must be 0
if tx_type == "MESSAGE" and value != 0:
return False, f"MESSAGE transactions must have value=0, got {value}"
# For MESSAGE transactions, only check fee
if tx_type == "MESSAGE":
total_cost = fee
else:
total_cost = value + fee
if sender_account.balance < total_cost:
return False, f"Insufficient balance for {sender_addr}: {sender_account.balance} < {total_cost}"
# Get recipient account (not required for MESSAGE or RECEIPT_CLAIM)
recipient_addr = tx_data.get("to")
if tx_type not in {"MESSAGE", "RECEIPT_CLAIM"}:
recipient_account = session.get(Account, (chain_id, recipient_addr))
if not recipient_account:
return False, f"Recipient account not found: {recipient_addr}"
# For RECEIPT_CLAIM transactions, validate receipt exists
if tx_type == "RECEIPT_CLAIM":
receipt_id = tx_data.get("payload", {}).get("receipt_id")
if not receipt_id:
return False, "RECEIPT_CLAIM transactions must include receipt_id in payload"
receipt = session.exec(
select(Receipt).where(
Receipt.chain_id == chain_id,
Receipt.receipt_id == receipt_id
)
).first()
if not receipt:
return False, f"Receipt not found: {receipt_id}"
if receipt.status != "pending":
return False, f"Receipt already claimed or invalid: {receipt.status}"
# Basic signature validation (full validation requires aitbc_sdk)
# Check that miner_signature exists and is non-empty
if not receipt.miner_signature or not isinstance(receipt.miner_signature, dict):
return False, f"Receipt {receipt_id} has invalid miner signature"
# Check that coordinator_attestations exists and is non-empty
if not receipt.coordinator_attestations or not isinstance(receipt.coordinator_attestations, list):
return False, f"Receipt {receipt_id} has invalid coordinator attestations"
return True, "Transaction validated successfully"
def apply_transaction(
self,
session: Session,
chain_id: str,
tx_data: Dict,
tx_hash: str
) -> Tuple[bool, str]:
"""
Apply a validated transaction to update state.
Args:
session: Database session
chain_id: Chain identifier
tx_data: Transaction data
tx_hash: Transaction hash
Returns:
Tuple of (success, error_message)
"""
# Validate first
is_valid, error_msg = self.validate_transaction(session, chain_id, tx_data, tx_hash)
if not is_valid:
return False, error_msg
# Get accounts
sender_addr = tx_data.get("from")
recipient_addr = tx_data.get("to")
sender_account = session.get(Account, (chain_id, sender_addr))
# Get transaction type - check Transaction model first, then tx_data
tx_record = session.exec(
select(Transaction).where(
Transaction.chain_id == chain_id,
Transaction.tx_hash == tx_hash
)
).first()
if tx_record and tx_record.type:
tx_type = tx_record.type.upper()
else:
tx_type = tx_data.get("type", "TRANSFER")
if not tx_type or tx_type == "TRANSFER":
# Check if type is in payload
payload = tx_data.get("payload", {})
if isinstance(payload, dict):
tx_type = payload.get("type", "TRANSFER")
if tx_type:
tx_type = tx_type.upper()
else:
tx_type = "TRANSFER"
# Apply balance changes
value = tx_data.get("value", 0)
fee = tx_data.get("fee", 0)
# For MESSAGE transactions, only deduct fee
if tx_type == "MESSAGE":
total_cost = fee
else:
total_cost = value + fee
recipient_account = session.get(Account, (chain_id, recipient_addr))
sender_account.balance -= total_cost
sender_account.nonce += 1
# For MESSAGE transactions, skip recipient balance change
if tx_type != "MESSAGE":
recipient_account.balance += value
# For RECEIPT_CLAIM transactions, mint reward and update receipt status
if tx_type == "RECEIPT_CLAIM":
receipt_id = tx_data.get("payload", {}).get("receipt_id")
receipt = session.exec(
select(Receipt).where(
Receipt.chain_id == chain_id,
Receipt.receipt_id == receipt_id
)
).first()
if receipt and receipt.minted_amount:
# Mint reward to claimant (sender)
sender_account.balance += receipt.minted_amount
# Update receipt status
receipt.status = "claimed"
receipt.claimed_at = datetime.utcnow()
receipt.claimed_by = sender_addr
logger.info(
f"Claimed receipt {receipt_id}: "
f"minted_amount={receipt.minted_amount}, claimed_by={sender_addr}"
)
# Mark transaction as processed
self._processed_tx_hashes.add(tx_hash)
self._processed_nonces[sender_addr] = sender_account.nonce
logger.info(
f"Applied transaction {tx_hash}: "
f"{sender_addr} -> {recipient_addr}, value={value}, fee={fee}, type={tx_type}"
)
return True, "Transaction applied successfully"
def validate_state_transition(
self,
session: Session,
chain_id: str,
old_accounts: Dict[str, Account],
new_accounts: Dict[str, Account]
) -> Tuple[bool, str]:
"""
Validate that state changes only occur through transactions.
Args:
session: Database session
chain_id: Chain identifier
old_accounts: Previous account state
new_accounts: New account state
Returns:
Tuple of (is_valid, error_message)
"""
for address, old_acc in old_accounts.items():
if address not in new_accounts:
continue
new_acc = new_accounts[address]
# Check if balance changed
if old_acc.balance != new_acc.balance:
# Balance changes should only occur through transactions
# This is a placeholder for full validation
logger.warning(
f"Balance change detected for {address}: "
f"{old_acc.balance} -> {new_acc.balance} "
f"(should be validated through transactions)"
)
return True, "State transition validated"
def get_processed_nonces(self) -> Dict[str, int]:
"""Get the last processed nonce for each address."""
return self._processed_nonces.copy()
def reset(self) -> None:
"""Reset the state transition validator (for testing)."""
self._processed_nonces.clear()
self._processed_tx_hashes.clear()
# Global state transition instance
_state_transition = StateTransition()
def get_state_transition() -> StateTransition:
"""Get the global state transition instance."""
return _state_transition