Implement RECEIPT_CLAIM transaction type
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
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
- 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
This commit is contained in:
@@ -229,15 +229,11 @@ class PoAProposer:
|
||||
|
||||
# Apply state transition through validated transaction
|
||||
state_transition = get_state_transition()
|
||||
tx_data = {
|
||||
"from": sender,
|
||||
"to": recipient,
|
||||
"value": value,
|
||||
"fee": fee,
|
||||
"nonce": sender_account.nonce
|
||||
}
|
||||
# Use original tx_data from mempool to preserve type and payload
|
||||
tx_data_for_transition = tx.content.copy()
|
||||
tx_data_for_transition["nonce"] = sender_account.nonce
|
||||
success, error_msg = state_transition.apply_transaction(
|
||||
session, self._config.chain_id, tx_data, tx.tx_hash
|
||||
session, self._config.chain_id, tx_data_for_transition, tx.tx_hash
|
||||
)
|
||||
|
||||
if not success:
|
||||
@@ -257,18 +253,30 @@ class PoAProposer:
|
||||
continue
|
||||
|
||||
# Create transaction record
|
||||
# Extract type from normalized tx_data (which should have the type field)
|
||||
tx_type = tx.content.get("type", "TRANSFER")
|
||||
self._logger.info(f"[PROPOSE] Transaction {tx.tx_hash} content type: {tx_type}, full content: {tx.content}")
|
||||
if tx_type:
|
||||
tx_type = tx_type.upper()
|
||||
else:
|
||||
tx_type = "TRANSFER"
|
||||
|
||||
# Store only the original payload, not the full normalized data
|
||||
original_payload = tx.content.get("payload", {})
|
||||
|
||||
transaction = Transaction(
|
||||
chain_id=self._config.chain_id,
|
||||
tx_hash=tx.tx_hash,
|
||||
sender=sender,
|
||||
recipient=recipient,
|
||||
payload=tx_data,
|
||||
payload=original_payload,
|
||||
value=value,
|
||||
fee=fee,
|
||||
nonce=sender_account.nonce - 1,
|
||||
timestamp=timestamp,
|
||||
block_height=next_height,
|
||||
status="confirmed"
|
||||
status="confirmed",
|
||||
type=tx_type
|
||||
)
|
||||
session.add(transaction)
|
||||
processed_txs.append(tx)
|
||||
|
||||
@@ -95,6 +95,7 @@ class Transaction(SQLModel, table=True):
|
||||
nonce: int = Field(default=0)
|
||||
value: int = Field(default=0)
|
||||
fee: int = Field(default=0)
|
||||
type: str = Field(default="TRANSFER", index=True)
|
||||
status: str = Field(default="pending")
|
||||
timestamp: Optional[str] = Field(default=None)
|
||||
tx_metadata: Optional[str] = Field(default=None)
|
||||
@@ -140,6 +141,9 @@ class Receipt(SQLModel, table=True):
|
||||
)
|
||||
minted_amount: Optional[int] = None
|
||||
recorded_at: datetime = Field(default_factory=datetime.utcnow, index=True)
|
||||
status: str = Field(default="pending", index=True) # pending, claimed, invalid
|
||||
claimed_at: Optional[datetime] = None
|
||||
claimed_by: Optional[str] = None
|
||||
|
||||
# Relationship
|
||||
block: Optional["Block"] = Relationship(
|
||||
|
||||
@@ -87,12 +87,28 @@ def _normalize_transaction_data(tx_data: Dict[str, Any], chain_id: str) -> Dict[
|
||||
if nonce < 0:
|
||||
raise ValueError("transaction.nonce must be non-negative")
|
||||
|
||||
payload = tx_data.get("payload", "0x")
|
||||
payload = tx_data.get("payload", {})
|
||||
if payload is None:
|
||||
payload = "0x"
|
||||
payload = {}
|
||||
|
||||
tx_type = tx_data.get("type", "TRANSFER")
|
||||
if tx_type:
|
||||
tx_type = tx_type.upper()
|
||||
|
||||
# Ensure payload is a dict
|
||||
if isinstance(payload, str):
|
||||
try:
|
||||
import json
|
||||
payload = json.loads(payload)
|
||||
except:
|
||||
payload = {}
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
|
||||
return {
|
||||
"chain_id": chain_id,
|
||||
"type": tx_type,
|
||||
"from": sender.strip(),
|
||||
"to": recipient.strip(),
|
||||
"amount": amount,
|
||||
@@ -174,7 +190,7 @@ def _serialize_receipt(receipt: Receipt) -> Dict[str, Any]:
|
||||
|
||||
class TransactionRequest(BaseModel):
|
||||
type: str = Field(description="Transaction type, e.g. TRANSFER, RECEIPT_CLAIM, GPU_MARKETPLACE, EXCHANGE, MESSAGE")
|
||||
sender: str
|
||||
sender: str = Field(alias="from") # Accept both "sender" and "from"
|
||||
nonce: int
|
||||
fee: int = Field(ge=0)
|
||||
payload: Dict[str, Any]
|
||||
@@ -271,15 +287,33 @@ async def get_block(height: int, chain_id: str = None) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@router.post("/transaction", summary="Submit transaction")
|
||||
async def submit_transaction(tx_data: dict) -> Dict[str, Any]:
|
||||
async def submit_transaction(tx_data: TransactionRequest) -> Dict[str, Any]:
|
||||
"""Submit a new transaction to the mempool"""
|
||||
from ..mempool import get_mempool
|
||||
|
||||
try:
|
||||
mempool = get_mempool()
|
||||
chain_id = tx_data.get("chain_id") or get_chain_id(None)
|
||||
chain_id = get_chain_id(None)
|
||||
|
||||
tx_data_dict = _normalize_transaction_data(tx_data, chain_id)
|
||||
# Convert TransactionRequest to dict for normalization
|
||||
# _normalize_transaction_data expects "from", not "sender"
|
||||
tx_data_dict = {
|
||||
"from": tx_data.sender,
|
||||
"to": tx_data.payload.get("to", tx_data.sender), # Get to from payload or default to sender
|
||||
"amount": tx_data.payload.get("amount", 0), # Get amount from payload
|
||||
"fee": tx_data.fee,
|
||||
"nonce": tx_data.nonce,
|
||||
"payload": tx_data.payload,
|
||||
"type": tx_data.type,
|
||||
"signature": tx_data.sig
|
||||
}
|
||||
|
||||
_logger.info(f"[ROUTER] Before normalization: type={tx_data.type}, full dict keys={list(tx_data_dict.keys())}")
|
||||
|
||||
tx_data_dict = _normalize_transaction_data(tx_data_dict, chain_id)
|
||||
|
||||
_logger.info(f"[ROUTER] After normalization: type={tx_data_dict.get('type')}, full dict keys={list(tx_data_dict.keys())}")
|
||||
|
||||
_validate_transaction_admission(tx_data_dict, mempool)
|
||||
|
||||
tx_hash = mempool.add(tx_data_dict, chain_id=chain_id)
|
||||
|
||||
@@ -10,8 +10,9 @@ 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
|
||||
from ..models import Account, Transaction, Receipt
|
||||
from ..logger import get_logger
|
||||
|
||||
|
||||
@@ -68,8 +69,27 @@ class StateTransition:
|
||||
if tx_nonce != expected_nonce:
|
||||
return False, f"Invalid nonce for {sender_addr}: expected {expected_nonce}, got {tx_nonce}"
|
||||
|
||||
# Get transaction type
|
||||
tx_type = tx_data.get("type", "TRANSFER").upper()
|
||||
# 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)
|
||||
@@ -88,14 +108,42 @@ class StateTransition:
|
||||
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)
|
||||
# Get recipient account (not required for MESSAGE or RECEIPT_CLAIM)
|
||||
recipient_addr = tx_data.get("to")
|
||||
if tx_type != "MESSAGE":
|
||||
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(
|
||||
@@ -128,8 +176,27 @@ class StateTransition:
|
||||
|
||||
sender_account = session.get(Account, (chain_id, sender_addr))
|
||||
|
||||
# Get transaction type
|
||||
tx_type = tx_data.get("type", "TRANSFER").upper()
|
||||
# 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)
|
||||
@@ -149,6 +216,30 @@ class StateTransition:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user