chore(security): enhance environment configuration, CI workflows, and wallet daemon with security improvements

- Restructure .env.example with security-focused documentation, service-specific environment file references, and AWS Secrets Manager integration
- Update CLI tests workflow to single Python 3.13 version, add pytest-mock dependency, and consolidate test execution with coverage
- Add comprehensive security validation to package publishing workflow with manual approval gates, secret scanning, and release
This commit is contained in:
oib
2026-03-03 10:33:46 +01:00
parent 00d00cb964
commit f353e00172
220 changed files with 42506 additions and 921 deletions

View File

@@ -16,7 +16,7 @@ def admin():
@admin.command()
@click.pass_context
def status(ctx):
"""Get system status"""
"""Show system status"""
config = ctx.obj['config']
try:
@@ -30,13 +30,77 @@ def status(ctx):
status_data = response.json()
output(status_data, ctx.obj['output_format'])
else:
error(f"Failed to get system status: {response.status_code}")
error(f"Failed to get status: {response.status_code}")
ctx.exit(1)
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.option("--output", type=click.Path(), help="Output report to file")
@click.pass_context
def audit_verify(ctx, output):
"""Verify audit log integrity"""
audit_logger = AuditLogger()
is_valid, issues = audit_logger.verify_integrity()
if is_valid:
success("Audit log integrity verified - no tampering detected")
else:
error("Audit log integrity compromised!")
for issue in issues:
error(f" - {issue}")
ctx.exit(1)
# Export detailed report if requested
if output:
try:
report = audit_logger.export_report(Path(output))
success(f"Audit report exported to {output}")
# Show summary
stats = report["audit_report"]["statistics"]
output({
"total_entries": stats["total_entries"],
"unique_actions": stats["unique_actions"],
"unique_users": stats["unique_users"],
"date_range": stats["date_range"]
}, ctx.obj['output_format'])
except Exception as e:
error(f"Failed to export report: {e}")
@admin.command()
@click.option("--limit", default=50, help="Number of entries to show")
@click.option("--action", help="Filter by action type")
@click.option("--search", help="Search query")
@click.pass_context
def audit_logs(ctx, limit: int, action: str, search: str):
"""View audit logs with integrity verification"""
audit_logger = AuditLogger()
try:
if search:
entries = audit_logger.search_logs(search, limit)
else:
entries = audit_logger.get_logs(limit, action)
if not entries:
warning("No audit entries found")
return
# Show entries
output({
"total_entries": len(entries),
"entries": entries
}, ctx.obj['output_format'])
except Exception as e:
error(f"Failed to read audit logs: {e}")
ctx.exit(1)
@admin.command()
@click.option("--limit", default=50, help="Number of jobs to show")
@click.option("--status", help="Filter by status")

View File

@@ -546,9 +546,9 @@ def progress(ctx, agent_id: str, metrics: str):
@click.argument("agent_id")
@click.option("--format", default="onnx", type=click.Choice(["onnx", "pickle", "torch"]),
help="Export format")
@click.option("--output", type=click.Path(), help="Output file path")
@click.option("--output-path", type=click.Path(), help="Output file path")
@click.pass_context
def export(ctx, agent_id: str, format: str, output: Optional[str]):
def export(ctx, agent_id: str, format: str, output_path: Optional[str]):
"""Export learned agent model"""
config = ctx.obj['config']
@@ -563,10 +563,10 @@ def export(ctx, agent_id: str, format: str, output: Optional[str]):
)
if response.status_code == 200:
if output:
with open(output, 'wb') as f:
if output_path:
with open(output_path, 'wb') as f:
f.write(response.content)
success(f"Model exported to {output}")
success(f"Model exported to {output_path}")
else:
# Output metadata about the export
export_info = response.headers.get('X-Export-Info', '{}')

View File

@@ -25,7 +25,7 @@ def simulate():
@click.pass_context
def init(ctx, distribute: str, reset: bool):
"""Initialize test economy"""
home_dir = Path("/home/oib/windsurf/aitbc/home")
home_dir = Path("/home/oib/windsurf/aitbc/tests/e2e/fixtures/home")
if reset:
success("Resetting simulation...")
@@ -115,7 +115,7 @@ def user():
@click.pass_context
def create(ctx, type: str, name: str, balance: float):
"""Create a test user"""
home_dir = Path("/home/oib/windsurf/aitbc/home")
home_dir = Path("/home/oib/windsurf/aitbc/tests/e2e/fixtures/home")
user_id = f"{type}_{name}"
wallet_path = home_dir / f"{user_id}_wallet.json"
@@ -151,7 +151,7 @@ def create(ctx, type: str, name: str, balance: float):
@click.pass_context
def list(ctx):
"""List all test users"""
home_dir = Path("/home/oib/windsurf/aitbc/home")
home_dir = Path("/home/oib/windsurf/aitbc/tests/e2e/fixtures/home")
users = []
for wallet_file in home_dir.glob("*_wallet.json"):
@@ -181,7 +181,7 @@ def list(ctx):
@click.pass_context
def balance(ctx, user: str):
"""Check user balance"""
home_dir = Path("/home/oib/windsurf/aitbc/home")
home_dir = Path("/home/oib/windsurf/aitbc/tests/e2e/fixtures/home")
wallet_path = home_dir / f"{user}_wallet.json"
if not wallet_path.exists():
@@ -203,7 +203,7 @@ def balance(ctx, user: str):
@click.pass_context
def fund(ctx, user: str, amount: float):
"""Fund a test user"""
home_dir = Path("/home/oib/windsurf/aitbc/home")
home_dir = Path("/home/oib/windsurf/aitbc/tests/e2e/fixtures/home")
# Load genesis wallet
genesis_path = home_dir / "genesis_wallet.json"

View File

@@ -0,0 +1,467 @@
"""
AITBC CLI Testing Commands
Provides testing and debugging utilities for the AITBC CLI
"""
import click
import json
import time
import tempfile
from pathlib import Path
from typing import Dict, Any, Optional
from unittest.mock import Mock, patch
from ..utils import output, success, error, warning
from ..config import get_config
@click.group()
def test():
"""Testing and debugging commands for AITBC CLI"""
pass
@test.command()
@click.option('--format', type=click.Choice(['json', 'table', 'yaml']), default='table', help='Output format')
@click.pass_context
def environment(ctx, format):
"""Test CLI environment and configuration"""
config = ctx.obj['config']
env_info = {
'coordinator_url': config.coordinator_url,
'api_key': config.api_key,
'output_format': ctx.obj['output_format'],
'test_mode': ctx.obj['test_mode'],
'dry_run': ctx.obj['dry_run'],
'timeout': ctx.obj['timeout'],
'no_verify': ctx.obj['no_verify'],
'log_level': ctx.obj['log_level']
}
if format == 'json':
output(json.dumps(env_info, indent=2))
else:
output("CLI Environment Test Results:")
output(f" Coordinator URL: {env_info['coordinator_url']}")
output(f" API Key: {env_info['api_key'][:10]}..." if env_info['api_key'] else " API Key: None")
output(f" Output Format: {env_info['output_format']}")
output(f" Test Mode: {env_info['test_mode']}")
output(f" Dry Run: {env_info['dry_run']}")
output(f" Timeout: {env_info['timeout']}s")
output(f" No Verify: {env_info['no_verify']}")
output(f" Log Level: {env_info['log_level']}")
@test.command()
@click.option('--endpoint', default='health', help='API endpoint to test')
@click.option('--method', default='GET', help='HTTP method')
@click.option('--data', help='JSON data to send (for POST/PUT)')
@click.pass_context
def api(ctx, endpoint, method, data):
"""Test API connectivity"""
config = ctx.obj['config']
try:
import httpx
# Prepare request
url = f"{config.coordinator_url.rstrip('/')}/api/v1/{endpoint.lstrip('/')}"
headers = {}
if config.api_key:
headers['Authorization'] = f"Bearer {config.api_key}"
# Prepare data
json_data = None
if data and method in ['POST', 'PUT']:
json_data = json.loads(data)
# Make request
with httpx.Client(verify=not ctx.obj['no_verify'], timeout=ctx.obj['timeout']) as client:
if method == 'GET':
response = client.get(url, headers=headers)
elif method == 'POST':
response = client.post(url, headers=headers, json=json_data)
elif method == 'PUT':
response = client.put(url, headers=headers, json=json_data)
else:
raise ValueError(f"Unsupported method: {method}")
# Display results
output(f"API Test Results:")
output(f" URL: {url}")
output(f" Method: {method}")
output(f" Status Code: {response.status_code}")
output(f" Response Time: {response.elapsed.total_seconds():.3f}s")
if response.status_code == 200:
success("✅ API test successful")
try:
response_data = response.json()
output("Response Data:")
output(json.dumps(response_data, indent=2))
except:
output(f"Response: {response.text}")
else:
error(f"❌ API test failed with status {response.status_code}")
output(f"Response: {response.text}")
except ImportError:
error("❌ httpx not installed. Install with: pip install httpx")
except Exception as e:
error(f"❌ API test failed: {str(e)}")
@test.command()
@click.option('--wallet-name', default='test-wallet', help='Test wallet name')
@click.option('--test-operations', is_flag=True, default=True, help='Test wallet operations')
@click.pass_context
def wallet(ctx, wallet_name, test_operations):
"""Test wallet functionality"""
from ..commands.wallet import wallet as wallet_cmd
output(f"Testing wallet functionality with wallet: {wallet_name}")
# Test wallet creation
try:
result = ctx.invoke(wallet_cmd, ['create', wallet_name])
if result.exit_code == 0:
success(f"✅ Wallet '{wallet_name}' created successfully")
else:
error(f"❌ Wallet creation failed: {result.output}")
return
except Exception as e:
error(f"❌ Wallet creation error: {str(e)}")
return
if test_operations:
# Test wallet balance
try:
result = ctx.invoke(wallet_cmd, ['balance'])
if result.exit_code == 0:
success("✅ Wallet balance check successful")
output(f"Balance output: {result.output}")
else:
warning(f"⚠️ Wallet balance check failed: {result.output}")
except Exception as e:
warning(f"⚠️ Wallet balance check error: {str(e)}")
# Test wallet info
try:
result = ctx.invoke(wallet_cmd, ['info'])
if result.exit_code == 0:
success("✅ Wallet info check successful")
output(f"Info output: {result.output}")
else:
warning(f"⚠️ Wallet info check failed: {result.output}")
except Exception as e:
warning(f"⚠️ Wallet info check error: {str(e)}")
@test.command()
@click.option('--job-type', default='ml_inference', help='Type of job to test')
@click.option('--test-data', default='{"model": "test-model", "input": "test-data"}', help='Test job data')
@click.pass_context
def job(ctx, job_type, test_data):
"""Test job submission and management"""
from ..commands.client import client as client_cmd
output(f"Testing job submission with type: {job_type}")
try:
# Parse test data
job_data = json.loads(test_data)
job_data['type'] = job_type
# Test job submission
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump(job_data, f)
temp_file = f.name
try:
result = ctx.invoke(client_cmd, ['submit', '--job-file', temp_file])
if result.exit_code == 0:
success("✅ Job submission successful")
output(f"Submission output: {result.output}")
# Extract job ID if present
if 'job_id' in result.output:
import re
job_id_match = re.search(r'job[_\s-]?id[:\s]+(\w+)', result.output, re.IGNORECASE)
if job_id_match:
job_id = job_id_match.group(1)
output(f"Extracted job ID: {job_id}")
# Test job status
try:
status_result = ctx.invoke(client_cmd, ['status', job_id])
if status_result.exit_code == 0:
success("✅ Job status check successful")
output(f"Status output: {status_result.output}")
else:
warning(f"⚠️ Job status check failed: {status_result.output}")
except Exception as e:
warning(f"⚠️ Job status check error: {str(e)}")
else:
error(f"❌ Job submission failed: {result.output}")
finally:
# Clean up temp file
Path(temp_file).unlink(missing_ok=True)
except json.JSONDecodeError:
error(f"❌ Invalid test data JSON: {test_data}")
except Exception as e:
error(f"❌ Job test failed: {str(e)}")
@test.command()
@click.option('--gpu-type', default='RTX 3080', help='GPU type to test')
@click.option('--price', type=float, default=0.1, help='Price to test')
@click.pass_context
def marketplace(ctx, gpu_type, price):
"""Test marketplace functionality"""
from ..commands.marketplace import marketplace as marketplace_cmd
output(f"Testing marketplace functionality for {gpu_type} at {price} AITBC/hour")
# Test marketplace offers listing
try:
result = ctx.invoke(marketplace_cmd, ['offers', 'list'])
if result.exit_code == 0:
success("✅ Marketplace offers list successful")
output(f"Offers output: {result.output}")
else:
warning(f"⚠️ Marketplace offers list failed: {result.output}")
except Exception as e:
warning(f"⚠️ Marketplace offers list error: {str(e)}")
# Test marketplace pricing
try:
result = ctx.invoke(marketplace_cmd, ['pricing', gpu_type])
if result.exit_code == 0:
success("✅ Marketplace pricing check successful")
output(f"Pricing output: {result.output}")
else:
warning(f"⚠️ Marketplace pricing check failed: {result.output}")
except Exception as e:
warning(f"⚠️ Marketplace pricing check error: {str(e)}")
@test.command()
@click.option('--test-endpoints', is_flag=True, default=True, help='Test blockchain endpoints')
@click.pass_context
def blockchain(ctx, test_endpoints):
"""Test blockchain functionality"""
from ..commands.blockchain import blockchain as blockchain_cmd
output("Testing blockchain functionality")
if test_endpoints:
# Test blockchain info
try:
result = ctx.invoke(blockchain_cmd, ['info'])
if result.exit_code == 0:
success("✅ Blockchain info successful")
output(f"Info output: {result.output}")
else:
warning(f"⚠️ Blockchain info failed: {result.output}")
except Exception as e:
warning(f"⚠️ Blockchain info error: {str(e)}")
# Test chain status
try:
result = ctx.invoke(blockchain_cmd, ['status'])
if result.exit_code == 0:
success("✅ Blockchain status successful")
output(f"Status output: {result.output}")
else:
warning(f"⚠️ Blockchain status failed: {result.output}")
except Exception as e:
warning(f"⚠️ Blockchain status error: {str(e)}")
@test.command()
@click.option('--component', help='Specific component to test (wallet, job, marketplace, blockchain, api)')
@click.option('--verbose', is_flag=True, help='Verbose test output')
@click.pass_context
def integration(ctx, component, verbose):
"""Run integration tests"""
if component:
output(f"Running integration tests for: {component}")
if component == 'wallet':
ctx.invoke(wallet, ['--test-operations'])
elif component == 'job':
ctx.invoke(job, [])
elif component == 'marketplace':
ctx.invoke(marketplace, [])
elif component == 'blockchain':
ctx.invoke(blockchain, [])
elif component == 'api':
ctx.invoke(api, ['--endpoint', 'health'])
else:
error(f"Unknown component: {component}")
return
else:
output("Running full integration test suite...")
# Test API connectivity first
output("1. Testing API connectivity...")
ctx.invoke(api, ['--endpoint', 'health'])
# Test wallet functionality
output("2. Testing wallet functionality...")
ctx.invoke(wallet, ['--wallet-name', 'integration-test-wallet'])
# Test marketplace functionality
output("3. Testing marketplace functionality...")
ctx.invoke(marketplace, [])
# Test blockchain functionality
output("4. Testing blockchain functionality...")
ctx.invoke(blockchain, [])
# Test job functionality
output("5. Testing job functionality...")
ctx.invoke(job, [])
success("✅ Integration test suite completed")
@test.command()
@click.option('--output-file', help='Save test results to file')
@click.pass_context
def diagnostics(ctx, output_file):
"""Run comprehensive diagnostics"""
diagnostics_data = {
'timestamp': time.time(),
'test_mode': ctx.obj['test_mode'],
'dry_run': ctx.obj['dry_run'],
'config': {
'coordinator_url': ctx.obj['config'].coordinator_url,
'api_key_present': bool(ctx.obj['config'].api_key),
'output_format': ctx.obj['output_format']
}
}
output("Running comprehensive diagnostics...")
# Test 1: Environment
output("1. Testing environment...")
try:
ctx.invoke(environment, ['--format', 'json'])
diagnostics_data['environment'] = 'PASS'
except Exception as e:
diagnostics_data['environment'] = f'FAIL: {str(e)}'
error(f"Environment test failed: {str(e)}")
# Test 2: API Connectivity
output("2. Testing API connectivity...")
try:
ctx.invoke(api, ['--endpoint', 'health'])
diagnostics_data['api_connectivity'] = 'PASS'
except Exception as e:
diagnostics_data['api_connectivity'] = f'FAIL: {str(e)}'
error(f"API connectivity test failed: {str(e)}")
# Test 3: Wallet Creation
output("3. Testing wallet creation...")
try:
ctx.invoke(wallet, ['--wallet-name', 'diagnostics-test', '--test-operations'])
diagnostics_data['wallet_creation'] = 'PASS'
except Exception as e:
diagnostics_data['wallet_creation'] = f'FAIL: {str(e)}'
error(f"Wallet creation test failed: {str(e)}")
# Test 4: Marketplace
output("4. Testing marketplace...")
try:
ctx.invoke(marketplace, [])
diagnostics_data['marketplace'] = 'PASS'
except Exception as e:
diagnostics_data['marketplace'] = f'FAIL: {str(e)}'
error(f"Marketplace test failed: {str(e)}")
# Generate summary
passed_tests = sum(1 for v in diagnostics_data.values() if isinstance(v, str) and v == 'PASS')
total_tests = len([k for k in diagnostics_data.keys() if k in ['environment', 'api_connectivity', 'wallet_creation', 'marketplace']])
diagnostics_data['summary'] = {
'total_tests': total_tests,
'passed_tests': passed_tests,
'failed_tests': total_tests - passed_tests,
'success_rate': (passed_tests / total_tests * 100) if total_tests > 0 else 0
}
# Display results
output("\n" + "="*50)
output("DIAGNOSTICS SUMMARY")
output("="*50)
output(f"Total Tests: {diagnostics_data['summary']['total_tests']}")
output(f"Passed: {diagnostics_data['summary']['passed_tests']}")
output(f"Failed: {diagnostics_data['summary']['failed_tests']}")
output(f"Success Rate: {diagnostics_data['summary']['success_rate']:.1f}%")
if diagnostics_data['summary']['success_rate'] == 100:
success("✅ All diagnostics passed!")
else:
warning(f"⚠️ {diagnostics_data['summary']['failed_tests']} test(s) failed")
# Save to file if requested
if output_file:
with open(output_file, 'w') as f:
json.dump(diagnostics_data, f, indent=2)
output(f"Diagnostics saved to: {output_file}")
@test.command()
def mock():
"""Generate mock data for testing"""
mock_data = {
'wallet': {
'name': 'test-wallet',
'address': 'aitbc1test123456789abcdef',
'balance': 1000.0,
'transactions': []
},
'job': {
'id': 'test-job-123',
'type': 'ml_inference',
'status': 'pending',
'requirements': {
'gpu_type': 'RTX 3080',
'memory_gb': 8,
'duration_minutes': 30
}
},
'marketplace': {
'offers': [
{
'id': 'offer-1',
'provider': 'test-provider',
'gpu_type': 'RTX 3080',
'price_per_hour': 0.1,
'available': True
}
]
},
'blockchain': {
'chain_id': 'aitbc-testnet',
'block_height': 1000,
'network_status': 'active'
}
}
output("Mock data for testing:")
output(json.dumps(mock_data, indent=2))
# Save to temp file
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump(mock_data, f, indent=2)
temp_file = f.name
output(f"Mock data saved to: {temp_file}")
return temp_file

View File

@@ -727,8 +727,12 @@ def send(ctx, to_address: str, amount: float, description: Optional[str]):
wallet_data["transactions"].append(transaction)
wallet_data["balance"] = balance - amount
with open(wallet_path, "w") as f:
json.dump(wallet_data, f, indent=2)
# Use _save_wallet to preserve encryption
if wallet_data.get("encrypted"):
password = _get_wallet_password(wallet_name)
_save_wallet(wallet_path, wallet_data, password)
else:
_save_wallet(wallet_path, wallet_data)
success(f"Sent {amount} AITBC to {to_address}")
output(
@@ -932,8 +936,7 @@ def unstake(ctx, stake_id: str):
error(f"Wallet '{wallet_name}' not found")
return
with open(wallet_path, "r") as f:
wallet_data = json.load(f)
wallet_data = _load_wallet(wallet_path, wallet_name)
staking = wallet_data.get("staking", [])
stake_record = next(
@@ -1145,13 +1148,85 @@ def multisig_propose(
)
@wallet.command(name="multisig-challenge")
@click.option("--wallet", "wallet_name", required=True, help="Multisig wallet name")
@click.argument("tx_id")
@click.pass_context
def multisig_challenge(ctx, wallet_name: str, tx_id: str):
"""Create a cryptographic challenge for multisig transaction signing"""
wallet_dir = ctx.obj.get("wallet_dir", Path.home() / ".aitbc" / "wallets")
multisig_path = wallet_dir / f"{wallet_name}_multisig.json"
if not multisig_path.exists():
error(f"Multisig wallet '{wallet_name}' not found")
return
with open(multisig_path) as f:
ms_data = json.load(f)
# Find pending transaction
pending = ms_data.get("pending_transactions", [])
tx = next(
(t for t in pending if t["tx_id"] == tx_id and t["status"] == "pending"), None
)
if not tx:
error(f"Pending transaction '{tx_id}' not found")
return
# Import crypto utilities
from ..utils.crypto_utils import multisig_security
try:
# Create signing request
signing_request = multisig_security.create_signing_request(tx, wallet_name)
output({
"tx_id": tx_id,
"wallet": wallet_name,
"challenge": signing_request["challenge"],
"nonce": signing_request["nonce"],
"message": signing_request["message"],
"instructions": [
"1. Copy the challenge string above",
"2. Sign it with your private key using: aitbc wallet sign-challenge <challenge> <private-key>",
"3. Use the returned signature with: aitbc wallet multisig-sign --wallet <wallet> <tx_id> --signer <address> --signature <signature>"
]
}, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Failed to create challenge: {e}")
@wallet.command(name="sign-challenge")
@click.argument("challenge")
@click.argument("private_key")
@click.pass_context
def sign_challenge(ctx, challenge: str, private_key: str):
"""Sign a cryptographic challenge (for testing multisig)"""
from ..utils.crypto_utils import sign_challenge
try:
signature = sign_challenge(challenge, private_key)
output({
"challenge": challenge,
"signature": signature,
"message": "Use this signature with multisig-sign command"
}, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Failed to sign challenge: {e}")
@wallet.command(name="multisig-sign")
@click.option("--wallet", "wallet_name", required=True, help="Multisig wallet name")
@click.argument("tx_id")
@click.option("--signer", required=True, help="Signer address")
@click.option("--signature", required=True, help="Cryptographic signature (hex)")
@click.pass_context
def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str):
"""Sign a pending multisig transaction"""
def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str, signature: str):
"""Sign a pending multisig transaction with cryptographic verification"""
wallet_dir = ctx.obj.get("wallet_dir", Path.home() / ".aitbc" / "wallets")
multisig_path = wallet_dir / f"{wallet_name}_multisig.json"
@@ -1167,6 +1242,16 @@ def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str):
ctx.exit(1)
return
# Import crypto utilities
from ..utils.crypto_utils import multisig_security
# Verify signature cryptographically
success, message = multisig_security.verify_and_add_signature(tx_id, signature, signer)
if not success:
error(f"Signature verification failed: {message}")
ctx.exit(1)
return
pending = ms_data.get("pending_transactions", [])
tx = next(
(t for t in pending if t["tx_id"] == tx_id and t["status"] == "pending"), None
@@ -1177,11 +1262,21 @@ def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str):
ctx.exit(1)
return
if signer in tx["signatures"]:
error(f"'{signer}' has already signed this transaction")
return
# Check if already signed
for sig in tx.get("signatures", []):
if sig["signer"] == signer:
error(f"'{signer}' has already signed this transaction")
return
tx["signatures"].append(signer)
# Add cryptographic signature
if "signatures" not in tx:
tx["signatures"] = []
tx["signatures"].append({
"signer": signer,
"signature": signature,
"timestamp": datetime.now().isoformat()
})
# Check if threshold met
if len(tx["signatures"]) >= ms_data["threshold"]: