docs: add cross-node agent communication achievements and fix blockchain sync issues
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
Documentation Validation / validate-docs (push) Has been cancelled
CLI Tests / test-cli (push) Has been cancelled
Systemd Sync / sync-systemd (push) Has been cancelled

- Document successful cross-node agent messaging implementation in README
- Add ping-pong test completion (Block 26952) between aitbc and aitbc1 nodes
- Document blockchain synchronization fixes (rate limiting disabled, blocks-range workaround)
- Add recent documentation links (cross-node communication, sync issues, training modules)
- Fix /rpc/blocks-range endpoint to include transaction data with include_tx parameter
This commit is contained in:
aitbc
2026-04-10 13:54:37 +02:00
parent 748264e44d
commit 56100f0099
11 changed files with 1561 additions and 28 deletions

View File

@@ -17,11 +17,14 @@ import os
import time
import argparse
import random
import hashlib
from pathlib import Path
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
import requests
from typing import Optional, Dict, Any, List
@@ -31,22 +34,64 @@ DEFAULT_KEYSTORE_DIR = Path("/var/lib/aitbc/keystore")
DEFAULT_RPC_URL = "http://localhost:8006"
def decrypt_private_key(keystore_path: Path, password: str) -> str:
"""Decrypt private key from keystore file"""
"""Decrypt private key from keystore file.
Supports both keystore formats:
- AES-256-GCM (blockchain-node standard)
- Fernet (scripts/utils standard)
"""
with open(keystore_path) as f:
ks = json.load(f)
crypto = ks['crypto']
salt = bytes.fromhex(crypto['kdfparams']['salt'])
kdf = PBKDF2HMAC(hashes.SHA256(), 32, salt, crypto['kdfparams']['c'])
key = kdf.derive(password.encode())
aesgcm = AESGCM(key)
nonce = bytes.fromhex(crypto['cipherparams']['nonce'])
priv = aesgcm.decrypt(nonce, bytes.fromhex(crypto['ciphertext']), None)
return priv.hex()
crypto = ks.get('crypto', ks) # Handle both nested and flat crypto structures
# Detect encryption method
cipher = crypto.get('cipher', crypto.get('algorithm', ''))
if cipher == 'aes-256-gcm' or cipher == 'aes-256-gcm':
# AES-256-GCM (blockchain-node standard)
salt = bytes.fromhex(crypto['kdfparams']['salt'])
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=crypto['kdfparams']['c'],
backend=default_backend()
)
key = kdf.derive(password.encode())
aesgcm = AESGCM(key)
nonce = bytes.fromhex(crypto['cipherparams']['nonce'])
priv = aesgcm.decrypt(nonce, bytes.fromhex(crypto['ciphertext']), None)
return priv.hex()
elif cipher == 'fernet' or cipher == 'PBKDF2-SHA256-Fernet':
# Fernet (scripts/utils standard)
from cryptography.fernet import Fernet
import base64
# Derive Fernet key using the same method as scripts/utils/keystore.py
kdfparams = crypto.get('kdfparams', {})
if 'salt' in kdfparams:
salt = base64.b64decode(kdfparams['salt'])
else:
# Fallback for older format
salt = bytes.fromhex(kdfparams.get('salt', ''))
# Simple KDF: hash(password + salt) - matches scripts/utils/keystore.py
dk = hashlib.sha256(password.encode() + salt).digest()
fernet_key = base64.urlsafe_b64encode(dk)
f = Fernet(fernet_key)
ciphertext = base64.b64decode(crypto['ciphertext'])
priv = f.decrypt(ciphertext)
return priv.decode()
else:
raise ValueError(f"Unsupported cipher: {cipher}")
def create_wallet(name: str, password: str, keystore_dir: Path = DEFAULT_KEYSTORE_DIR) -> str:
"""Create a new wallet"""
"""Create a new wallet using blockchain-node standard AES-256-GCM encryption"""
keystore_dir.mkdir(parents=True, exist_ok=True)
# Generate new key pair
@@ -58,15 +103,21 @@ def create_wallet(name: str, password: str, keystore_dir: Path = DEFAULT_KEYSTOR
# Calculate address (simplified - in real implementation this would be more complex)
address = f"ait1{public_key_hex[:40]}"
# Encrypt private key
# Encrypt private key using blockchain-node standard (AES-256-GCM with PBKDF2)
salt = os.urandom(32)
kdf = PBKDF2HMAC(hashes.SHA256(), 32, salt, 100000)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100_000,
backend=default_backend()
)
key = kdf.derive(password.encode())
aesgcm = AESGCM(key)
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, bytes.fromhex(private_key_hex), None)
# Create keystore file
# Create keystore file matching blockchain-node format
keystore_data = {
"address": address,
"public_key": public_key_hex,
@@ -74,7 +125,7 @@ def create_wallet(name: str, password: str, keystore_dir: Path = DEFAULT_KEYSTOR
"kdf": "pbkdf2",
"kdfparams": {
"salt": salt.hex(),
"c": 100000,
"c": 100_000,
"dklen": 32,
"prf": "hmac-sha256"
},
@@ -84,6 +135,7 @@ def create_wallet(name: str, password: str, keystore_dir: Path = DEFAULT_KEYSTOR
},
"ciphertext": ciphertext.hex()
},
"keytype": "ed25519",
"version": 1
}
@@ -877,6 +929,161 @@ def agent_operations(action: str, **kwargs) -> Optional[Dict]:
"total_count": len(agents)
}
elif action == "message":
# Send message via blockchain transaction payload
agent = kwargs.get("agent")
message = kwargs.get("message")
wallet = kwargs.get("wallet")
password = kwargs.get("password")
password_file = kwargs.get("password_file")
rpc_url = kwargs.get("rpc_url", DEFAULT_RPC_URL)
if not agent or not message:
print("Error: agent and message are required")
return None
if not wallet:
print("Error: wallet is required to send messages")
return None
# Get password
if password_file:
with open(password_file) as f:
password = f.read().strip()
elif not password:
print("Error: password or password_file is required")
return None
try:
# Decrypt wallet
keystore_path = DEFAULT_KEYSTORE_DIR / f"{wallet}.json"
private_key_hex = decrypt_private_key(keystore_path, password)
private_key_bytes = bytes.fromhex(private_key_hex)
# Get sender address
with open(keystore_path) as f:
keystore_data = json.load(f)
sender_address = keystore_data['address']
# Create transaction with message as payload
priv_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key_bytes)
pub_hex = priv_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
).hex()
tx = {
"type": "transfer",
"from": sender_address,
"to": agent,
"amount": 0,
"fee": 10,
"nonce": int(time.time() * 1000),
"payload": message,
"chain_id": "ait-mainnet"
}
# Sign transaction
tx_string = json.dumps(tx, sort_keys=True)
tx_hash = hashlib.sha256(tx_string.encode()).hexdigest()
tx["signature"] = priv_key.sign(tx_string.encode()).hex()
tx["public_key"] = pub_hex
# Submit transaction
response = requests.post(f"{rpc_url}/rpc/transaction", json=tx)
if response.status_code == 200:
result = response.json()
print(f"Message sent successfully")
print(f"From: {sender_address}")
print(f"To: {agent}")
print(f"Message: {message}")
print(f"Transaction Hash: {result.get('transaction_hash', 'N/A')}")
return {
"action": "message",
"status": "sent",
"transaction_hash": result.get('transaction_hash'),
"from": sender_address,
"to": agent,
"message": message
}
else:
print(f"Error sending message: {response.text}")
return None
except Exception as e:
print(f"Error sending message: {e}")
return None
elif action == "messages":
# Retrieve messages for an agent
agent = kwargs.get("agent")
wallet = kwargs.get("wallet")
rpc_url = kwargs.get("rpc_url", DEFAULT_RPC_URL)
if not agent:
print("Error: agent address is required")
return None
try:
# Since /rpc/transactions endpoint is not implemented, query local database
import sys
sys.path.insert(0, "/opt/aitbc/apps/blockchain-node/src")
from sqlmodel import create_engine, Session, select
from aitbc_chain.models import Transaction
engine = create_engine("sqlite:////var/lib/aitbc/data/ait-mainnet/chain.db")
with Session(engine) as session:
# Query transactions where recipient is the agent
txs = session.exec(
select(Transaction).where(Transaction.recipient == agent)
.order_by(Transaction.timestamp.desc())
.limit(50)
).all()
messages = []
for tx in txs:
# Extract payload
payload = ""
if hasattr(tx, "tx_metadata") and tx.tx_metadata:
if isinstance(tx.tx_metadata, dict):
payload = tx.tx_metadata.get("payload", "")
elif isinstance(tx.tx_metadata, str):
try:
payload = json.loads(tx.tx_metadata).get("payload", "")
except:
pass
elif hasattr(tx, "payload") and tx.payload:
if isinstance(tx.payload, dict):
payload = tx.payload.get("payload", "")
if payload: # Only include transactions with payloads
messages.append({
"from": tx.sender,
"message": payload,
"timestamp": tx.timestamp,
"block_height": tx.block_height,
"tx_hash": tx.tx_hash
})
print(f"Found {len(messages)} messages for {agent}")
for msg in messages:
print(f"From: {msg['from']}")
print(f"Message: {msg['message']}")
print(f"Block: {msg['block_height']}")
print(f"Time: {msg['timestamp']}")
print("-" * 40)
return {
"action": "messages",
"agent": agent,
"count": len(messages),
"messages": messages
}
except Exception as e:
print(f"Error retrieving messages: {e}")
return None
else:
return {"action": action, "status": "Not implemented yet"}